This is page 5 of 12. Use http://codebase.md/cameroncooke/xcodebuildmcp?lines=false&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/utils/test-common.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Common Test Utilities - Shared logic for test tools
*
* This module provides shared functionality for all test-related tools across different platforms.
* It includes common test execution logic, xcresult parsing, and utility functions used by
* platform-specific test tools.
*
* Responsibilities:
* - Parsing xcresult bundles into human-readable format
* - Shared test execution logic with platform-specific handling
* - Common error handling and cleanup for test operations
* - Temporary directory management for xcresult files
*/
import { promisify } from 'util';
import { exec } from 'child_process';
import { mkdtemp, rm } from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';
import { log } from './logger.ts';
import { XcodePlatform } from './xcode.ts';
import { executeXcodeBuildCommand } from './build/index.ts';
import { createTextResponse, consolidateContentForClaudeCode } from './validation.ts';
import { normalizeTestRunnerEnv } from './environment.ts';
import { ToolResponse } from '../types/common.ts';
import { CommandExecutor, CommandExecOptions, getDefaultCommandExecutor } from './command.ts';
/**
* Type definition for test summary structure from xcresulttool
*/
interface TestSummary {
title?: string;
result?: string;
totalTestCount?: number;
passedTests?: number;
failedTests?: number;
skippedTests?: number;
expectedFailures?: number;
environmentDescription?: string;
devicesAndConfigurations?: Array<{
device?: {
deviceName?: string;
platform?: string;
osVersion?: string;
};
}>;
testFailures?: Array<{
testName?: string;
targetName?: string;
failureText?: string;
}>;
topInsights?: Array<{
impact?: string;
text?: string;
}>;
}
/**
* Parse xcresult bundle using xcrun xcresulttool
*/
export async function parseXcresultBundle(resultBundlePath: string): Promise<string> {
try {
const execAsync = promisify(exec);
const { stdout } = await execAsync(
`xcrun xcresulttool get test-results summary --path "${resultBundlePath}"`,
);
// Parse JSON response and format as human-readable
const summary = JSON.parse(stdout) as TestSummary;
return formatTestSummary(summary);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log('error', `Error parsing xcresult bundle: ${errorMessage}`);
throw error;
}
}
/**
* Format test summary JSON into human-readable text
*/
function formatTestSummary(summary: TestSummary): string {
const lines: string[] = [];
lines.push(`Test Summary: ${summary.title ?? 'Unknown'}`);
lines.push(`Overall Result: ${summary.result ?? 'Unknown'}`);
lines.push('');
lines.push('Test Counts:');
lines.push(` Total: ${summary.totalTestCount ?? 0}`);
lines.push(` Passed: ${summary.passedTests ?? 0}`);
lines.push(` Failed: ${summary.failedTests ?? 0}`);
lines.push(` Skipped: ${summary.skippedTests ?? 0}`);
lines.push(` Expected Failures: ${summary.expectedFailures ?? 0}`);
lines.push('');
if (summary.environmentDescription) {
lines.push(`Environment: ${summary.environmentDescription}`);
lines.push('');
}
if (
summary.devicesAndConfigurations &&
Array.isArray(summary.devicesAndConfigurations) &&
summary.devicesAndConfigurations.length > 0
) {
const device = summary.devicesAndConfigurations[0].device;
if (device) {
lines.push(
`Device: ${device.deviceName ?? 'Unknown'} (${device.platform ?? 'Unknown'} ${device.osVersion ?? 'Unknown'})`,
);
lines.push('');
}
}
if (
summary.testFailures &&
Array.isArray(summary.testFailures) &&
summary.testFailures.length > 0
) {
lines.push('Test Failures:');
summary.testFailures.forEach((failure, index: number) => {
lines.push(
` ${index + 1}. ${failure.testName ?? 'Unknown Test'} (${failure.targetName ?? 'Unknown Target'})`,
);
if (failure.failureText) {
lines.push(` ${failure.failureText}`);
}
});
lines.push('');
}
if (summary.topInsights && Array.isArray(summary.topInsights) && summary.topInsights.length > 0) {
lines.push('Insights:');
summary.topInsights.forEach((insight, index: number) => {
lines.push(
` ${index + 1}. [${insight.impact ?? 'Unknown'}] ${insight.text ?? 'No description'}`,
);
});
}
return lines.join('\n');
}
/**
* Internal logic for running tests with platform-specific handling
*/
export async function handleTestLogic(
params: {
workspacePath?: string;
projectPath?: string;
scheme: string;
configuration: string;
simulatorName?: string;
simulatorId?: string;
deviceId?: string;
useLatestOS?: boolean;
derivedDataPath?: string;
extraArgs?: string[];
preferXcodebuild?: boolean;
platform: XcodePlatform;
testRunnerEnv?: Record<string, string>;
},
executor?: CommandExecutor,
): Promise<ToolResponse> {
log(
'info',
`Starting test run for scheme ${params.scheme} on platform ${params.platform} (internal)`,
);
try {
// Create temporary directory for xcresult bundle
const tempDir = await mkdtemp(join(tmpdir(), 'xcodebuild-test-'));
const resultBundlePath = join(tempDir, 'TestResults.xcresult');
// Add resultBundlePath to extraArgs
const extraArgs = [...(params.extraArgs ?? []), `-resultBundlePath`, resultBundlePath];
// Prepare execution options with TEST_RUNNER_ environment variables
const execOpts: CommandExecOptions | undefined = params.testRunnerEnv
? { env: normalizeTestRunnerEnv(params.testRunnerEnv) }
: undefined;
// Run the test command
const testResult = await executeXcodeBuildCommand(
{
...params,
extraArgs,
},
{
platform: params.platform,
simulatorName: params.simulatorName,
simulatorId: params.simulatorId,
deviceId: params.deviceId,
useLatestOS: params.useLatestOS,
logPrefix: 'Test Run',
},
params.preferXcodebuild,
'test',
executor ?? getDefaultCommandExecutor(),
execOpts,
);
// Parse xcresult bundle if it exists, regardless of whether tests passed or failed
// Test failures are expected and should not prevent xcresult parsing
try {
log('info', `Attempting to parse xcresult bundle at: ${resultBundlePath}`);
// Check if the file exists
try {
const { stat } = await import('fs/promises');
await stat(resultBundlePath);
log('info', `xcresult bundle exists at: ${resultBundlePath}`);
} catch {
log('warn', `xcresult bundle does not exist at: ${resultBundlePath}`);
throw new Error(`xcresult bundle not found at ${resultBundlePath}`);
}
const testSummary = await parseXcresultBundle(resultBundlePath);
log('info', 'Successfully parsed xcresult bundle');
// Clean up temporary directory
await rm(tempDir, { recursive: true, force: true });
// Return combined result - preserve isError from testResult (test failures should be marked as errors)
const combinedResponse: ToolResponse = {
content: [
...(testResult.content || []),
{
type: 'text',
text: '\nTest Results Summary:\n' + testSummary,
},
],
isError: testResult.isError,
};
// Apply Claude Code workaround if enabled
return consolidateContentForClaudeCode(combinedResponse);
} catch (parseError) {
// If parsing fails, return original test result
log('warn', `Failed to parse xcresult bundle: ${parseError}`);
// Clean up temporary directory even if parsing fails
try {
await rm(tempDir, { recursive: true, force: true });
} catch (cleanupError) {
log('warn', `Failed to clean up temporary directory: ${cleanupError}`);
}
return consolidateContentForClaudeCode(testResult);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log('error', `Error during test run: ${errorMessage}`);
return consolidateContentForClaudeCode(
createTextResponse(`Error during test run: ${errorMessage}`, true),
);
}
}
```
--------------------------------------------------------------------------------
/src/mcp/tools/macos/build_run_macos.ts:
--------------------------------------------------------------------------------
```typescript
/**
* macOS Shared Plugin: Build and Run macOS (Unified)
*
* Builds and runs a macOS app from a project or workspace in one step.
* Accepts mutually exclusive `projectPath` or `workspacePath`.
*/
import * as z from 'zod';
import { log } from '../../../utils/logging/index.ts';
import { createTextResponse } from '../../../utils/responses/index.ts';
import { executeXcodeBuildCommand } from '../../../utils/build/index.ts';
import { ToolResponse, XcodePlatform } from '../../../types/common.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
createSessionAwareTool,
getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
// Unified schema: XOR between projectPath and workspacePath
const baseSchemaObject = z.object({
projectPath: z.string().optional().describe('Path to the .xcodeproj file'),
workspacePath: z.string().optional().describe('Path to the .xcworkspace file'),
scheme: z.string().describe('The scheme to use'),
configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
derivedDataPath: z
.string()
.optional()
.describe('Path where build products and other derived data will go'),
arch: z
.enum(['arm64', 'x86_64'])
.optional()
.describe('Architecture to build for (arm64 or x86_64). For macOS only.'),
extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'),
preferXcodebuild: z
.boolean()
.optional()
.describe('If true, prefers xcodebuild over the experimental incremental build system'),
});
const publicSchemaObject = baseSchemaObject.omit({
projectPath: true,
workspacePath: true,
scheme: true,
configuration: true,
arch: true,
} as const);
const buildRunMacOSSchema = z.preprocess(
nullifyEmptyStrings,
baseSchemaObject
.refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
message: 'Either projectPath or workspacePath is required.',
})
.refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
}),
);
export type BuildRunMacOSParams = z.infer<typeof buildRunMacOSSchema>;
/**
* Internal logic for building macOS apps.
*/
async function _handleMacOSBuildLogic(
params: BuildRunMacOSParams,
executor: CommandExecutor,
): Promise<ToolResponse> {
log('info', `Starting macOS build for scheme ${params.scheme} (internal)`);
return executeXcodeBuildCommand(
{
...params,
configuration: params.configuration ?? 'Debug',
},
{
platform: XcodePlatform.macOS,
arch: params.arch,
logPrefix: 'macOS Build',
},
params.preferXcodebuild ?? false,
'build',
executor,
);
}
async function _getAppPathFromBuildSettings(
params: BuildRunMacOSParams,
executor: CommandExecutor,
): Promise<{ success: true; appPath: string } | { success: false; error: string }> {
try {
// Create the command array for xcodebuild
const command = ['xcodebuild', '-showBuildSettings'];
// Add the project or workspace
if (params.projectPath) {
command.push('-project', params.projectPath);
} else if (params.workspacePath) {
command.push('-workspace', params.workspacePath);
}
// Add the scheme and configuration
command.push('-scheme', params.scheme);
command.push('-configuration', params.configuration ?? 'Debug');
// Add derived data path if provided
if (params.derivedDataPath) {
command.push('-derivedDataPath', params.derivedDataPath);
}
// Add extra args if provided
if (params.extraArgs && params.extraArgs.length > 0) {
command.push(...params.extraArgs);
}
// Execute the command directly
const result = await executor(command, 'Get Build Settings for Launch', true, undefined);
if (!result.success) {
return {
success: false,
error: result.error ?? 'Failed to get build settings',
};
}
// Parse the output to extract the app path
const buildSettingsOutput = result.output;
const builtProductsDirMatch = buildSettingsOutput.match(/^\s*BUILT_PRODUCTS_DIR\s*=\s*(.+)$/m);
const fullProductNameMatch = buildSettingsOutput.match(/^\s*FULL_PRODUCT_NAME\s*=\s*(.+)$/m);
if (!builtProductsDirMatch || !fullProductNameMatch) {
return { success: false, error: 'Could not extract app path from build settings' };
}
const appPath = `${builtProductsDirMatch[1].trim()}/${fullProductNameMatch[1].trim()}`;
return { success: true, appPath };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return { success: false, error: errorMessage };
}
}
/**
* Business logic for building and running macOS apps.
*/
export async function buildRunMacOSLogic(
params: BuildRunMacOSParams,
executor: CommandExecutor,
): Promise<ToolResponse> {
log('info', 'Handling macOS build & run logic...');
try {
// First, build the app
const buildResult = await _handleMacOSBuildLogic(params, executor);
// 1. Check if the build itself failed
if (buildResult.isError) {
return buildResult; // Return build failure directly
}
const buildWarningMessages = buildResult.content?.filter((c) => c.type === 'text') ?? [];
// 2. Build succeeded, now get the app path using the helper
const appPathResult = await _getAppPathFromBuildSettings(params, executor);
// 3. Check if getting the app path failed
if (!appPathResult.success) {
log('error', 'Build succeeded, but failed to get app path to launch.');
const response = createTextResponse(
`✅ Build succeeded, but failed to get app path to launch: ${appPathResult.error}`,
false, // Build succeeded, so not a full error
);
if (response.content) {
response.content.unshift(...buildWarningMessages);
}
return response;
}
const appPath = appPathResult.appPath; // success === true narrows to string
log('info', `App path determined as: ${appPath}`);
// 4. Launch the app using CommandExecutor
const launchResult = await executor(['open', appPath], 'Launch macOS App', true);
if (!launchResult.success) {
log('error', `Build succeeded, but failed to launch app ${appPath}: ${launchResult.error}`);
const errorResponse = createTextResponse(
`✅ Build succeeded, but failed to launch app ${appPath}. Error: ${launchResult.error}`,
false, // Build succeeded
);
if (errorResponse.content) {
errorResponse.content.unshift(...buildWarningMessages);
}
return errorResponse;
}
log('info', `✅ macOS app launched successfully: ${appPath}`);
const successResponse: ToolResponse = {
content: [
...buildWarningMessages,
{
type: 'text',
text: `✅ macOS build and run succeeded for scheme ${params.scheme}. App launched: ${appPath}`,
},
],
isError: false,
};
return successResponse;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log('error', `Error during macOS build & run logic: ${errorMessage}`);
const errorResponse = createTextResponse(
`Error during macOS build and run: ${errorMessage}`,
true,
);
return errorResponse;
}
}
export default {
name: 'build_run_macos',
description: 'Builds and runs a macOS app.',
schema: getSessionAwareToolSchemaShape({
sessionAware: publicSchemaObject,
legacy: baseSchemaObject,
}),
annotations: {
title: 'Build Run macOS',
destructiveHint: true,
},
handler: createSessionAwareTool<BuildRunMacOSParams>({
internalSchema: buildRunMacOSSchema as unknown as z.ZodType<BuildRunMacOSParams, unknown>,
logicFunction: buildRunMacOSLogic,
getExecutor: getDefaultCommandExecutor,
requirements: [
{ allOf: ['scheme'], message: 'scheme is required' },
{ oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
],
exclusivePairs: [['projectPath', 'workspacePath']],
}),
};
```
--------------------------------------------------------------------------------
/src/mcp/tools/device/__tests__/stop_app_device.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Tests for stop_app_device plugin (device-shared)
* Following CLAUDE.md testing standards with literal validation
* Using dependency injection for deterministic testing
*/
import { describe, it, expect, beforeEach } from 'vitest';
import * as z from 'zod';
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
import stopAppDevice, { stop_app_deviceLogic } from '../stop_app_device.ts';
import { sessionStore } from '../../../../utils/session-store.ts';
describe('stop_app_device plugin', () => {
beforeEach(() => {
sessionStore.clear();
});
describe('Export Field Validation (Literal)', () => {
it('should have correct name', () => {
expect(stopAppDevice.name).toBe('stop_app_device');
});
it('should have correct description', () => {
expect(stopAppDevice.description).toBe('Stops a running app on a connected device.');
});
it('should have handler function', () => {
expect(typeof stopAppDevice.handler).toBe('function');
});
it('should require processId in public schema', () => {
const schema = z.strictObject(stopAppDevice.schema);
expect(schema.safeParse({ processId: 12345 }).success).toBe(true);
expect(schema.safeParse({}).success).toBe(false);
expect(schema.safeParse({ deviceId: 'test-device-123' }).success).toBe(false);
expect(Object.keys(stopAppDevice.schema)).toEqual(['processId']);
});
});
describe('Handler Requirements', () => {
it('should require deviceId when not provided', async () => {
const result = await stopAppDevice.handler({ processId: 12345 });
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('deviceId is required');
});
});
describe('Command Generation', () => {
it('should generate correct devicectl command with basic parameters', async () => {
let capturedCommand: string[] = [];
let capturedDescription: string = '';
let capturedUseShell: boolean = false;
let capturedEnv: Record<string, string> | undefined = undefined;
const mockExecutor = createMockExecutor({
success: true,
output: 'App terminated successfully',
process: { pid: 12345 },
});
const trackingExecutor = async (
command: string[],
description?: string,
useShell?: boolean,
opts?: { env?: Record<string, string> },
_detached?: boolean,
) => {
capturedCommand = command;
capturedDescription = description ?? '';
capturedUseShell = !!useShell;
capturedEnv = opts?.env;
return mockExecutor(command, description, useShell, opts, _detached);
};
await stop_app_deviceLogic(
{
deviceId: 'test-device-123',
processId: 12345,
},
trackingExecutor,
);
expect(capturedCommand).toEqual([
'xcrun',
'devicectl',
'device',
'process',
'terminate',
'--device',
'test-device-123',
'--pid',
'12345',
]);
expect(capturedDescription).toBe('Stop app on device');
expect(capturedUseShell).toBe(true);
expect(capturedEnv).toBe(undefined);
});
it('should generate correct command with different device ID and process ID', async () => {
let capturedCommand: string[] = [];
const mockExecutor = createMockExecutor({
success: true,
output: 'Process terminated',
process: { pid: 12345 },
});
const trackingExecutor = async (command: string[]) => {
capturedCommand = command;
return mockExecutor(command);
};
await stop_app_deviceLogic(
{
deviceId: 'different-device-uuid',
processId: 99999,
},
trackingExecutor,
);
expect(capturedCommand).toEqual([
'xcrun',
'devicectl',
'device',
'process',
'terminate',
'--device',
'different-device-uuid',
'--pid',
'99999',
]);
});
it('should generate correct command with large process ID', async () => {
let capturedCommand: string[] = [];
const mockExecutor = createMockExecutor({
success: true,
output: 'Process terminated',
process: { pid: 12345 },
});
const trackingExecutor = async (command: string[]) => {
capturedCommand = command;
return mockExecutor(command);
};
await stop_app_deviceLogic(
{
deviceId: 'test-device-123',
processId: 2147483647,
},
trackingExecutor,
);
expect(capturedCommand).toEqual([
'xcrun',
'devicectl',
'device',
'process',
'terminate',
'--device',
'test-device-123',
'--pid',
'2147483647',
]);
});
});
describe('Success Path Tests', () => {
it('should return successful stop response', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: 'App terminated successfully',
});
const result = await stop_app_deviceLogic(
{
deviceId: 'test-device-123',
processId: 12345,
},
mockExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: '✅ App stopped successfully\n\nApp terminated successfully',
},
],
});
});
it('should return successful stop with detailed output', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: 'Terminating process...\nProcess ID: 12345\nTermination completed successfully',
});
const result = await stop_app_deviceLogic(
{
deviceId: 'device-456',
processId: 67890,
},
mockExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: '✅ App stopped successfully\n\nTerminating process...\nProcess ID: 12345\nTermination completed successfully',
},
],
});
});
it('should return successful stop with empty output', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: '',
});
const result = await stop_app_deviceLogic(
{
deviceId: 'empty-output-device',
processId: 54321,
},
mockExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: '✅ App stopped successfully\n\n',
},
],
});
});
});
describe('Error Handling', () => {
it('should return stop failure response', async () => {
const mockExecutor = createMockExecutor({
success: false,
error: 'Terminate failed: Process not found',
});
const result = await stop_app_deviceLogic(
{
deviceId: 'test-device-123',
processId: 99999,
},
mockExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Failed to stop app: Terminate failed: Process not found',
},
],
isError: true,
});
});
it('should return exception handling response', async () => {
const mockExecutor = createMockExecutor(new Error('Network error'));
const result = await stop_app_deviceLogic(
{
deviceId: 'test-device-123',
processId: 12345,
},
mockExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Failed to stop app on device: Network error',
},
],
isError: true,
});
});
it('should return string error handling response', async () => {
const mockExecutor = createMockExecutor('String error');
const result = await stop_app_deviceLogic(
{
deviceId: 'test-device-123',
processId: 12345,
},
mockExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Failed to stop app on device: String error',
},
],
isError: true,
});
});
});
});
```
--------------------------------------------------------------------------------
/src/mcp/tools/ui-testing/swipe.ts:
--------------------------------------------------------------------------------
```typescript
/**
* UI Testing Plugin: Swipe
*
* Swipe from one coordinate to another on iOS simulator with customizable duration and delta.
*/
import * as z from 'zod';
import { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.ts';
import { DependencyError, AxeError, SystemError } from '../../../utils/errors.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultDebuggerManager } from '../../../utils/debugger/index.ts';
import type { DebuggerManager } from '../../../utils/debugger/debugger-manager.ts';
import { guardUiAutomationAgainstStoppedDebugger } from '../../../utils/debugger/ui-automation-guard.ts';
import {
createAxeNotAvailableResponse,
getAxePath,
getBundledAxeEnvironment,
} from '../../../utils/axe-helpers.ts';
import {
createSessionAwareTool,
getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
// Define schema as ZodObject
const swipeSchema = z.object({
simulatorId: z.uuid({ message: 'Invalid Simulator UUID format' }),
x1: z.number().int({ message: 'Start X coordinate' }),
y1: z.number().int({ message: 'Start Y coordinate' }),
x2: z.number().int({ message: 'End X coordinate' }),
y2: z.number().int({ message: 'End Y coordinate' }),
duration: z.number().min(0, { message: 'Duration must be non-negative' }).optional(),
delta: z.number().min(0, { message: 'Delta must be non-negative' }).optional(),
preDelay: z.number().min(0, { message: 'Pre-delay must be non-negative' }).optional(),
postDelay: z.number().min(0, { message: 'Post-delay must be non-negative' }).optional(),
});
// Use z.infer for type safety
export type SwipeParams = z.infer<typeof swipeSchema>;
const publicSchemaObject = z.strictObject(swipeSchema.omit({ simulatorId: true } as const).shape);
export interface AxeHelpers {
getAxePath: () => string | null;
getBundledAxeEnvironment: () => Record<string, string>;
createAxeNotAvailableResponse: () => ToolResponse;
}
const LOG_PREFIX = '[AXe]';
/**
* Core swipe logic implementation
*/
export async function swipeLogic(
params: SwipeParams,
executor: CommandExecutor,
axeHelpers: AxeHelpers = {
getAxePath,
getBundledAxeEnvironment,
createAxeNotAvailableResponse,
},
debuggerManager: DebuggerManager = getDefaultDebuggerManager(),
): Promise<ToolResponse> {
const toolName = 'swipe';
const { simulatorId, x1, y1, x2, y2, duration, delta, preDelay, postDelay } = params;
const guard = await guardUiAutomationAgainstStoppedDebugger({
debugger: debuggerManager,
simulatorId,
toolName,
});
if (guard.blockedResponse) return guard.blockedResponse;
const commandArgs = [
'swipe',
'--start-x',
String(x1),
'--start-y',
String(y1),
'--end-x',
String(x2),
'--end-y',
String(y2),
];
if (duration !== undefined) {
commandArgs.push('--duration', String(duration));
}
if (delta !== undefined) {
commandArgs.push('--delta', String(delta));
}
if (preDelay !== undefined) {
commandArgs.push('--pre-delay', String(preDelay));
}
if (postDelay !== undefined) {
commandArgs.push('--post-delay', String(postDelay));
}
const optionsText = duration ? ` duration=${duration}s` : '';
log(
'info',
`${LOG_PREFIX}/${toolName}: Starting swipe (${x1},${y1})->(${x2},${y2})${optionsText} on ${simulatorId}`,
);
try {
await executeAxeCommand(commandArgs, simulatorId, 'swipe', executor, axeHelpers);
log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`);
const coordinateWarning = getCoordinateWarning(simulatorId);
const message = `Swipe from (${x1}, ${y1}) to (${x2}, ${y2})${optionsText} simulated successfully.`;
const warnings = [guard.warningText, coordinateWarning].filter(Boolean).join('\n\n');
if (warnings) {
return createTextResponse(`${message}\n\n${warnings}`);
}
return createTextResponse(message);
} catch (error) {
log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`);
if (error instanceof DependencyError) {
return axeHelpers.createAxeNotAvailableResponse();
} else if (error instanceof AxeError) {
return createErrorResponse(`Failed to simulate swipe: ${error.message}`, error.axeOutput);
} else if (error instanceof SystemError) {
return createErrorResponse(
`System error executing axe: ${error.message}`,
error.originalError?.stack,
);
}
return createErrorResponse(
`An unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
export default {
name: 'swipe',
description:
"Swipe from one point to another. Use describe_ui for precise coordinates (don't guess from screenshots). Supports configurable timing.",
schema: getSessionAwareToolSchemaShape({
sessionAware: publicSchemaObject,
legacy: swipeSchema,
}),
annotations: {
title: 'Swipe',
destructiveHint: true,
},
handler: createSessionAwareTool<SwipeParams>({
internalSchema: swipeSchema as unknown as z.ZodType<SwipeParams>,
logicFunction: (params: SwipeParams, executor: CommandExecutor) =>
swipeLogic(params, executor, {
getAxePath,
getBundledAxeEnvironment,
createAxeNotAvailableResponse,
}),
getExecutor: getDefaultCommandExecutor,
requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
}),
};
// Session tracking for describe_ui warnings
interface DescribeUISession {
timestamp: number;
simulatorId: string;
}
const describeUITimestamps = new Map<string, DescribeUISession>();
const DESCRIBE_UI_WARNING_TIMEOUT = 60000; // 60 seconds
function getCoordinateWarning(simulatorId: string): string | null {
const session = describeUITimestamps.get(simulatorId);
if (!session) {
return 'Warning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.';
}
const timeSinceDescribe = Date.now() - session.timestamp;
if (timeSinceDescribe > DESCRIBE_UI_WARNING_TIMEOUT) {
const secondsAgo = Math.round(timeSinceDescribe / 1000);
return `Warning: describe_ui was last called ${secondsAgo} seconds ago. Consider refreshing UI coordinates with describe_ui instead of using potentially stale coordinates.`;
}
return null;
}
// Helper function for executing axe commands (inlined from src/tools/axe/index.ts)
async function executeAxeCommand(
commandArgs: string[],
simulatorId: string,
commandName: string,
executor: CommandExecutor = getDefaultCommandExecutor(),
axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse },
): Promise<void> {
// Get the appropriate axe binary path
const axeBinary = axeHelpers.getAxePath();
if (!axeBinary) {
throw new DependencyError('AXe binary not found');
}
// Add --udid parameter to all commands
const fullArgs = [...commandArgs, '--udid', simulatorId];
// Construct the full command array with the axe binary as the first element
const fullCommand = [axeBinary, ...fullArgs];
try {
// Determine environment variables for bundled AXe
const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined;
const result = await executor(
fullCommand,
`${LOG_PREFIX}: ${commandName}`,
false,
axeEnv ? { env: axeEnv } : undefined,
);
if (!result.success) {
throw new AxeError(
`axe command '${commandName}' failed.`,
commandName,
result.error ?? result.output,
simulatorId,
);
}
// Check for stderr output in successful commands
if (result.error) {
log(
'warn',
`${LOG_PREFIX}: Command '${commandName}' produced stderr output but exited successfully. Output: ${result.error}`,
);
}
// Function now returns void - the calling code creates its own response
} catch (error) {
if (error instanceof Error) {
if (error instanceof AxeError) {
throw error;
}
// Otherwise wrap it in a SystemError
throw new SystemError(`Failed to execute axe command: ${error.message}`, error);
}
// For any other type of error
throw new SystemError(`Failed to execute axe command: ${String(error)}`);
}
}
```
--------------------------------------------------------------------------------
/src/mcp/tools/ui-testing/gesture.ts:
--------------------------------------------------------------------------------
```typescript
/**
* UI Testing Plugin: Gesture
*
* Perform gesture on iOS simulator using preset gestures: scroll-up, scroll-down, scroll-left, scroll-right,
* swipe-from-left-edge, swipe-from-right-edge, swipe-from-top-edge, swipe-from-bottom-edge.
*/
import * as z from 'zod';
import { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import {
createTextResponse,
createErrorResponse,
DependencyError,
AxeError,
SystemError,
} from '../../../utils/responses/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultDebuggerManager } from '../../../utils/debugger/index.ts';
import type { DebuggerManager } from '../../../utils/debugger/debugger-manager.ts';
import { guardUiAutomationAgainstStoppedDebugger } from '../../../utils/debugger/ui-automation-guard.ts';
import {
createAxeNotAvailableResponse,
getAxePath,
getBundledAxeEnvironment,
} from '../../../utils/axe/index.ts';
import {
createSessionAwareTool,
getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
// Define schema as ZodObject
const gestureSchema = z.object({
simulatorId: z.uuid({ message: 'Invalid Simulator UUID format' }),
preset: z
.enum([
'scroll-up',
'scroll-down',
'scroll-left',
'scroll-right',
'swipe-from-left-edge',
'swipe-from-right-edge',
'swipe-from-top-edge',
'swipe-from-bottom-edge',
])
.describe(
'The gesture preset to perform. Must be one of: scroll-up, scroll-down, scroll-left, scroll-right, swipe-from-left-edge, swipe-from-right-edge, swipe-from-top-edge, swipe-from-bottom-edge.',
),
screenWidth: z
.number()
.int()
.min(1)
.optional()
.describe(
'Optional: Screen width in pixels. Used for gesture calculations. Auto-detected if not provided.',
),
screenHeight: z
.number()
.int()
.min(1)
.optional()
.describe(
'Optional: Screen height in pixels. Used for gesture calculations. Auto-detected if not provided.',
),
duration: z
.number()
.min(0, { message: 'Duration must be non-negative' })
.optional()
.describe('Optional: Duration of the gesture in seconds.'),
delta: z
.number()
.min(0, { message: 'Delta must be non-negative' })
.optional()
.describe('Optional: Distance to move in pixels.'),
preDelay: z
.number()
.min(0, { message: 'Pre-delay must be non-negative' })
.optional()
.describe('Optional: Delay before starting the gesture in seconds.'),
postDelay: z
.number()
.min(0, { message: 'Post-delay must be non-negative' })
.optional()
.describe('Optional: Delay after completing the gesture in seconds.'),
});
// Use z.infer for type safety
type GestureParams = z.infer<typeof gestureSchema>;
export interface AxeHelpers {
getAxePath: () => string | null;
getBundledAxeEnvironment: () => Record<string, string>;
createAxeNotAvailableResponse: () => ToolResponse;
}
const LOG_PREFIX = '[AXe]';
export async function gestureLogic(
params: GestureParams,
executor: CommandExecutor,
axeHelpers: AxeHelpers = {
getAxePath,
getBundledAxeEnvironment,
createAxeNotAvailableResponse,
},
debuggerManager: DebuggerManager = getDefaultDebuggerManager(),
): Promise<ToolResponse> {
const toolName = 'gesture';
const { simulatorId, preset, screenWidth, screenHeight, duration, delta, preDelay, postDelay } =
params;
const guard = await guardUiAutomationAgainstStoppedDebugger({
debugger: debuggerManager,
simulatorId,
toolName,
});
if (guard.blockedResponse) return guard.blockedResponse;
const commandArgs = ['gesture', preset];
if (screenWidth !== undefined) {
commandArgs.push('--screen-width', String(screenWidth));
}
if (screenHeight !== undefined) {
commandArgs.push('--screen-height', String(screenHeight));
}
if (duration !== undefined) {
commandArgs.push('--duration', String(duration));
}
if (delta !== undefined) {
commandArgs.push('--delta', String(delta));
}
if (preDelay !== undefined) {
commandArgs.push('--pre-delay', String(preDelay));
}
if (postDelay !== undefined) {
commandArgs.push('--post-delay', String(postDelay));
}
log('info', `${LOG_PREFIX}/${toolName}: Starting gesture '${preset}' on ${simulatorId}`);
try {
await executeAxeCommand(commandArgs, simulatorId, 'gesture', executor, axeHelpers);
log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`);
const message = `Gesture '${preset}' executed successfully.`;
if (guard.warningText) {
return createTextResponse(`${message}\n\n${guard.warningText}`);
}
return createTextResponse(message);
} catch (error) {
log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`);
if (error instanceof DependencyError) {
return axeHelpers.createAxeNotAvailableResponse();
} else if (error instanceof AxeError) {
return createErrorResponse(
`Failed to execute gesture '${preset}': ${error.message}`,
error.axeOutput,
);
} else if (error instanceof SystemError) {
return createErrorResponse(
`System error executing axe: ${error.message}`,
error.originalError?.stack,
);
}
return createErrorResponse(
`An unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
const publicSchemaObject = z.strictObject(gestureSchema.omit({ simulatorId: true } as const).shape);
export default {
name: 'gesture',
description:
'Perform gesture on iOS simulator using preset gestures: scroll-up, scroll-down, scroll-left, scroll-right, swipe-from-left-edge, swipe-from-right-edge, swipe-from-top-edge, swipe-from-bottom-edge',
schema: getSessionAwareToolSchemaShape({
sessionAware: publicSchemaObject,
legacy: gestureSchema,
}),
annotations: {
title: 'Gesture',
destructiveHint: true,
},
handler: createSessionAwareTool<GestureParams>({
internalSchema: gestureSchema as unknown as z.ZodType<GestureParams, unknown>,
logicFunction: (params: GestureParams, executor: CommandExecutor) =>
gestureLogic(params, executor, {
getAxePath,
getBundledAxeEnvironment,
createAxeNotAvailableResponse,
}),
getExecutor: getDefaultCommandExecutor,
requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
}),
};
// Helper function for executing axe commands (inlined from src/tools/axe/index.ts)
async function executeAxeCommand(
commandArgs: string[],
simulatorId: string,
commandName: string,
executor: CommandExecutor = getDefaultCommandExecutor(),
axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse },
): Promise<void> {
// Get the appropriate axe binary path
const axeBinary = axeHelpers.getAxePath();
if (!axeBinary) {
throw new DependencyError('AXe binary not found');
}
// Add --udid parameter to all commands
const fullArgs = [...commandArgs, '--udid', simulatorId];
// Construct the full command array with the axe binary as the first element
const fullCommand = [axeBinary, ...fullArgs];
try {
// Determine environment variables for bundled AXe
const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined;
const result = await executor(
fullCommand,
`${LOG_PREFIX}: ${commandName}`,
false,
axeEnv ? { env: axeEnv } : undefined,
);
if (!result.success) {
throw new AxeError(
`axe command '${commandName}' failed.`,
commandName,
result.error ?? result.output,
simulatorId,
);
}
// Check for stderr output in successful commands
if (result.error) {
log(
'warn',
`${LOG_PREFIX}: Command '${commandName}' produced stderr output but exited successfully. Output: ${result.error}`,
);
}
// Function now returns void - the calling code creates its own response
} catch (error) {
if (error instanceof Error) {
if (error instanceof AxeError) {
throw error;
}
// Otherwise wrap it in a SystemError
throw new SystemError(`Failed to execute axe command: ${error.message}`, error);
}
// For any other type of error
throw new SystemError(`Failed to execute axe command: ${String(error)}`);
}
}
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
# Changelog
## [Unreleased]
### Added
- Add Smithery support for packaging/distribution.
- Add DAP-based debugger backend and simulator debugging toolset (attach, breakpoints, stack, variables, LLDB command).
- Add session-status MCP resource with session identifiers.
- Add UI automation guard that blocks UI tools when the debugger is paused.
### Changed
- Migrate to Zod v4.
- Improve session default handling (reconcile mutual exclusivity and ignore explicit undefined clears).
### Fixed
- Update UI automation guard guidance to point at `debug_continue` when paused.
- Fix tool loading bugs in static tool registration.
## [1.16.0] - 2025-12-30
- Remove dynamic tool discovery (`discover_tools`) and `XCODEBUILDMCP_DYNAMIC_TOOLS`. Use `XCODEBUILDMCP_ENABLED_WORKFLOWS` to limit startup tool registration.
- Add MCP tool annotations to all tools.
## [1.14.0] - 2025-09-22
- Add video capture tool for simulators
## [1.13.1] - 2025-09-21
- Add simulator erase content and settings tool
## [1.12.3] - 2025-08-22
- Pass environment variables to test runs on device, simulator, and macOS via an optional testRunnerEnv input (auto-prefixed as TEST_RUNNER_).
## [1.12.2] - 2025-08-21
### Fixed
- **Clean tool**: Fixed issue where clean would fail for simulators
## [1.12.1] - 2025-08-18
### Improved
- **Sentry Logging**: No longer logs domain errors to Sentry, now only logs MCP server errors.
## [1.12.0] - 2025-08-17
### Added
- Unify project/workspace and sim id/name tools into a single tools reducing the number of tools from 81 to 59, this helps reduce the client agent's context window size by 27%!
- **Selective Workflow Loading**: New `XCODEBUILDMCP_ENABLED_WORKFLOWS` environment variable allows loading only specific workflow groups in static mode, reducing context window usage for clients that don't support MCP sampling (Thanks to @codeman9 for their first contribution!)
- Rename `diagnosics` tool and cli to `doctor`
- Add Sentry instrumentation to track MCP usage statistics (can be disabled by setting `XCODEBUILDMCP_SENTRY_DISABLED=true`)
- Add support for MCP setLevel handler to allow clients to control the log level of the MCP server
## [v1.11.2] - 2025-08-08
- Fixed "registerTools is not a function" errors during package upgrades
## [v1.11.1] - 2025-08-07
- Improved tool discovery to be more accurate and context-aware
## [v1.11.0] - 2025-08-07
- Major refactor/rewrite to improve code quality and maintainability in preparation for future development
- Added support for dynamic tools (VSCode only for now)
- Added support for MCP Resources (devices, simulators, environment info)
- Workaround for https://github.com/cameroncooke/XcodeBuildMCP/issues/66 and https://github.com/anthropics/claude-code/issues/1804 issues where Claude Code would only see the first text content from tool responses
## [v1.10.0] - 2025-06-10
### Added
- **App Lifecycle Management**: New tools for stopping running applications
- `stop_app_device`: Stop apps running on physical Apple devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro)
- `stop_app_sim`: Stop apps running on iOS/watchOS/tvOS/visionOS simulators
- `stop_mac_app`: Stop macOS applications by name or process ID
- **Enhanced Launch Tools**: Device launch tools now return process IDs for better app management
- **Bundled AXe Distribution**: AXe binary and frameworks now included in npm package for zero-setup UI automation
### Fixed
- **WiFi Device Detection**: Improved detection of Apple devices connected over WiFi networks
- **Device Connectivity**: Better handling of paired devices with different connection states
### Improved
- **Simplified Installation**: No separate AXe installation required - everything works out of the box
## [v1.9.0] - 2025-06-09
- Added support for hardware devices over USB and Wi-Fi
- New tools for Apple device deployment:
- `install_app_device`
- `launch_app_device`
- Updated all simulator and device tools to be platform-agnostic, supporting all Apple platforms (iOS, iPadOS, watchOS, tvOS, visionOS)
- Changed `get_ios_bundle_id` to `get_app_bundle_id` with support for all Apple platforms
## [v1.8.0] - 2025-06-07
- Added support for running tests on macOS, iOS simulators, and iOS devices
- New tools for testing:
- `test_macos_workspace`
- `test_macos_project`
- `test_ios_simulator_name_workspace`
- `test_ios_simulator_name_project`
- `test_ios_simulator_id_workspace`
- `test_ios_simulator_id_project`
- `test_ios_device_workspace`
- `test_ios_device_project`
## [v1.7.0] - 2025-06-04
- Added support for Swift Package Manager (SPM)
- New tools for Swift Package Manager:
- `swift_package_build`
- `swift_package_clean`
- `swift_package_test`
- `swift_package_run`
- `swift_package_list`
- `swift_package_stop`
## [v1.6.1] - 2025-06-03
- Improve UI tool hints
## [v1.6.0] - 2025-06-03
- Moved project templates to external GitHub repositories for independent versioning
- Added support for downloading templates from GitHub releases
- Added local template override support via environment variables
- Added `scaffold_ios_project` and `scaffold_macos_project` tools for creating new projects
- Centralized template version management in package.json for easier updates
## [v1.5.0] - 2025-06-01
- UI automation is no longer in beta!
- Added support for AXe UI automation
- Revised default installation instructions to prefer npx instead of mise
## [v1.4.0] - 2025-05-11
- Merge the incremental build beta branch into main
- Add preferXcodebuild argument to build tools with improved error handling allowing the agent to force the use of xcodebuild over xcodemake for complex projects. It also adds a hint when incremental builds fail due to non-compiler errors, enabling the agent to automatically switch to xcodebuild for a recovery build attempt, improving reliability.
## [v1.3.7] - 2025-05-08
- Fix Claude Code issue due to long tool names
## [v1.4.0-beta.3] - 2025-05-07
- Fixed issue where incremental builds would only work for "Debug" build configurations
-
## [v1.4.0-beta.2] - 2025-05-07
- Same as beta 1 but has the latest features from the main release channel
## [v1.4.0-beta.1] - 2025-05-05
- Added experimental support for incremental builds (requires opt-in)
## [v1.3.6] - 2025-05-07
- Added support for enabling/disabling tools via environment variables
## [v1.3.5] - 2025-05-05
- Fixed the text input UI automation tool
- Improve the UI automation tool hints to reduce agent tool call errors
- Improved the project discovery tool to reduce agent tool call errors
- Added instructions for installing idb client manually
## [v1.3.4] - 2025-05-04
- Improved Sentry integration
## [v1.3.3] - 2025-05-04
- Added Sentry opt-out functionality
## [v1.3.1] - 2025-05-03
- Added Sentry integration for error reporting
## [v1.3.0] - 2025-04-28
- Added support for interacting with the simulator (tap, swipe etc.)
- Added support for capturing simulator screenshots
Please note that the UI automation features are an early preview and currently in beta your mileage may vary.
## [v1.2.4] - 2025-04-24
- Improved xcodebuild reporting of warnings and errors in tool response
- Refactor build utils and remove redundant code
## [v1.2.3] - 2025-04-23
- Added support for skipping macro validation
## [v1.2.2] - 2025-04-23
- Improved log readability with version information for easier debugging
- Enhanced overall stability and performance
## [v1.2.1] - 2025-04-23
- General stability improvements and bug fixes
## [v1.2.0] - 2025-04-14
### Added
- New simulator log capture feature: Easily view and debug your app's logs while running in the simulator
- Automatic project discovery: XcodeBuildMCP now finds your Xcode projects and workspaces automatically
- Support for both Intel and Apple Silicon Macs in macOS builds
### Improved
- Cleaner, more readable build output with better error messages
- Faster build times and more reliable build process
- Enhanced documentation with clearer usage examples
## [v1.1.0] - 2025-04-05
### Added
- Real-time build progress reporting
- Separate tools for iOS and macOS builds
- Better workspace and project support
### Improved
- Simplified build commands with better parameter handling
- More reliable clean operations for both projects and workspaces
## [v1.0.2] - 2025-04-02
- Improved documentation with better examples and clearer instructions
- Easier version tracking for compatibility checks
## [v1.0.1] - 2025-04-02
- Initial release of XcodeBuildMCP
- Basic support for building iOS and macOS applications
```
--------------------------------------------------------------------------------
/src/mcp/tools/swift-package/swift_package_run.ts:
--------------------------------------------------------------------------------
```typescript
import * as z from 'zod';
import path from 'node:path';
import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.ts';
import { log } from '../../../utils/logging/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { ToolResponse, createTextContent } from '../../../types/common.ts';
import { addProcess } from './active-processes.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
// Define schema as ZodObject
const swiftPackageRunSchema = z.object({
packagePath: z.string().describe('Path to the Swift package root (Required)'),
executableName: z
.string()
.optional()
.describe('Name of executable to run (defaults to package name)'),
arguments: z.array(z.string()).optional().describe('Arguments to pass to the executable'),
configuration: z
.enum(['debug', 'release'])
.optional()
.describe("Build configuration: 'debug' (default) or 'release'"),
timeout: z.number().optional().describe('Timeout in seconds (default: 30, max: 300)'),
background: z
.boolean()
.optional()
.describe('Run in background and return immediately (default: false)'),
parseAsLibrary: z
.boolean()
.optional()
.describe('Add -parse-as-library flag for @main support (default: false)'),
});
// Use z.infer for type safety
type SwiftPackageRunParams = z.infer<typeof swiftPackageRunSchema>;
export async function swift_package_runLogic(
params: SwiftPackageRunParams,
executor: CommandExecutor,
): Promise<ToolResponse> {
const resolvedPath = path.resolve(params.packagePath);
const timeout = Math.min(params.timeout ?? 30, 300) * 1000; // Convert to ms, max 5 minutes
// Detect test environment to prevent real spawn calls during testing
const isTestEnvironment = process.env.VITEST === 'true' || process.env.NODE_ENV === 'test';
const swiftArgs = ['run', '--package-path', resolvedPath];
if (params.configuration && params.configuration.toLowerCase() === 'release') {
swiftArgs.push('-c', 'release');
} else if (params.configuration && params.configuration.toLowerCase() !== 'debug') {
return createTextResponse("Invalid configuration. Use 'debug' or 'release'.", true);
}
if (params.parseAsLibrary) {
swiftArgs.push('-Xswiftc', '-parse-as-library');
}
if (params.executableName) {
swiftArgs.push(params.executableName);
}
// Add double dash before executable arguments
if (params.arguments && params.arguments.length > 0) {
swiftArgs.push('--');
swiftArgs.push(...params.arguments);
}
log('info', `Running swift ${swiftArgs.join(' ')}`);
try {
if (params.background) {
// Background mode: Use CommandExecutor but don't wait for completion
if (isTestEnvironment) {
// In test environment, return mock response without real process
const mockPid = 12345;
return {
content: [
createTextContent(
`🚀 Started executable in background (PID: ${mockPid})\n` +
`💡 Process is running independently. Use swift_package_stop with PID ${mockPid} to terminate when needed.`,
),
],
};
} else {
// Production: use CommandExecutor to start the process
const command = ['swift', ...swiftArgs];
// Filter out undefined values from process.env
const cleanEnv = Object.fromEntries(
Object.entries(process.env).filter(([, value]) => value !== undefined),
) as Record<string, string>;
const result = await executor(
command,
'Swift Package Run (Background)',
true,
cleanEnv,
true,
);
// Store the process in active processes system if available
if (result.process?.pid) {
addProcess(result.process.pid, {
process: {
kill: (signal?: string) => {
// Adapt string signal to NodeJS.Signals
if (result.process) {
result.process.kill(signal as NodeJS.Signals);
}
},
on: (event: string, callback: () => void) => {
if (result.process) {
result.process.on(event, callback);
}
},
pid: result.process.pid,
},
startedAt: new Date(),
});
return {
content: [
createTextContent(
`🚀 Started executable in background (PID: ${result.process.pid})\n` +
`💡 Process is running independently. Use swift_package_stop with PID ${result.process.pid} to terminate when needed.`,
),
],
};
} else {
return {
content: [
createTextContent(
`🚀 Started executable in background\n` +
`💡 Process is running independently. PID not available for this execution.`,
),
],
};
}
}
} else {
// Foreground mode: use CommandExecutor but handle long-running processes
const command = ['swift', ...swiftArgs];
// Create a promise that will either complete with the command result or timeout
const commandPromise = executor(command, 'Swift Package Run', true, undefined);
const timeoutPromise = new Promise<{
success: boolean;
output: string;
error: string;
timedOut: boolean;
}>((resolve) => {
setTimeout(() => {
resolve({
success: false,
output: '',
error: `Process timed out after ${timeout / 1000} seconds`,
timedOut: true,
});
}, timeout);
});
// Race between command completion and timeout
const result = await Promise.race([commandPromise, timeoutPromise]);
if ('timedOut' in result && result.timedOut) {
// For timeout case, the process may still be running - provide timeout response
if (isTestEnvironment) {
// In test environment, return mock response
const mockPid = 12345;
return {
content: [
createTextContent(
`⏱️ Process timed out after ${timeout / 1000} seconds but may continue running.`,
),
createTextContent(`PID: ${mockPid} (mock)`),
createTextContent(
`💡 Process may still be running. Use swift_package_stop with PID ${mockPid} to terminate when needed.`,
),
createTextContent(result.output || '(no output so far)'),
],
};
} else {
// Production: timeout occurred, but we don't start a new process
return {
content: [
createTextContent(`⏱️ Process timed out after ${timeout / 1000} seconds.`),
createTextContent(
`💡 Process execution exceeded the timeout limit. Consider using background mode for long-running executables.`,
),
createTextContent(result.output || '(no output so far)'),
],
};
}
}
if (result.success) {
return {
content: [
createTextContent('✅ Swift executable completed successfully.'),
createTextContent('💡 Process finished cleanly. Check output for results.'),
createTextContent(result.output || '(no output)'),
],
};
} else {
const content = [
createTextContent('❌ Swift executable failed.'),
createTextContent(result.output || '(no output)'),
];
if (result.error) {
content.push(createTextContent(`Errors:\n${result.error}`));
}
return { content };
}
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log('error', `Swift run failed: ${message}`);
return createErrorResponse('Failed to execute swift run', message);
}
}
export default {
name: 'swift_package_run',
description: 'Runs an executable target from a Swift Package with swift run',
schema: swiftPackageRunSchema.shape, // MCP SDK compatibility
annotations: {
title: 'Swift Package Run',
destructiveHint: true,
},
handler: createTypedTool(
swiftPackageRunSchema,
swift_package_runLogic,
getDefaultCommandExecutor,
),
};
```
--------------------------------------------------------------------------------
/src/mcp/tools/swift-package/__tests__/swift_package_test.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Tests for swift_package_test plugin
* Following CLAUDE.md testing standards with literal validation
* Using dependency injection for deterministic testing
*/
import { describe, it, expect } from 'vitest';
import {
createMockExecutor,
createMockFileSystemExecutor,
createNoopExecutor,
createMockCommandResponse,
} from '../../../../test-utils/mock-executors.ts';
import swiftPackageTest, { swift_package_testLogic } from '../swift_package_test.ts';
import type { CommandExecutor } from '../../../../utils/execution/index.ts';
describe('swift_package_test plugin', () => {
describe('Export Field Validation (Literal)', () => {
it('should have correct name', () => {
expect(swiftPackageTest.name).toBe('swift_package_test');
});
it('should have correct description', () => {
expect(swiftPackageTest.description).toBe('Runs tests for a Swift Package with swift test');
});
it('should have handler function', () => {
expect(typeof swiftPackageTest.handler).toBe('function');
});
it('should validate schema correctly', () => {
// Test required fields
expect(swiftPackageTest.schema.packagePath.safeParse('/test/package').success).toBe(true);
expect(swiftPackageTest.schema.packagePath.safeParse('').success).toBe(true);
// Test optional fields
expect(swiftPackageTest.schema.testProduct.safeParse('MyTests').success).toBe(true);
expect(swiftPackageTest.schema.testProduct.safeParse(undefined).success).toBe(true);
expect(swiftPackageTest.schema.filter.safeParse('Test.*').success).toBe(true);
expect(swiftPackageTest.schema.filter.safeParse(undefined).success).toBe(true);
expect(swiftPackageTest.schema.configuration.safeParse('debug').success).toBe(true);
expect(swiftPackageTest.schema.configuration.safeParse('release').success).toBe(true);
expect(swiftPackageTest.schema.configuration.safeParse(undefined).success).toBe(true);
expect(swiftPackageTest.schema.parallel.safeParse(true).success).toBe(true);
expect(swiftPackageTest.schema.parallel.safeParse(undefined).success).toBe(true);
expect(swiftPackageTest.schema.showCodecov.safeParse(true).success).toBe(true);
expect(swiftPackageTest.schema.showCodecov.safeParse(undefined).success).toBe(true);
expect(swiftPackageTest.schema.parseAsLibrary.safeParse(true).success).toBe(true);
expect(swiftPackageTest.schema.parseAsLibrary.safeParse(undefined).success).toBe(true);
// Test invalid inputs
expect(swiftPackageTest.schema.packagePath.safeParse(null).success).toBe(false);
expect(swiftPackageTest.schema.configuration.safeParse('invalid').success).toBe(false);
expect(swiftPackageTest.schema.parallel.safeParse('yes').success).toBe(false);
expect(swiftPackageTest.schema.showCodecov.safeParse('yes').success).toBe(false);
expect(swiftPackageTest.schema.parseAsLibrary.safeParse('yes').success).toBe(false);
});
});
describe('Command Generation Testing', () => {
it('should build correct command for basic test', async () => {
const calls: Array<{
args: string[];
name?: string;
hideOutput?: boolean;
opts?: { env?: Record<string, string>; cwd?: string };
}> = [];
const mockExecutor: CommandExecutor = async (args, name, hideOutput, opts) => {
calls.push({ args, name, hideOutput, opts });
return createMockCommandResponse({
success: true,
output: 'Test Passed',
error: undefined,
});
};
await swift_package_testLogic(
{
packagePath: '/test/package',
},
mockExecutor,
);
expect(calls).toHaveLength(1);
expect(calls[0]).toEqual({
args: ['swift', 'test', '--package-path', '/test/package'],
name: 'Swift Package Test',
hideOutput: true,
opts: undefined,
});
});
it('should build correct command with all parameters', async () => {
const calls: Array<{
args: string[];
name?: string;
hideOutput?: boolean;
opts?: { env?: Record<string, string>; cwd?: string };
}> = [];
const mockExecutor: CommandExecutor = async (args, name, hideOutput, opts) => {
calls.push({ args, name, hideOutput, opts });
return createMockCommandResponse({
success: true,
output: 'Tests completed',
error: undefined,
});
};
await swift_package_testLogic(
{
packagePath: '/test/package',
testProduct: 'MyTests',
filter: 'Test.*',
configuration: 'release',
parallel: false,
showCodecov: true,
parseAsLibrary: true,
},
mockExecutor,
);
expect(calls).toHaveLength(1);
expect(calls[0]).toEqual({
args: [
'swift',
'test',
'--package-path',
'/test/package',
'-c',
'release',
'--test-product',
'MyTests',
'--filter',
'Test.*',
'--no-parallel',
'--show-code-coverage',
'-Xswiftc',
'-parse-as-library',
],
name: 'Swift Package Test',
hideOutput: true,
opts: undefined,
});
});
});
describe('Response Logic Testing', () => {
it('should handle empty packagePath parameter', async () => {
// When packagePath is empty, the function should still process it
// but the command execution may fail, which is handled by the executor
const mockExecutor = createMockExecutor({
success: true,
output: 'Tests completed with empty path',
});
const result = await swift_package_testLogic({ packagePath: '' }, mockExecutor);
expect(result.isError).toBe(false);
expect(result.content[0].text).toBe('✅ Swift package tests completed.');
});
it('should return successful test response', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: 'All tests passed.',
});
const result = await swift_package_testLogic(
{
packagePath: '/test/package',
},
mockExecutor,
);
expect(result).toEqual({
content: [
{ type: 'text', text: '✅ Swift package tests completed.' },
{
type: 'text',
text: '💡 Next: Execute your app with swift_package_run if tests passed',
},
{ type: 'text', text: 'All tests passed.' },
],
isError: false,
});
});
it('should return error response for test failure', async () => {
const mockExecutor = createMockExecutor({
success: false,
error: '2 tests failed',
});
const result = await swift_package_testLogic(
{
packagePath: '/test/package',
},
mockExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Error: Swift package tests failed\nDetails: 2 tests failed',
},
],
isError: true,
});
});
it('should handle spawn error', async () => {
const mockExecutor = async () => {
throw new Error('spawn ENOENT');
};
const result = await swift_package_testLogic(
{
packagePath: '/test/package',
},
mockExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Error: Failed to execute swift test\nDetails: spawn ENOENT',
},
],
isError: true,
});
});
it('should handle successful test with parameters', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: 'Tests completed.',
});
const result = await swift_package_testLogic(
{
packagePath: '/test/package',
testProduct: 'MyTests',
filter: 'Test.*',
configuration: 'release',
parallel: false,
showCodecov: true,
parseAsLibrary: true,
},
mockExecutor,
);
expect(result).toEqual({
content: [
{ type: 'text', text: '✅ Swift package tests completed.' },
{
type: 'text',
text: '💡 Next: Execute your app with swift_package_run if tests passed',
},
{ type: 'text', text: 'Tests completed.' },
],
isError: false,
});
});
});
});
```
--------------------------------------------------------------------------------
/build-plugins/plugin-discovery.js:
--------------------------------------------------------------------------------
```javascript
import { readdirSync, readFileSync, existsSync } from 'fs';
import { join } from 'path';
import path from 'path';
export function createPluginDiscoveryPlugin() {
return {
name: 'plugin-discovery',
setup(build) {
// Generate the workflow loaders file before build starts
build.onStart(async () => {
try {
await generateWorkflowLoaders();
await generateResourceLoaders();
} catch (error) {
console.error('Failed to generate loaders:', error);
throw error;
}
});
},
};
}
async function generateWorkflowLoaders() {
const pluginsDir = path.resolve(process.cwd(), 'src/mcp/tools');
if (!existsSync(pluginsDir)) {
throw new Error(`Plugins directory not found: ${pluginsDir}`);
}
// Scan for workflow directories
const workflowDirs = readdirSync(pluginsDir, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
const workflowLoaders = {};
const workflowMetadata = {};
for (const dirName of workflowDirs) {
const dirPath = join(pluginsDir, dirName);
const indexPath = join(dirPath, 'index.ts');
// Check if workflow has index.ts file
if (!existsSync(indexPath)) {
console.warn(`Skipping ${dirName}: no index.ts file found`);
continue;
}
// Try to extract workflow metadata from index.ts
try {
const indexContent = readFileSync(indexPath, 'utf8');
const metadata = extractWorkflowMetadata(indexContent);
if (metadata) {
// Find all tool files in this workflow directory
const toolFiles = readdirSync(dirPath, { withFileTypes: true })
.filter((dirent) => dirent.isFile())
.map((dirent) => dirent.name)
.filter(
(name) =>
(name.endsWith('.ts') || name.endsWith('.js')) &&
name !== 'index.ts' &&
name !== 'index.js' &&
!name.endsWith('.test.ts') &&
!name.endsWith('.test.js') &&
name !== 'active-processes.ts', // Special exclusion for swift-package
);
// Generate dynamic loader function that loads workflow and all its tools
workflowLoaders[dirName] = generateWorkflowLoader(dirName, toolFiles);
workflowMetadata[dirName] = metadata;
console.log(
`✅ Discovered workflow: ${dirName} - ${metadata.name} (${toolFiles.length} tools)`,
);
} else {
console.warn(`⚠️ Skipping ${dirName}: invalid workflow metadata`);
}
} catch (error) {
console.warn(`⚠️ Error processing ${dirName}:`, error);
}
}
// Generate the content for generated-plugins.ts
const generatedContent = await generatePluginsFileContent(workflowLoaders, workflowMetadata);
// Write to the generated file
const outputPath = path.resolve(process.cwd(), 'src/core/generated-plugins.ts');
const fs = await import('fs');
await fs.promises.writeFile(outputPath, generatedContent, 'utf8');
console.log(`🔧 Generated workflow loaders for ${Object.keys(workflowLoaders).length} workflows`);
}
function generateWorkflowLoader(workflowName, toolFiles) {
const toolImports = toolFiles
.map((file, index) => {
const toolName = file.replace(/\.(ts|js)$/, '');
return `const tool_${index} = await import('../mcp/tools/${workflowName}/${toolName}.ts').then(m => m.default)`;
})
.join(';\n ');
const toolExports = toolFiles
.map((file, index) => {
const toolName = file.replace(/\.(ts|js)$/, '');
return `'${toolName}': tool_${index}`;
})
.join(',\n ');
return `async () => {
const { workflow } = await import('../mcp/tools/${workflowName}/index.ts');
${toolImports ? toolImports + ';\n ' : ''}
return {
workflow,
${toolExports ? toolExports : ''}
};
}`;
}
function extractWorkflowMetadata(content) {
try {
// Simple regex to extract workflow export object
const workflowMatch = content.match(/export\s+const\s+workflow\s*=\s*({[\s\S]*?});/);
if (!workflowMatch) {
return null;
}
const workflowObj = workflowMatch[1];
// Extract name
const nameMatch = workflowObj.match(/name\s*:\s*['"`]([^'"`]+)['"`]/);
if (!nameMatch) return null;
// Extract description
const descMatch = workflowObj.match(/description\s*:\s*['"`]([\s\S]*?)['"`]/);
if (!descMatch) return null;
const result = {
name: nameMatch[1],
description: descMatch[1],
};
return result;
} catch (error) {
console.warn('Failed to extract workflow metadata:', error);
return null;
}
}
async function generatePluginsFileContent(workflowLoaders, workflowMetadata) {
const loaderEntries = Object.entries(workflowLoaders)
.map(([key, loader]) => {
// Indent the loader function properly
const indentedLoader = loader
.split('\n')
.map((line, index) => (index === 0 ? ` '${key}': ${line}` : ` ${line}`))
.join('\n');
return indentedLoader;
})
.join(',\n');
const metadataEntries = Object.entries(workflowMetadata)
.map(([key, metadata]) => {
const metadataJson = JSON.stringify(metadata, null, 4)
.split('\n')
.map((line) => ` ${line}`)
.join('\n');
return ` '${key}': ${metadataJson.trim()}`;
})
.join(',\n');
const content = `// AUTO-GENERATED - DO NOT EDIT
// This file is generated by the plugin discovery esbuild plugin
// Generated based on filesystem scan
export const WORKFLOW_LOADERS = {
${loaderEntries}
};
export type WorkflowName = keyof typeof WORKFLOW_LOADERS;
// Optional: Export workflow metadata for quick access
export const WORKFLOW_METADATA = {
${metadataEntries}
};
`;
return formatGenerated(content);
}
async function generateResourceLoaders() {
const resourcesDir = path.resolve(process.cwd(), 'src/mcp/resources');
if (!existsSync(resourcesDir)) {
console.log('Resources directory not found, skipping resource generation');
return;
}
// Scan for resource files
const resourceFiles = readdirSync(resourcesDir, { withFileTypes: true })
.filter((dirent) => dirent.isFile())
.map((dirent) => dirent.name)
.filter(
(name) =>
(name.endsWith('.ts') || name.endsWith('.js')) &&
!name.endsWith('.test.ts') &&
!name.endsWith('.test.js') &&
!name.startsWith('__'), // Exclude test directories
);
const resourceLoaders = {};
for (const fileName of resourceFiles) {
const resourceName = fileName.replace(/\.(ts|js)$/, '');
// Generate dynamic loader for this resource
resourceLoaders[resourceName] = `async () => {
const module = await import('../mcp/resources/${resourceName}.ts');
return module.default;
}`;
console.log(`✅ Discovered resource: ${resourceName}`);
}
// Generate the content for generated-resources.ts
const generatedContent = await generateResourcesFileContent(resourceLoaders);
// Write to the generated file
const outputPath = path.resolve(process.cwd(), 'src/core/generated-resources.ts');
const fs = await import('fs');
await fs.promises.writeFile(outputPath, generatedContent, 'utf8');
console.log(`🔧 Generated resource loaders for ${Object.keys(resourceLoaders).length} resources`);
}
async function generateResourcesFileContent(resourceLoaders) {
const loaderEntries = Object.entries(resourceLoaders)
.map(([key, loader]) => ` '${key}': ${loader}`)
.join(',\n');
const content = `// AUTO-GENERATED - DO NOT EDIT
// This file is generated by the plugin discovery esbuild plugin
export const RESOURCE_LOADERS = {
${loaderEntries}
};
export type ResourceName = keyof typeof RESOURCE_LOADERS;
`;
return formatGenerated(content);
}
async function formatGenerated(content) {
try {
const { resolve } = await import('node:path');
const { pathToFileURL } = await import('node:url');
const prettier = await import('prettier');
let config = (await prettier.resolveConfig(process.cwd())) ?? null;
if (!config) {
try {
const configUrl = pathToFileURL(resolve(process.cwd(), '.prettierrc.js')).href;
const configModule = await import(configUrl);
config = configModule.default ?? configModule;
} catch {
config = null;
}
}
const options = {
semi: true,
trailingComma: 'all',
singleQuote: true,
printWidth: 100,
tabWidth: 2,
endOfLine: 'auto',
...config,
parser: 'typescript',
};
return prettier.format(content, options);
} catch {
return content;
}
}
```
--------------------------------------------------------------------------------
/src/mcp/tools/device/__tests__/install_app_device.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Tests for install_app_device plugin (device-shared)
* Following CLAUDE.md testing standards with literal validation
* Using dependency injection for deterministic testing
*/
import { describe, it, expect, beforeEach } from 'vitest';
import * as z from 'zod';
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
import installAppDevice, { install_app_deviceLogic } from '../install_app_device.ts';
import { sessionStore } from '../../../../utils/session-store.ts';
describe('install_app_device plugin', () => {
beforeEach(() => {
sessionStore.clear();
});
describe('Handler Requirements', () => {
it('should require deviceId when session defaults are missing', async () => {
const result = await installAppDevice.handler({
appPath: '/path/to/test.app',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('deviceId is required');
});
});
describe('Export Field Validation (Literal)', () => {
it('should have correct name', () => {
expect(installAppDevice.name).toBe('install_app_device');
});
it('should have correct description', () => {
expect(installAppDevice.description).toBe('Installs an app on a connected device.');
});
it('should have handler function', () => {
expect(typeof installAppDevice.handler).toBe('function');
});
it('should require appPath in public schema', () => {
const schema = z.strictObject(installAppDevice.schema);
expect(schema.safeParse({ appPath: '/path/to/test.app' }).success).toBe(true);
expect(schema.safeParse({}).success).toBe(false);
expect(schema.safeParse({ deviceId: 'test-device-123' }).success).toBe(false);
expect(Object.keys(installAppDevice.schema)).toEqual(['appPath']);
});
});
describe('Command Generation', () => {
it('should generate correct devicectl command with basic parameters', async () => {
let capturedCommand: string[] = [];
let capturedDescription: string = '';
let capturedUseShell: boolean = false;
let capturedEnv: Record<string, string> | undefined = undefined;
const mockExecutor = createMockExecutor({
success: true,
output: 'App installation successful',
process: { pid: 12345 },
});
const trackingExecutor = async (
command: string[],
description?: string,
useShell?: boolean,
opts?: { env?: Record<string, string> },
_detached?: boolean,
) => {
capturedCommand = command;
capturedDescription = description ?? '';
capturedUseShell = !!useShell;
capturedEnv = opts?.env;
return mockExecutor(command, description, useShell, opts, _detached);
};
await install_app_deviceLogic(
{
deviceId: 'test-device-123',
appPath: '/path/to/test.app',
},
trackingExecutor,
);
expect(capturedCommand).toEqual([
'xcrun',
'devicectl',
'device',
'install',
'app',
'--device',
'test-device-123',
'/path/to/test.app',
]);
expect(capturedDescription).toBe('Install app on device');
expect(capturedUseShell).toBe(true);
expect(capturedEnv).toBe(undefined);
});
it('should generate correct command with different device ID', async () => {
let capturedCommand: string[] = [];
const mockExecutor = createMockExecutor({
success: true,
output: 'App installation successful',
process: { pid: 12345 },
});
const trackingExecutor = async (command: string[]) => {
capturedCommand = command;
return mockExecutor(command);
};
await install_app_deviceLogic(
{
deviceId: 'different-device-uuid',
appPath: '/apps/MyApp.app',
},
trackingExecutor,
);
expect(capturedCommand).toEqual([
'xcrun',
'devicectl',
'device',
'install',
'app',
'--device',
'different-device-uuid',
'/apps/MyApp.app',
]);
});
it('should generate correct command with paths containing spaces', async () => {
let capturedCommand: string[] = [];
const mockExecutor = createMockExecutor({
success: true,
output: 'App installation successful',
process: { pid: 12345 },
});
const trackingExecutor = async (command: string[]) => {
capturedCommand = command;
return mockExecutor(command);
};
await install_app_deviceLogic(
{
deviceId: 'test-device-123',
appPath: '/path/to/My App.app',
},
trackingExecutor,
);
expect(capturedCommand).toEqual([
'xcrun',
'devicectl',
'device',
'install',
'app',
'--device',
'test-device-123',
'/path/to/My App.app',
]);
});
});
describe('Success Path Tests', () => {
it('should return successful installation response', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: 'App installation successful',
});
const result = await install_app_deviceLogic(
{
deviceId: 'test-device-123',
appPath: '/path/to/test.app',
},
mockExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: '✅ App installed successfully on device test-device-123\n\nApp installation successful',
},
],
});
});
it('should return successful installation with detailed output', async () => {
const mockExecutor = createMockExecutor({
success: true,
output:
'Installing app...\nApp bundle: /path/to/test.app\nInstallation completed successfully',
});
const result = await install_app_deviceLogic(
{
deviceId: 'device-456',
appPath: '/apps/TestApp.app',
},
mockExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: '✅ App installed successfully on device device-456\n\nInstalling app...\nApp bundle: /path/to/test.app\nInstallation completed successfully',
},
],
});
});
it('should return successful installation with empty output', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: '',
});
const result = await install_app_deviceLogic(
{
deviceId: 'empty-output-device',
appPath: '/path/to/app.app',
},
mockExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: '✅ App installed successfully on device empty-output-device\n\n',
},
],
});
});
});
describe('Error Handling', () => {
it('should return installation failure response', async () => {
const mockExecutor = createMockExecutor({
success: false,
error: 'Installation failed: App not found',
});
const result = await install_app_deviceLogic(
{
deviceId: 'test-device-123',
appPath: '/path/to/nonexistent.app',
},
mockExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Failed to install app: Installation failed: App not found',
},
],
isError: true,
});
});
it('should return exception handling response', async () => {
const mockExecutor = createMockExecutor(new Error('Network error'));
const result = await install_app_deviceLogic(
{
deviceId: 'test-device-123',
appPath: '/path/to/test.app',
},
mockExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Failed to install app on device: Network error',
},
],
isError: true,
});
});
it('should return string error handling response', async () => {
const mockExecutor = createMockExecutor('String error');
const result = await install_app_deviceLogic(
{
deviceId: 'test-device-123',
appPath: '/path/to/test.app',
},
mockExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Failed to install app on device: String error',
},
],
isError: true,
});
});
});
});
```
--------------------------------------------------------------------------------
/src/mcp/tools/doctor/lib/doctor.deps.ts:
--------------------------------------------------------------------------------
```typescript
import * as os from 'os';
import type { CommandExecutor } from '../../../../utils/execution/index.ts';
import { loadWorkflowGroups } from '../../../../utils/plugin-registry/index.ts';
import { getRuntimeRegistration } from '../../../../utils/runtime-registry.ts';
import {
collectToolNames,
resolveSelectedWorkflows,
} from '../../../../utils/workflow-selection.ts';
import { areAxeToolsAvailable, resolveAxeBinary } from '../../../../utils/axe/index.ts';
import {
isXcodemakeEnabled,
isXcodemakeAvailable,
doesMakefileExist,
} from '../../../../utils/xcodemake/index.ts';
export interface BinaryChecker {
checkBinaryAvailability(binary: string): Promise<{ available: boolean; version?: string }>;
}
export interface XcodeInfoProvider {
getXcodeInfo(): Promise<
| { version: string; path: string; selectedXcode: string; xcrunVersion: string }
| { error: string }
>;
}
export interface EnvironmentInfoProvider {
getEnvironmentVariables(): Record<string, string | undefined>;
getSystemInfo(): {
platform: string;
release: string;
arch: string;
cpus: string;
memory: string;
hostname: string;
username: string;
homedir: string;
tmpdir: string;
};
getNodeInfo(): {
version: string;
execPath: string;
pid: string;
ppid: string;
platform: string;
arch: string;
cwd: string;
argv: string;
};
}
export interface PluginInfoProvider {
getPluginSystemInfo(): Promise<
| {
totalPlugins: number;
pluginDirectories: number;
pluginsByDirectory: Record<string, string[]>;
systemMode: string;
}
| { error: string; systemMode: string }
>;
}
export interface RuntimeInfoProvider {
getRuntimeToolInfo(): Promise<
| {
mode: 'runtime';
enabledWorkflows: string[];
enabledTools: string[];
totalRegistered: number;
}
| {
mode: 'static';
enabledWorkflows: string[];
enabledTools: string[];
totalRegistered: number;
note: string;
}
>;
}
export interface FeatureDetector {
areAxeToolsAvailable(): boolean;
isXcodemakeEnabled(): boolean;
isXcodemakeAvailable(): Promise<boolean>;
doesMakefileExist(path: string): boolean;
}
export interface DoctorDependencies {
commandExecutor: CommandExecutor;
binaryChecker: BinaryChecker;
xcode: XcodeInfoProvider;
env: EnvironmentInfoProvider;
plugins: PluginInfoProvider;
runtime: RuntimeInfoProvider;
features: FeatureDetector;
}
export function createDoctorDependencies(executor: CommandExecutor): DoctorDependencies {
const commandExecutor = executor;
const binaryChecker: BinaryChecker = {
async checkBinaryAvailability(binary: string) {
if (binary === 'axe') {
const axeBinary = resolveAxeBinary();
if (!axeBinary) {
return { available: false };
}
let version: string | undefined;
try {
const res = await executor([axeBinary.path, '--version'], 'Get AXe Version');
if (res.success && res.output) {
version = res.output.trim();
}
} catch {
// ignore
}
return {
available: true,
version: version ?? 'Available (version info not available)',
};
}
try {
const which = await executor(['which', binary], 'Check Binary Availability');
if (!which.success) {
return { available: false };
}
} catch {
return { available: false };
}
let version: string | undefined;
const versionCommands: Record<string, string> = {
mise: 'mise --version',
};
if (binary in versionCommands) {
try {
const res = await executor(versionCommands[binary]!.split(' '), 'Get Binary Version');
if (res.success && res.output) {
version = res.output.trim();
}
} catch {
// ignore
}
}
return { available: true, version: version ?? 'Available (version info not available)' };
},
};
const xcode: XcodeInfoProvider = {
async getXcodeInfo() {
try {
const xcodebuild = await executor(['xcodebuild', '-version'], 'Get Xcode Version');
if (!xcodebuild.success) throw new Error('xcodebuild command failed');
const version = xcodebuild.output.trim().split('\n').slice(0, 2).join(' - ');
const pathRes = await executor(['xcode-select', '-p'], 'Get Xcode Path');
if (!pathRes.success) throw new Error('xcode-select command failed');
const path = pathRes.output.trim();
const selected = await executor(['xcrun', '--find', 'xcodebuild'], 'Find Xcodebuild');
if (!selected.success) throw new Error('xcrun --find command failed');
const selectedXcode = selected.output.trim();
const xcrun = await executor(['xcrun', '--version'], 'Get Xcrun Version');
if (!xcrun.success) throw new Error('xcrun --version command failed');
const xcrunVersion = xcrun.output.trim();
return { version, path, selectedXcode, xcrunVersion };
} catch (error) {
return { error: error instanceof Error ? error.message : String(error) };
}
},
};
const env: EnvironmentInfoProvider = {
getEnvironmentVariables() {
const relevantVars = [
'INCREMENTAL_BUILDS_ENABLED',
'PATH',
'DEVELOPER_DIR',
'HOME',
'USER',
'TMPDIR',
'NODE_ENV',
'SENTRY_DISABLED',
];
const envVars: Record<string, string | undefined> = {};
for (const varName of relevantVars) {
envVars[varName] = process.env[varName];
}
Object.keys(process.env).forEach((key) => {
if (key.startsWith('XCODEBUILDMCP_')) {
envVars[key] = process.env[key];
}
});
return envVars;
},
getSystemInfo() {
return {
platform: os.platform(),
release: os.release(),
arch: os.arch(),
cpus: `${os.cpus().length} x ${os.cpus()[0]?.model ?? 'Unknown'}`,
memory: `${Math.round(os.totalmem() / (1024 * 1024 * 1024))} GB`,
hostname: os.hostname(),
username: os.userInfo().username,
homedir: os.homedir(),
tmpdir: os.tmpdir(),
};
},
getNodeInfo() {
return {
version: process.version,
execPath: process.execPath,
pid: process.pid.toString(),
ppid: process.ppid.toString(),
platform: process.platform,
arch: process.arch,
cwd: process.cwd(),
argv: process.argv.join(' '),
};
},
};
const plugins: PluginInfoProvider = {
async getPluginSystemInfo() {
try {
const workflows = await loadWorkflowGroups();
const pluginsByDirectory: Record<string, string[]> = {};
let totalPlugins = 0;
for (const [dirName, wf] of workflows.entries()) {
const toolNames = wf.tools.map((t) => t.name).filter(Boolean) as string[];
totalPlugins += toolNames.length;
pluginsByDirectory[dirName] = toolNames;
}
return {
totalPlugins,
pluginDirectories: workflows.size,
pluginsByDirectory,
systemMode: 'plugin-based',
};
} catch (error) {
return {
error: `Failed to load plugins: ${error instanceof Error ? error.message : 'Unknown error'}`,
systemMode: 'error',
};
}
},
};
const runtime: RuntimeInfoProvider = {
async getRuntimeToolInfo() {
const runtimeInfo = getRuntimeRegistration();
if (runtimeInfo) {
return runtimeInfo;
}
const workflows = await loadWorkflowGroups();
const enabledWorkflowEnv = process.env.XCODEBUILDMCP_ENABLED_WORKFLOWS ?? '';
const workflowNames = enabledWorkflowEnv
.split(',')
.map((workflow) => workflow.trim())
.filter(Boolean);
const selection = resolveSelectedWorkflows(workflows, workflowNames);
const enabledWorkflows = selection.selectedWorkflows.map(
(workflow) => workflow.directoryName,
);
const enabledTools = collectToolNames(selection.selectedWorkflows);
return {
mode: 'static',
enabledWorkflows,
enabledTools,
totalRegistered: enabledTools.length,
note: 'Runtime registry unavailable; showing expected tools from selection rules.',
};
},
};
const features: FeatureDetector = {
areAxeToolsAvailable,
isXcodemakeEnabled,
isXcodemakeAvailable,
doesMakefileExist,
};
return { commandExecutor, binaryChecker, xcode, env, plugins, runtime, features };
}
export type { CommandExecutor };
export default {} as const;
```
--------------------------------------------------------------------------------
/build-plugins/plugin-discovery.ts:
--------------------------------------------------------------------------------
```typescript
import { Plugin } from 'esbuild';
import { readdirSync, readFileSync, existsSync } from 'fs';
import { join } from 'path';
import path from 'path';
export interface WorkflowMetadata {
name: string;
description: string;
}
export function createPluginDiscoveryPlugin(): Plugin {
return {
name: 'plugin-discovery',
setup(build) {
// Generate the workflow loaders file before build starts
build.onStart(async () => {
try {
await generateWorkflowLoaders();
await generateResourceLoaders();
} catch (error) {
console.error('Failed to generate loaders:', error);
throw error;
}
});
},
};
}
export async function generateWorkflowLoaders(): Promise<void> {
const pluginsDir = path.resolve(process.cwd(), 'src/mcp/tools');
if (!existsSync(pluginsDir)) {
throw new Error(`Plugins directory not found: ${pluginsDir}`);
}
// Scan for workflow directories
const workflowDirs = readdirSync(pluginsDir, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
const workflowLoaders: Record<string, string> = {};
const workflowMetadata: Record<string, WorkflowMetadata> = {};
for (const dirName of workflowDirs) {
const dirPath = join(pluginsDir, dirName);
const indexPath = join(dirPath, 'index.ts');
// Check if workflow has index.ts file
if (!existsSync(indexPath)) {
console.warn(`Skipping ${dirName}: no index.ts file found`);
continue;
}
// Try to extract workflow metadata from index.ts
try {
const indexContent = readFileSync(indexPath, 'utf8');
const metadata = extractWorkflowMetadata(indexContent);
if (metadata) {
// Find all tool files in this workflow directory
const toolFiles = readdirSync(dirPath, { withFileTypes: true })
.filter((dirent) => dirent.isFile())
.map((dirent) => dirent.name)
.filter(
(name) =>
(name.endsWith('.ts') || name.endsWith('.js')) &&
name !== 'index.ts' &&
name !== 'index.js' &&
!name.endsWith('.test.ts') &&
!name.endsWith('.test.js') &&
name !== 'active-processes.ts',
);
workflowLoaders[dirName] = generateWorkflowLoader(dirName, toolFiles);
workflowMetadata[dirName] = metadata;
console.log(
`✅ Discovered workflow: ${dirName} - ${metadata.name} (${toolFiles.length} tools)`,
);
} else {
console.warn(`⚠️ Skipping ${dirName}: invalid workflow metadata`);
}
} catch (error) {
console.warn(`⚠️ Error processing ${dirName}:`, error);
}
}
// Generate the content for generated-plugins.ts
const generatedContent = await generatePluginsFileContent(workflowLoaders, workflowMetadata);
// Write to the generated file
const outputPath = path.resolve(process.cwd(), 'src/core/generated-plugins.ts');
const fs = await import('fs');
await fs.promises.writeFile(outputPath, generatedContent, 'utf8');
console.log(`🔧 Generated workflow loaders for ${Object.keys(workflowLoaders).length} workflows`);
}
function generateWorkflowLoader(workflowName: string, toolFiles: string[]): string {
const toolImports = toolFiles
.map((file, index) => {
const toolName = file.replace(/\.(ts|js)$/, '');
return `const tool_${index} = await import('../mcp/tools/${workflowName}/${toolName}.ts').then(m => m.default)`;
})
.join(';\n ');
const toolExports = toolFiles
.map((file, index) => {
const toolName = file.replace(/\.(ts|js)$/, '');
return `'${toolName}': tool_${index}`;
})
.join(',\n ');
return `async () => {
const { workflow } = await import('../mcp/tools/${workflowName}/index.ts');
${toolImports ? toolImports + ';\n ' : ''}
return {
workflow,
${toolExports ? toolExports : ''}
};
}`;
}
function extractWorkflowMetadata(content: string): WorkflowMetadata | null {
try {
// Simple regex to extract workflow export object
const workflowMatch = content.match(/export\s+const\s+workflow\s*=\s*({[\s\S]*?});/);
if (!workflowMatch) {
return null;
}
const workflowObj = workflowMatch[1];
// Extract name
const nameMatch = workflowObj.match(/name\s*:\s*['"`]([^'"`]+)['"`]/);
if (!nameMatch) return null;
// Extract description
const descMatch = workflowObj.match(/description\s*:\s*['"`]([\s\S]*?)['"`]/);
if (!descMatch) return null;
return {
name: nameMatch[1],
description: descMatch[1],
};
} catch (error) {
console.warn('Failed to extract workflow metadata:', error);
return null;
}
}
async function generatePluginsFileContent(
workflowLoaders: Record<string, string>,
workflowMetadata: Record<string, WorkflowMetadata>,
): Promise<string> {
const loaderEntries = Object.entries(workflowLoaders)
.map(([key, loader]) => {
const indentedLoader = loader
.split('\n')
.map((line, index) => (index === 0 ? ` '${key}': ${line}` : ` ${line}`))
.join('\n');
return indentedLoader;
})
.join(',\n');
const metadataEntries = Object.entries(workflowMetadata)
.map(([key, metadata]) => {
const metadataJson = JSON.stringify(metadata, null, 4)
.split('\n')
.map((line) => ` ${line}`)
.join('\n');
return ` '${key}': ${metadataJson.trim()}`;
})
.join(',\n');
const content = `// AUTO-GENERATED - DO NOT EDIT
// This file is generated by the plugin discovery esbuild plugin
// Generated based on filesystem scan
export const WORKFLOW_LOADERS = {
${loaderEntries}
};
export type WorkflowName = keyof typeof WORKFLOW_LOADERS;
// Optional: Export workflow metadata for quick access
export const WORKFLOW_METADATA = {
${metadataEntries}
};
`;
return formatGenerated(content);
}
export async function generateResourceLoaders(): Promise<void> {
const resourcesDir = path.resolve(process.cwd(), 'src/mcp/resources');
if (!existsSync(resourcesDir)) {
console.log('Resources directory not found, skipping resource generation');
return;
}
const resourceFiles = readdirSync(resourcesDir, { withFileTypes: true })
.filter((dirent) => dirent.isFile())
.map((dirent) => dirent.name)
.filter(
(name) =>
(name.endsWith('.ts') || name.endsWith('.js')) &&
!name.endsWith('.test.ts') &&
!name.endsWith('.test.js') &&
!name.startsWith('__'),
);
const resourceLoaders: Record<string, string> = {};
for (const fileName of resourceFiles) {
const resourceName = fileName.replace(/\.(ts|js)$/, '');
resourceLoaders[resourceName] = `async () => {
const module = await import('../mcp/resources/${resourceName}.ts');
return module.default;
}`;
console.log(`✅ Discovered resource: ${resourceName}`);
}
const generatedContent = await generateResourcesFileContent(resourceLoaders);
const outputPath = path.resolve(process.cwd(), 'src/core/generated-resources.ts');
const fs = await import('fs');
await fs.promises.writeFile(outputPath, generatedContent, 'utf8');
console.log(`🔧 Generated resource loaders for ${Object.keys(resourceLoaders).length} resources`);
}
async function generateResourcesFileContent(
resourceLoaders: Record<string, string>,
): Promise<string> {
const loaderEntries = Object.entries(resourceLoaders)
.map(([key, loader]) => ` '${key}': ${loader}`)
.join(',\n');
const content = `// AUTO-GENERATED - DO NOT EDIT
// This file is generated by the plugin discovery esbuild plugin
export const RESOURCE_LOADERS = {
${loaderEntries}
};
export type ResourceName = keyof typeof RESOURCE_LOADERS;
`;
return formatGenerated(content);
}
async function formatGenerated(content: string): Promise<string> {
try {
const { resolve } = await import('node:path');
const { pathToFileURL } = await import('node:url');
const prettier = await import('prettier');
let config = (await prettier.resolveConfig(process.cwd())) ?? null;
if (!config) {
try {
const configUrl = pathToFileURL(resolve(process.cwd(), '.prettierrc.js')).href;
const configModule = await import(configUrl);
config = (configModule as { default?: unknown }).default ?? configModule;
} catch {
config = null;
}
}
const options = {
semi: true,
trailingComma: 'all' as const,
singleQuote: true,
printWidth: 100,
tabWidth: 2,
endOfLine: 'auto' as const,
...(config as Record<string, unknown> | null),
parser: 'typescript',
};
return prettier.format(content, options);
} catch {
return content;
}
}
```
--------------------------------------------------------------------------------
/src/mcp/tools/swift-package/__tests__/swift_package_build.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Tests for swift_package_build plugin
* Following CLAUDE.md testing standards with literal validation
* Using dependency injection for deterministic testing
*/
import { describe, it, expect, beforeEach } from 'vitest';
import {
createMockExecutor,
createMockFileSystemExecutor,
createNoopExecutor,
createMockCommandResponse,
} from '../../../../test-utils/mock-executors.ts';
import swiftPackageBuild, { swift_package_buildLogic } from '../swift_package_build.ts';
import type { CommandExecutor } from '../../../../utils/execution/index.ts';
describe('swift_package_build plugin', () => {
describe('Export Field Validation (Literal)', () => {
it('should have correct name', () => {
expect(swiftPackageBuild.name).toBe('swift_package_build');
});
it('should have correct description', () => {
expect(swiftPackageBuild.description).toBe('Builds a Swift Package with swift build');
});
it('should have handler function', () => {
expect(typeof swiftPackageBuild.handler).toBe('function');
});
it('should validate schema correctly', () => {
// Test required fields
expect(swiftPackageBuild.schema.packagePath.safeParse('/test/package').success).toBe(true);
expect(swiftPackageBuild.schema.packagePath.safeParse('').success).toBe(true);
// Test optional fields
expect(swiftPackageBuild.schema.targetName.safeParse('MyTarget').success).toBe(true);
expect(swiftPackageBuild.schema.targetName.safeParse(undefined).success).toBe(true);
expect(swiftPackageBuild.schema.configuration.safeParse('debug').success).toBe(true);
expect(swiftPackageBuild.schema.configuration.safeParse('release').success).toBe(true);
expect(swiftPackageBuild.schema.configuration.safeParse(undefined).success).toBe(true);
expect(swiftPackageBuild.schema.architectures.safeParse(['arm64']).success).toBe(true);
expect(swiftPackageBuild.schema.architectures.safeParse(undefined).success).toBe(true);
expect(swiftPackageBuild.schema.parseAsLibrary.safeParse(true).success).toBe(true);
expect(swiftPackageBuild.schema.parseAsLibrary.safeParse(undefined).success).toBe(true);
// Test invalid inputs
expect(swiftPackageBuild.schema.packagePath.safeParse(null).success).toBe(false);
expect(swiftPackageBuild.schema.configuration.safeParse('invalid').success).toBe(false);
expect(swiftPackageBuild.schema.architectures.safeParse('not-array').success).toBe(false);
expect(swiftPackageBuild.schema.parseAsLibrary.safeParse('yes').success).toBe(false);
});
});
let executorCalls: any[] = [];
beforeEach(() => {
executorCalls = [];
});
describe('Command Generation Testing', () => {
it('should build correct command for basic build', async () => {
const executor: CommandExecutor = async (args, description, useShell, opts) => {
executorCalls.push({ args, description, useShell, cwd: opts?.cwd });
return createMockCommandResponse({
success: true,
output: 'Build succeeded',
error: undefined,
});
};
await swift_package_buildLogic(
{
packagePath: '/test/package',
},
executor,
);
expect(executorCalls).toEqual([
{
args: ['swift', 'build', '--package-path', '/test/package'],
description: 'Swift Package Build',
useShell: true,
cwd: undefined,
},
]);
});
it('should build correct command with release configuration', async () => {
const executor: CommandExecutor = async (args, description, useShell, opts) => {
executorCalls.push({ args, description, useShell, cwd: opts?.cwd });
return createMockCommandResponse({
success: true,
output: 'Build succeeded',
error: undefined,
});
};
await swift_package_buildLogic(
{
packagePath: '/test/package',
configuration: 'release',
},
executor,
);
expect(executorCalls).toEqual([
{
args: ['swift', 'build', '--package-path', '/test/package', '-c', 'release'],
description: 'Swift Package Build',
useShell: true,
cwd: undefined,
},
]);
});
it('should build correct command with all parameters', async () => {
const executor: CommandExecutor = async (args, description, useShell, opts) => {
executorCalls.push({ args, description, useShell, cwd: opts?.cwd });
return createMockCommandResponse({
success: true,
output: 'Build succeeded',
error: undefined,
});
};
await swift_package_buildLogic(
{
packagePath: '/test/package',
targetName: 'MyTarget',
configuration: 'release',
architectures: ['arm64', 'x86_64'],
parseAsLibrary: true,
},
executor,
);
expect(executorCalls).toEqual([
{
args: [
'swift',
'build',
'--package-path',
'/test/package',
'-c',
'release',
'--target',
'MyTarget',
'--arch',
'arm64',
'--arch',
'x86_64',
'-Xswiftc',
'-parse-as-library',
],
description: 'Swift Package Build',
useShell: true,
cwd: undefined,
},
]);
});
});
describe('Response Logic Testing', () => {
it('should handle missing packagePath parameter (Zod handles validation)', async () => {
// Note: With createTypedTool, Zod validation happens before the logic function is called
// So we test with a valid but minimal parameter set since validation is handled upstream
const executor = createMockExecutor({
success: true,
output: 'Build succeeded',
});
const result = await swift_package_buildLogic({ packagePath: '/test/package' }, executor);
// The logic function should execute normally with valid parameters
// Zod validation errors are handled by createTypedTool wrapper
expect(result.isError).toBe(false);
});
it('should return successful build response', async () => {
const executor = createMockExecutor({
success: true,
output: 'Build complete.',
});
const result = await swift_package_buildLogic(
{
packagePath: '/test/package',
},
executor,
);
expect(result).toEqual({
content: [
{ type: 'text', text: '✅ Swift package build succeeded.' },
{
type: 'text',
text: '💡 Next: Run tests with swift_package_test or execute with swift_package_run',
},
{ type: 'text', text: 'Build complete.' },
],
isError: false,
});
});
it('should return error response for build failure', async () => {
const executor = createMockExecutor({
success: false,
error: 'Compilation failed: error in main.swift',
});
const result = await swift_package_buildLogic(
{
packagePath: '/test/package',
},
executor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Error: Swift package build failed\nDetails: Compilation failed: error in main.swift',
},
],
isError: true,
});
});
it('should handle spawn error', async () => {
const executor = async () => {
throw new Error('spawn ENOENT');
};
const result = await swift_package_buildLogic(
{
packagePath: '/test/package',
},
executor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Error: Failed to execute swift build\nDetails: spawn ENOENT',
},
],
isError: true,
});
});
it('should handle successful build with parameters', async () => {
const executor = createMockExecutor({
success: true,
output: 'Build complete.',
});
const result = await swift_package_buildLogic(
{
packagePath: '/test/package',
targetName: 'MyTarget',
configuration: 'release',
architectures: ['arm64', 'x86_64'],
parseAsLibrary: true,
},
executor,
);
expect(result).toEqual({
content: [
{ type: 'text', text: '✅ Swift package build succeeded.' },
{
type: 'text',
text: '💡 Next: Run tests with swift_package_test or execute with swift_package_run',
},
{ type: 'text', text: 'Build complete.' },
],
isError: false,
});
});
});
});
```
--------------------------------------------------------------------------------
/src/mcp/tools/macos/__tests__/launch_mac_app.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Pure dependency injection test for launch_mac_app plugin
*
* Tests plugin structure and macOS app launching functionality including parameter validation,
* command generation, file validation, and response formatting.
*
* Uses manual call tracking and createMockFileSystemExecutor for file operations.
*/
import { describe, it, expect } from 'vitest';
import * as z from 'zod';
import {
createMockCommandResponse,
createMockFileSystemExecutor,
} from '../../../../test-utils/mock-executors.ts';
import launchMacApp, { launch_mac_appLogic } from '../launch_mac_app.ts';
describe('launch_mac_app plugin', () => {
describe('Export Field Validation (Literal)', () => {
it('should have correct name', () => {
expect(launchMacApp.name).toBe('launch_mac_app');
});
it('should have correct description', () => {
expect(launchMacApp.description).toBe(
"Launches a macOS application. IMPORTANT: You MUST provide the appPath parameter. Example: launch_mac_app({ appPath: '/path/to/your/app.app' }) Note: In some environments, this tool may be prefixed as mcp0_launch_macos_app.",
);
});
it('should have handler function', () => {
expect(typeof launchMacApp.handler).toBe('function');
});
it('should validate schema with valid inputs', () => {
const schema = z.object(launchMacApp.schema);
expect(
schema.safeParse({
appPath: '/path/to/MyApp.app',
}).success,
).toBe(true);
expect(
schema.safeParse({
appPath: '/Applications/Calculator.app',
args: ['--debug'],
}).success,
).toBe(true);
expect(
schema.safeParse({
appPath: '/path/to/MyApp.app',
args: ['--debug', '--verbose'],
}).success,
).toBe(true);
});
it('should validate schema with invalid inputs', () => {
const schema = z.object(launchMacApp.schema);
expect(schema.safeParse({}).success).toBe(false);
expect(schema.safeParse({ appPath: null }).success).toBe(false);
expect(schema.safeParse({ appPath: 123 }).success).toBe(false);
expect(schema.safeParse({ appPath: '/path/to/MyApp.app', args: 'not-array' }).success).toBe(
false,
);
});
});
describe('Input Validation', () => {
it('should handle non-existent app path', async () => {
const mockExecutor = async () => Promise.resolve(createMockCommandResponse());
const mockFileSystem = createMockFileSystemExecutor({
existsSync: () => false,
});
const result = await launch_mac_appLogic(
{
appPath: '/path/to/NonExistent.app',
},
mockExecutor,
mockFileSystem,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: "File not found: '/path/to/NonExistent.app'. Please check the path and try again.",
},
],
isError: true,
});
});
});
describe('Command Generation', () => {
it('should generate correct command with minimal parameters', async () => {
const calls: any[] = [];
const mockExecutor = async (command: string[]) => {
calls.push({ command });
return createMockCommandResponse();
};
const mockFileSystem = createMockFileSystemExecutor({
existsSync: () => true,
});
await launch_mac_appLogic(
{
appPath: '/path/to/MyApp.app',
},
mockExecutor,
mockFileSystem,
);
expect(calls).toHaveLength(1);
expect(calls[0].command).toEqual(['open', '/path/to/MyApp.app']);
});
it('should generate correct command with args parameter', async () => {
const calls: any[] = [];
const mockExecutor = async (command: string[]) => {
calls.push({ command });
return createMockCommandResponse();
};
const mockFileSystem = createMockFileSystemExecutor({
existsSync: () => true,
});
await launch_mac_appLogic(
{
appPath: '/path/to/MyApp.app',
args: ['--debug', '--verbose'],
},
mockExecutor,
mockFileSystem,
);
expect(calls).toHaveLength(1);
expect(calls[0].command).toEqual([
'open',
'/path/to/MyApp.app',
'--args',
'--debug',
'--verbose',
]);
});
it('should generate correct command with empty args array', async () => {
const calls: any[] = [];
const mockExecutor = async (command: string[]) => {
calls.push({ command });
return createMockCommandResponse();
};
const mockFileSystem = createMockFileSystemExecutor({
existsSync: () => true,
});
await launch_mac_appLogic(
{
appPath: '/path/to/MyApp.app',
args: [],
},
mockExecutor,
mockFileSystem,
);
expect(calls).toHaveLength(1);
expect(calls[0].command).toEqual(['open', '/path/to/MyApp.app']);
});
it('should handle paths with spaces correctly', async () => {
const calls: any[] = [];
const mockExecutor = async (command: string[]) => {
calls.push({ command });
return createMockCommandResponse();
};
const mockFileSystem = createMockFileSystemExecutor({
existsSync: () => true,
});
await launch_mac_appLogic(
{
appPath: '/Applications/My App.app',
},
mockExecutor,
mockFileSystem,
);
expect(calls).toHaveLength(1);
expect(calls[0].command).toEqual(['open', '/Applications/My App.app']);
});
});
describe('Response Processing', () => {
it('should return successful launch response', async () => {
const mockExecutor = async () => Promise.resolve(createMockCommandResponse());
const mockFileSystem = createMockFileSystemExecutor({
existsSync: () => true,
});
const result = await launch_mac_appLogic(
{
appPath: '/path/to/MyApp.app',
},
mockExecutor,
mockFileSystem,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: '✅ macOS app launched successfully: /path/to/MyApp.app',
},
],
});
});
it('should return successful launch response with args', async () => {
const mockExecutor = async () => Promise.resolve(createMockCommandResponse());
const mockFileSystem = createMockFileSystemExecutor({
existsSync: () => true,
});
const result = await launch_mac_appLogic(
{
appPath: '/path/to/MyApp.app',
args: ['--debug', '--verbose'],
},
mockExecutor,
mockFileSystem,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: '✅ macOS app launched successfully: /path/to/MyApp.app',
},
],
});
});
it('should handle launch failure with Error object', async () => {
const mockExecutor = async () => {
throw new Error('App not found');
};
const mockFileSystem = createMockFileSystemExecutor({
existsSync: () => true,
});
const result = await launch_mac_appLogic(
{
appPath: '/path/to/MyApp.app',
},
mockExecutor,
mockFileSystem,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: '❌ Launch macOS app operation failed: App not found',
},
],
isError: true,
});
});
it('should handle launch failure with string error', async () => {
const mockExecutor = async () => {
throw 'Permission denied';
};
const mockFileSystem = createMockFileSystemExecutor({
existsSync: () => true,
});
const result = await launch_mac_appLogic(
{
appPath: '/path/to/MyApp.app',
},
mockExecutor,
mockFileSystem,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: '❌ Launch macOS app operation failed: Permission denied',
},
],
isError: true,
});
});
it('should handle launch failure with unknown error type', async () => {
const mockExecutor = async () => {
throw 123;
};
const mockFileSystem = createMockFileSystemExecutor({
existsSync: () => true,
});
const result = await launch_mac_appLogic(
{
appPath: '/path/to/MyApp.app',
},
mockExecutor,
mockFileSystem,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: '❌ Launch macOS app operation failed: 123',
},
],
isError: true,
});
});
});
});
```
--------------------------------------------------------------------------------
/src/mcp/tools/project-discovery/__tests__/get_mac_bundle_id.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, beforeEach } from 'vitest';
import * as z from 'zod';
import plugin, { get_mac_bundle_idLogic } from '../get_mac_bundle_id.ts';
import {
createMockFileSystemExecutor,
createCommandMatchingMockExecutor,
} from '../../../../test-utils/mock-executors.ts';
describe('get_mac_bundle_id plugin', () => {
// Helper function to create mock executor for command matching
const createMockExecutorForCommands = (results: Record<string, string | Error>) => {
return createCommandMatchingMockExecutor(
Object.fromEntries(
Object.entries(results).map(([command, result]) => [
command,
result instanceof Error
? { success: false, error: result.message }
: { success: true, output: result },
]),
),
);
};
describe('Export Field Validation (Literal)', () => {
it('should have correct name', () => {
expect(plugin.name).toBe('get_mac_bundle_id');
});
it('should have correct description', () => {
expect(plugin.description).toBe(
"Extracts the bundle identifier from a macOS app bundle (.app). IMPORTANT: You MUST provide the appPath parameter. Example: get_mac_bundle_id({ appPath: '/path/to/your/app.app' }) Note: In some environments, this tool may be prefixed as mcp0_get_macos_bundle_id.",
);
});
it('should have handler function', () => {
expect(typeof plugin.handler).toBe('function');
});
it('should validate schema with valid inputs', () => {
const schema = z.object(plugin.schema);
expect(schema.safeParse({ appPath: '/Applications/TextEdit.app' }).success).toBe(true);
expect(schema.safeParse({ appPath: '/Users/dev/MyApp.app' }).success).toBe(true);
});
it('should validate schema with invalid inputs', () => {
const schema = z.object(plugin.schema);
expect(schema.safeParse({}).success).toBe(false);
expect(schema.safeParse({ appPath: 123 }).success).toBe(false);
expect(schema.safeParse({ appPath: null }).success).toBe(false);
expect(schema.safeParse({ appPath: undefined }).success).toBe(false);
});
});
describe('Handler Behavior (Complete Literal Returns)', () => {
// Note: appPath validation is now handled by Zod schema validation in createTypedTool
// This test would not reach the logic function as Zod validation occurs before it
it('should return error when file exists validation fails', async () => {
const mockExecutor = createMockExecutorForCommands({});
const mockFileSystemExecutor = createMockFileSystemExecutor({
existsSync: () => false,
});
const result = await get_mac_bundle_idLogic(
{ appPath: '/Applications/MyApp.app' },
mockExecutor,
mockFileSystemExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: "File not found: '/Applications/MyApp.app'. Please check the path and try again.",
},
],
isError: true,
});
});
it('should return success with bundle ID using defaults read', async () => {
const mockExecutor = createMockExecutorForCommands({
'defaults read "/Applications/MyApp.app/Contents/Info" CFBundleIdentifier':
'com.example.MyMacApp',
});
const mockFileSystemExecutor = createMockFileSystemExecutor({
existsSync: () => true,
});
const result = await get_mac_bundle_idLogic(
{ appPath: '/Applications/MyApp.app' },
mockExecutor,
mockFileSystemExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: '✅ Bundle ID: com.example.MyMacApp',
},
{
type: 'text',
text: `Next Steps:
- Launch: launch_mac_app({ appPath: "/Applications/MyApp.app" })
- Build again: build_macos({ scheme: "SCHEME_NAME" })`,
},
],
isError: false,
});
});
it('should fallback to PlistBuddy when defaults read fails', async () => {
const mockExecutor = createMockExecutorForCommands({
'defaults read "/Applications/MyApp.app/Contents/Info" CFBundleIdentifier': new Error(
'defaults read failed',
),
'/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/Applications/MyApp.app/Contents/Info.plist"':
'com.example.MyMacApp',
});
const mockFileSystemExecutor = createMockFileSystemExecutor({
existsSync: () => true,
});
const result = await get_mac_bundle_idLogic(
{ appPath: '/Applications/MyApp.app' },
mockExecutor,
mockFileSystemExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: '✅ Bundle ID: com.example.MyMacApp',
},
{
type: 'text',
text: `Next Steps:
- Launch: launch_mac_app({ appPath: "/Applications/MyApp.app" })
- Build again: build_macos({ scheme: "SCHEME_NAME" })`,
},
],
isError: false,
});
});
it('should return error when both extraction methods fail', async () => {
const mockExecutor = createMockExecutorForCommands({
'defaults read "/Applications/MyApp.app/Contents/Info" CFBundleIdentifier': new Error(
'Command failed',
),
'/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/Applications/MyApp.app/Contents/Info.plist"':
new Error('Command failed'),
});
const mockFileSystemExecutor = createMockFileSystemExecutor({
existsSync: () => true,
});
const result = await get_mac_bundle_idLogic(
{ appPath: '/Applications/MyApp.app' },
mockExecutor,
mockFileSystemExecutor,
);
expect(result.isError).toBe(true);
expect(result.content).toHaveLength(2);
expect(result.content[0].type).toBe('text');
expect(result.content[0].text).toContain('Error extracting macOS bundle ID');
expect(result.content[0].text).toContain('Could not extract bundle ID from Info.plist');
expect(result.content[0].text).toContain('Command failed');
expect(result.content[1].type).toBe('text');
expect(result.content[1].text).toBe(
'Make sure the path points to a valid macOS app bundle (.app directory).',
);
});
it('should handle Error objects in catch blocks', async () => {
const mockExecutor = createMockExecutorForCommands({
'defaults read "/Applications/MyApp.app/Contents/Info" CFBundleIdentifier': new Error(
'Custom error message',
),
'/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/Applications/MyApp.app/Contents/Info.plist"':
new Error('Custom error message'),
});
const mockFileSystemExecutor = createMockFileSystemExecutor({
existsSync: () => true,
});
const result = await get_mac_bundle_idLogic(
{ appPath: '/Applications/MyApp.app' },
mockExecutor,
mockFileSystemExecutor,
);
expect(result.isError).toBe(true);
expect(result.content).toHaveLength(2);
expect(result.content[0].type).toBe('text');
expect(result.content[0].text).toContain('Error extracting macOS bundle ID');
expect(result.content[0].text).toContain('Could not extract bundle ID from Info.plist');
expect(result.content[0].text).toContain('Custom error message');
expect(result.content[1].type).toBe('text');
expect(result.content[1].text).toBe(
'Make sure the path points to a valid macOS app bundle (.app directory).',
);
});
it('should handle string errors in catch blocks', async () => {
const mockExecutor = createMockExecutorForCommands({
'defaults read "/Applications/MyApp.app/Contents/Info" CFBundleIdentifier': new Error(
'String error',
),
'/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/Applications/MyApp.app/Contents/Info.plist"':
new Error('String error'),
});
const mockFileSystemExecutor = createMockFileSystemExecutor({
existsSync: () => true,
});
const result = await get_mac_bundle_idLogic(
{ appPath: '/Applications/MyApp.app' },
mockExecutor,
mockFileSystemExecutor,
);
expect(result.isError).toBe(true);
expect(result.content).toHaveLength(2);
expect(result.content[0].type).toBe('text');
expect(result.content[0].text).toContain('Error extracting macOS bundle ID');
expect(result.content[0].text).toContain('Could not extract bundle ID from Info.plist');
expect(result.content[0].text).toContain('String error');
expect(result.content[1].type).toBe('text');
expect(result.content[1].text).toBe(
'Make sure the path points to a valid macOS app bundle (.app directory).',
);
});
});
});
```
--------------------------------------------------------------------------------
/src/mcp/tools/doctor/__tests__/doctor.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Tests for doctor plugin
* Following CLAUDE.md testing standards with literal validation
* Using dependency injection for deterministic testing
*/
import { describe, it, expect, beforeEach } from 'vitest';
import * as z from 'zod';
import doctor, { runDoctor, type DoctorDependencies } from '../doctor.ts';
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
function createDeps(overrides?: Partial<DoctorDependencies>): DoctorDependencies {
const base: DoctorDependencies = {
commandExecutor: createMockExecutor({ output: 'lldb-dap' }),
binaryChecker: {
async checkBinaryAvailability(binary: string) {
// default: all available with generic version
return { available: true, version: `${binary} version 1.0.0` };
},
},
xcode: {
async getXcodeInfo() {
return {
version: 'Xcode 15.0 - Build version 15A240d',
path: '/Applications/Xcode.app/Contents/Developer',
selectedXcode: '/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild',
xcrunVersion: 'xcrun version 65',
};
},
},
env: {
getEnvironmentVariables() {
const x: Record<string, string | undefined> = {
XCODEBUILDMCP_DEBUG: 'true',
INCREMENTAL_BUILDS_ENABLED: '1',
PATH: '/usr/local/bin:/usr/bin:/bin',
DEVELOPER_DIR: '/Applications/Xcode.app/Contents/Developer',
HOME: '/Users/testuser',
USER: 'testuser',
TMPDIR: '/tmp',
NODE_ENV: 'test',
SENTRY_DISABLED: 'false',
};
return x;
},
getSystemInfo() {
return {
platform: 'darwin',
release: '25.0.0',
arch: 'arm64',
cpus: '10 x Apple M3',
memory: '32 GB',
hostname: 'localhost',
username: 'testuser',
homedir: '/Users/testuser',
tmpdir: '/tmp',
};
},
getNodeInfo() {
return {
version: 'v22.0.0',
execPath: '/usr/local/bin/node',
pid: '123',
ppid: '1',
platform: 'darwin',
arch: 'arm64',
cwd: '/',
argv: 'node build/index.js',
};
},
},
plugins: {
async getPluginSystemInfo() {
return {
totalPlugins: 1,
pluginDirectories: 1,
pluginsByDirectory: { doctor: ['doctor'] },
systemMode: 'plugin-based',
};
},
},
features: {
areAxeToolsAvailable: () => true,
isXcodemakeEnabled: () => true,
isXcodemakeAvailable: async () => true,
doesMakefileExist: () => true,
},
runtime: {
async getRuntimeToolInfo() {
return {
mode: 'runtime' as const,
enabledWorkflows: ['doctor'],
enabledTools: ['doctor'],
totalRegistered: 1,
};
},
},
};
return {
...base,
...overrides,
binaryChecker: {
...base.binaryChecker,
...(overrides?.binaryChecker ?? {}),
},
xcode: {
...base.xcode,
...(overrides?.xcode ?? {}),
},
env: {
...base.env,
...(overrides?.env ?? {}),
},
plugins: {
...base.plugins,
...(overrides?.plugins ?? {}),
},
features: {
...base.features,
...(overrides?.features ?? {}),
},
};
}
describe('doctor tool', () => {
// Reset any state if needed
describe('Export Field Validation (Literal)', () => {
it('should have correct name', () => {
expect(doctor.name).toBe('doctor');
});
it('should have correct description', () => {
expect(doctor.description).toBe(
'Provides comprehensive information about the MCP server environment, available dependencies, and configuration status.',
);
});
it('should have handler function', () => {
expect(typeof doctor.handler).toBe('function');
});
it('should have correct schema with enabled boolean field', () => {
const schema = z.object(doctor.schema);
// Valid inputs
expect(schema.safeParse({ enabled: true }).success).toBe(true);
expect(schema.safeParse({ enabled: false }).success).toBe(true);
expect(schema.safeParse({}).success).toBe(true); // enabled is optional
// Invalid inputs
expect(schema.safeParse({ enabled: 'true' }).success).toBe(false);
expect(schema.safeParse({ enabled: 1 }).success).toBe(false);
expect(schema.safeParse({ enabled: null }).success).toBe(false);
});
});
describe('Handler Behavior (Complete Literal Returns)', () => {
it('should handle successful doctor execution', async () => {
const deps = createDeps();
const result = await runDoctor({ enabled: true }, deps);
expect(result.content).toEqual([
{
type: 'text',
text: result.content[0].text,
},
]);
expect(typeof result.content[0].text).toBe('string');
});
it('should handle plugin loading failure', async () => {
const deps = createDeps({
plugins: {
async getPluginSystemInfo() {
return { error: 'Plugin loading failed', systemMode: 'error' };
},
},
});
const result = await runDoctor({ enabled: true }, deps);
expect(result.content).toEqual([
{
type: 'text',
text: result.content[0].text,
},
]);
expect(typeof result.content[0].text).toBe('string');
});
it('should handle xcode command failure', async () => {
const deps = createDeps({
xcode: {
async getXcodeInfo() {
return { error: 'Xcode not found' };
},
},
});
const result = await runDoctor({ enabled: true }, deps);
expect(result.content).toEqual([
{
type: 'text',
text: result.content[0].text,
},
]);
expect(typeof result.content[0].text).toBe('string');
});
it('should handle xcodemake check failure', async () => {
const deps = createDeps({
features: {
areAxeToolsAvailable: () => true,
isXcodemakeEnabled: () => true,
isXcodemakeAvailable: async () => false,
doesMakefileExist: () => true,
},
binaryChecker: {
async checkBinaryAvailability(binary: string) {
if (binary === 'xcodemake') return { available: false };
return { available: true, version: `${binary} version 1.0.0` };
},
},
});
const result = await runDoctor({ enabled: true }, deps);
expect(result.content).toEqual([
{
type: 'text',
text: result.content[0].text,
},
]);
expect(typeof result.content[0].text).toBe('string');
});
it('should handle axe tools not available', async () => {
const deps = createDeps({
features: {
areAxeToolsAvailable: () => false,
isXcodemakeEnabled: () => false,
isXcodemakeAvailable: async () => false,
doesMakefileExist: () => false,
},
binaryChecker: {
async checkBinaryAvailability(binary: string) {
if (binary === 'axe') return { available: false };
if (binary === 'xcodemake') return { available: false };
if (binary === 'mise') return { available: true, version: 'mise 1.0.0' };
return { available: true };
},
},
env: {
getEnvironmentVariables() {
const x: Record<string, string | undefined> = {
XCODEBUILDMCP_DEBUG: 'true',
INCREMENTAL_BUILDS_ENABLED: '0',
PATH: '/usr/local/bin:/usr/bin:/bin',
DEVELOPER_DIR: '/Applications/Xcode.app/Contents/Developer',
HOME: '/Users/testuser',
USER: 'testuser',
TMPDIR: '/tmp',
NODE_ENV: 'test',
SENTRY_DISABLED: 'true',
};
return x;
},
getSystemInfo: () => ({
platform: 'darwin',
release: '25.0.0',
arch: 'arm64',
cpus: '10 x Apple M3',
memory: '32 GB',
hostname: 'localhost',
username: 'testuser',
homedir: '/Users/testuser',
tmpdir: '/tmp',
}),
getNodeInfo: () => ({
version: 'v22.0.0',
execPath: '/usr/local/bin/node',
pid: '123',
ppid: '1',
platform: 'darwin',
arch: 'arm64',
cwd: '/',
argv: 'node build/index.js',
}),
},
});
const result = await runDoctor({ enabled: true }, deps);
expect(result.content).toEqual([
{
type: 'text',
text: result.content[0].text,
},
]);
expect(typeof result.content[0].text).toBe('string');
});
});
});
```
--------------------------------------------------------------------------------
/src/mcp/tools/logging/__tests__/start_sim_log_cap.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Tests for start_sim_log_cap plugin
*/
import { describe, it, expect } from 'vitest';
import * as z from 'zod';
import plugin, { start_sim_log_capLogic } from '../start_sim_log_cap.ts';
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
describe('start_sim_log_cap plugin', () => {
// Reset any test state if needed
describe('Export Field Validation (Literal)', () => {
it('should export an object with required properties', () => {
expect(plugin).toHaveProperty('name');
expect(plugin).toHaveProperty('description');
expect(plugin).toHaveProperty('schema');
expect(plugin).toHaveProperty('handler');
});
it('should have correct tool name', () => {
expect(plugin.name).toBe('start_sim_log_cap');
});
it('should have correct description', () => {
expect(plugin.description).toBe(
'Starts capturing logs from a specified simulator. Returns a session ID. By default, captures only structured logs.',
);
});
it('should have handler as a function', () => {
expect(typeof plugin.handler).toBe('function');
});
it('should validate schema with valid parameters', () => {
const schema = z.object(plugin.schema);
expect(schema.safeParse({ bundleId: 'com.example.app' }).success).toBe(true);
expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: true }).success).toBe(
true,
);
expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: false }).success).toBe(
true,
);
});
it('should reject invalid schema parameters', () => {
const schema = z.object(plugin.schema);
expect(schema.safeParse({ bundleId: null }).success).toBe(false);
expect(schema.safeParse({ captureConsole: true }).success).toBe(false);
expect(schema.safeParse({}).success).toBe(false);
expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: 'yes' }).success).toBe(
false,
);
expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: 123 }).success).toBe(
false,
);
const withSimId = schema.safeParse({ simulatorId: 'test-uuid', bundleId: 'com.example.app' });
expect(withSimId.success).toBe(true);
expect('simulatorId' in (withSimId.data as any)).toBe(false);
});
});
describe('Handler Behavior (Complete Literal Returns)', () => {
// Note: Parameter validation is now handled by createTypedTool wrapper
// Invalid parameters will not reach the logic function, so we test valid scenarios
it('should return error when log capture fails', async () => {
const mockExecutor = createMockExecutor({ success: true, output: '' });
const logCaptureStub = (params: any, executor: any) => {
return Promise.resolve({
sessionId: '',
logFilePath: '',
processes: [],
error: 'Permission denied',
});
};
const result = await start_sim_log_capLogic(
{
simulatorId: 'test-uuid',
bundleId: 'com.example.app',
},
mockExecutor,
logCaptureStub,
);
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe('Error starting log capture: Permission denied');
});
it('should return success with session ID when log capture starts successfully', async () => {
const mockExecutor = createMockExecutor({ success: true, output: '' });
const logCaptureStub = (params: any, executor: any) => {
return Promise.resolve({
sessionId: 'test-uuid-123',
logFilePath: '/tmp/test.log',
processes: [],
error: undefined,
});
};
const result = await start_sim_log_capLogic(
{
simulatorId: 'test-uuid',
bundleId: 'com.example.app',
},
mockExecutor,
logCaptureStub,
);
expect(result.isError).toBeUndefined();
expect(result.content[0].text).toBe(
"Log capture started successfully. Session ID: test-uuid-123.\n\nNote: Only structured logs are being captured.\n\nNext Steps:\n1. Interact with your simulator and app.\n2. Use 'stop_sim_log_cap' with session ID 'test-uuid-123' to stop capture and retrieve logs.",
);
});
it('should indicate console capture when captureConsole is true', async () => {
const mockExecutor = createMockExecutor({ success: true, output: '' });
const logCaptureStub = (params: any, executor: any) => {
return Promise.resolve({
sessionId: 'test-uuid-123',
logFilePath: '/tmp/test.log',
processes: [],
error: undefined,
});
};
const result = await start_sim_log_capLogic(
{
simulatorId: 'test-uuid',
bundleId: 'com.example.app',
captureConsole: true,
},
mockExecutor,
logCaptureStub,
);
expect(result.content[0].text).toBe(
"Log capture started successfully. Session ID: test-uuid-123.\n\nNote: Your app was relaunched to capture console output.\n\nNext Steps:\n1. Interact with your simulator and app.\n2. Use 'stop_sim_log_cap' with session ID 'test-uuid-123' to stop capture and retrieve logs.",
);
});
it('should create correct spawn commands for console capture', async () => {
const mockExecutor = createMockExecutor({ success: true, output: '' });
const spawnCalls: Array<{
command: string;
args: string[];
}> = [];
const logCaptureStub = (params: any, executor: any) => {
if (params.captureConsole) {
// Record the console capture spawn call
spawnCalls.push({
command: 'xcrun',
args: [
'simctl',
'launch',
'--console-pty',
'--terminate-running-process',
params.simulatorUuid,
params.bundleId,
],
});
}
// Record the structured log capture spawn call
spawnCalls.push({
command: 'xcrun',
args: [
'simctl',
'spawn',
params.simulatorUuid,
'log',
'stream',
'--level=debug',
'--predicate',
`subsystem == "${params.bundleId}"`,
],
});
return Promise.resolve({
sessionId: 'test-uuid-123',
logFilePath: '/tmp/test.log',
processes: [],
error: undefined,
});
};
await start_sim_log_capLogic(
{
simulatorId: 'test-uuid',
bundleId: 'com.example.app',
captureConsole: true,
},
mockExecutor,
logCaptureStub,
);
// Should spawn both console capture and structured log capture
expect(spawnCalls).toHaveLength(2);
expect(spawnCalls[0]).toEqual({
command: 'xcrun',
args: [
'simctl',
'launch',
'--console-pty',
'--terminate-running-process',
'test-uuid',
'com.example.app',
],
});
expect(spawnCalls[1]).toEqual({
command: 'xcrun',
args: [
'simctl',
'spawn',
'test-uuid',
'log',
'stream',
'--level=debug',
'--predicate',
'subsystem == "com.example.app"',
],
});
});
it('should create correct spawn commands for structured logs only', async () => {
const mockExecutor = createMockExecutor({ success: true, output: '' });
const spawnCalls: Array<{
command: string;
args: string[];
}> = [];
const logCaptureStub = (params: any, executor: any) => {
// Record the structured log capture spawn call only
spawnCalls.push({
command: 'xcrun',
args: [
'simctl',
'spawn',
params.simulatorUuid,
'log',
'stream',
'--level=debug',
'--predicate',
`subsystem == "${params.bundleId}"`,
],
});
return Promise.resolve({
sessionId: 'test-uuid-123',
logFilePath: '/tmp/test.log',
processes: [],
error: undefined,
});
};
await start_sim_log_capLogic(
{
simulatorId: 'test-uuid',
bundleId: 'com.example.app',
captureConsole: false,
},
mockExecutor,
logCaptureStub,
);
// Should only spawn structured log capture
expect(spawnCalls).toHaveLength(1);
expect(spawnCalls[0]).toEqual({
command: 'xcrun',
args: [
'simctl',
'spawn',
'test-uuid',
'log',
'stream',
'--level=debug',
'--predicate',
'subsystem == "com.example.app"',
],
});
});
});
});
```
--------------------------------------------------------------------------------
/src/utils/typed-tool-factory.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Type-safe tool factory for XcodeBuildMCP
*
* This module provides a factory function to create MCP tool handlers that safely
* convert from the generic Record<string, unknown> signature required by the MCP SDK
* to strongly-typed parameters using runtime validation with Zod.
*
* This eliminates the need for unsafe type assertions while maintaining full
* compatibility with the MCP SDK's tool handler signature requirements.
*/
import * as z from 'zod';
import { ToolResponse } from '../types/common.ts';
import type { CommandExecutor } from './execution/index.ts';
import { createErrorResponse } from './responses/index.ts';
import { sessionStore, type SessionDefaults } from './session-store.ts';
import { isSessionDefaultsSchemaOptOutEnabled } from './environment.ts';
function createValidatedHandler<TParams, TContext>(
schema: z.ZodType<TParams, unknown>,
logicFunction: (params: TParams, context: TContext) => Promise<ToolResponse>,
getContext: () => TContext,
): (args: Record<string, unknown>) => Promise<ToolResponse> {
return async (args: Record<string, unknown>): Promise<ToolResponse> => {
try {
const validatedParams = schema.parse(args);
return await logicFunction(validatedParams, getContext());
} catch (error) {
if (error instanceof z.ZodError) {
const details = `Invalid parameters:\n${formatZodIssues(error)}`;
return createErrorResponse('Parameter validation failed', details);
}
// Re-throw unexpected errors (they'll be caught by the MCP framework)
throw error;
}
};
}
/**
* Creates a type-safe tool handler that validates parameters at runtime
* before passing them to the typed logic function.
*
* @param schema - Zod schema for parameter validation
* @param logicFunction - The typed logic function to execute
* @param getExecutor - Function to get the command executor (must be provided)
* @returns A handler function compatible with MCP SDK requirements
*/
export function createTypedTool<TParams>(
schema: z.ZodType<TParams, unknown>,
logicFunction: (params: TParams, executor: CommandExecutor) => Promise<ToolResponse>,
getExecutor: () => CommandExecutor,
): (args: Record<string, unknown>) => Promise<ToolResponse> {
return createValidatedHandler(schema, logicFunction, getExecutor);
}
export function createTypedToolWithContext<TParams, TContext>(
schema: z.ZodType<TParams, unknown>,
logicFunction: (params: TParams, context: TContext) => Promise<ToolResponse>,
getContext: () => TContext,
): (args: Record<string, unknown>) => Promise<ToolResponse> {
return createValidatedHandler(schema, logicFunction, getContext);
}
export type SessionRequirement =
| { allOf: (keyof SessionDefaults)[]; message?: string }
| { oneOf: (keyof SessionDefaults)[]; message?: string };
function missingFromMerged(
keys: (keyof SessionDefaults)[],
merged: Record<string, unknown>,
): string[] {
return keys.filter((k) => merged[k] == null);
}
function formatRequirementError(opts: {
message: string;
setHint?: string;
optOutEnabled: boolean;
}): { title: string; body: string } {
const title = opts.optOutEnabled
? 'Missing required parameters'
: 'Missing required session defaults';
const body = opts.optOutEnabled
? opts.message
: [opts.message, opts.setHint].filter(Boolean).join('\n');
return { title, body };
}
type ToolSchemaShape = Record<string, z.ZodType>;
export function getSessionAwareToolSchemaShape(opts: {
sessionAware: z.ZodObject<ToolSchemaShape>;
legacy: z.ZodObject<ToolSchemaShape>;
}): ToolSchemaShape {
return isSessionDefaultsSchemaOptOutEnabled() ? opts.legacy.shape : opts.sessionAware.shape;
}
export function createSessionAwareTool<TParams>(opts: {
internalSchema: z.ZodType<TParams, unknown>;
logicFunction: (params: TParams, executor: CommandExecutor) => Promise<ToolResponse>;
getExecutor: () => CommandExecutor;
requirements?: SessionRequirement[];
exclusivePairs?: (keyof SessionDefaults)[][]; // when args provide one side, drop conflicting session-default side(s)
}): (rawArgs: Record<string, unknown>) => Promise<ToolResponse> {
return createSessionAwareHandler({
internalSchema: opts.internalSchema,
logicFunction: opts.logicFunction,
getContext: opts.getExecutor,
requirements: opts.requirements,
exclusivePairs: opts.exclusivePairs,
});
}
export function createSessionAwareToolWithContext<TParams, TContext>(opts: {
internalSchema: z.ZodType<TParams, unknown>;
logicFunction: (params: TParams, context: TContext) => Promise<ToolResponse>;
getContext: () => TContext;
requirements?: SessionRequirement[];
exclusivePairs?: (keyof SessionDefaults)[][];
}): (rawArgs: Record<string, unknown>) => Promise<ToolResponse> {
return createSessionAwareHandler(opts);
}
function createSessionAwareHandler<TParams, TContext>(opts: {
internalSchema: z.ZodType<TParams, unknown>;
logicFunction: (params: TParams, context: TContext) => Promise<ToolResponse>;
getContext: () => TContext;
requirements?: SessionRequirement[];
exclusivePairs?: (keyof SessionDefaults)[][];
}): (rawArgs: Record<string, unknown>) => Promise<ToolResponse> {
const {
internalSchema,
logicFunction,
getContext,
requirements = [],
exclusivePairs = [],
} = opts;
return async (rawArgs: Record<string, unknown>): Promise<ToolResponse> => {
try {
// Sanitize args: treat null/undefined as "not provided" so they don't override session defaults
const sanitizedArgs: Record<string, unknown> = {};
for (const [k, v] of Object.entries(rawArgs)) {
if (v === null || v === undefined) continue;
if (typeof v === 'string' && v.trim() === '') continue;
sanitizedArgs[k] = v;
}
// Factory-level mutual exclusivity check: if user provides multiple explicit values
// within an exclusive group, reject early even if tool schema doesn't enforce XOR.
for (const pair of exclusivePairs) {
const provided = pair.filter((k) => Object.prototype.hasOwnProperty.call(sanitizedArgs, k));
if (provided.length >= 2) {
return createErrorResponse(
'Parameter validation failed',
`Invalid parameters:\nMutually exclusive parameters provided: ${provided.join(
', ',
)}. Provide only one.`,
);
}
}
// Start with session defaults merged with explicit args (args override session)
const merged: Record<string, unknown> = { ...sessionStore.getAll(), ...sanitizedArgs };
// Apply exclusive pair pruning: only when caller provided a concrete (non-null/undefined) value
// for any key in the pair. When activated, drop other keys in the pair coming from session defaults.
for (const pair of exclusivePairs) {
const userProvidedConcrete = pair.some((k) =>
Object.prototype.hasOwnProperty.call(sanitizedArgs, k),
);
if (!userProvidedConcrete) continue;
for (const k of pair) {
if (!Object.prototype.hasOwnProperty.call(sanitizedArgs, k) && k in merged) {
delete merged[k];
}
}
}
for (const req of requirements) {
if ('allOf' in req) {
const missing = missingFromMerged(req.allOf, merged);
if (missing.length > 0) {
const setHint = `Set with: session-set-defaults { ${missing
.map((k) => `"${k}": "..."`)
.join(', ')} }`;
const { title, body } = formatRequirementError({
message: req.message ?? `Required: ${req.allOf.join(', ')}`,
setHint,
optOutEnabled: isSessionDefaultsSchemaOptOutEnabled(),
});
return createErrorResponse(title, body);
}
} else if ('oneOf' in req) {
const satisfied = req.oneOf.some((k) => merged[k] != null);
if (!satisfied) {
const options = req.oneOf.join(', ');
const setHints = req.oneOf
.map((k) => `session-set-defaults { "${k}": "..." }`)
.join(' OR ');
const { title, body } = formatRequirementError({
message: req.message ?? `Provide one of: ${options}`,
setHint: `Set with: ${setHints}`,
optOutEnabled: isSessionDefaultsSchemaOptOutEnabled(),
});
return createErrorResponse(title, body);
}
}
}
const validated = internalSchema.parse(merged);
return await logicFunction(validated, getContext());
} catch (error) {
if (error instanceof z.ZodError) {
const details = `Invalid parameters:\n${formatZodIssues(error)}`;
return createErrorResponse('Parameter validation failed', details);
}
throw error;
}
};
}
function formatZodIssues(error: z.ZodError): string {
return error.issues
.map((issue) => {
const path = issue.path.length > 0 ? issue.path.map(String).join('.') : 'root';
return `${path}: ${issue.message}`;
})
.join('\n');
}
```
--------------------------------------------------------------------------------
/docs/dev/SMITHERY.md:
--------------------------------------------------------------------------------
```markdown
# TypeScript Servers
> Deploy and publish TypeScript MCP servers on Smithery using Smithery CLI
## Overview
Deploy TypeScript MCP servers using the official MCP SDK with two deployment options:
* **Remote deployment**: Automatic containerization and infrastructure managed by Smithery
* **Local servers** (Beta): Distribute your server as [MCP bundle](https://github.com/anthropics/mcpb) allowing users to run it locally and one-click install it
## Prerequisites
* TypeScript MCP server using the official MCP SDK that exports the MCP server object at entry point
* Node.js 18+ and npm installed locally
* Smithery CLI installed as a dev dependency (`npm i -D @smithery/cli`)
<Note>
**New to MCP servers?** See the [Getting Started guide](/getting_started) to learn how to build TypeScript MCP servers from scratch using the official SDK.
</Note>
## Project Structure
Your TypeScript project should look like this:
```
my-mcp-server/
smithery.yaml # Smithery configuration
package.json # Node.js dependencies and scripts
tsconfig.json # TypeScript configuration
src/
index.ts # Your MCP server code with exported createServer function
```
## Setup
### 1. Configure smithery.yaml
Create a `smithery.yaml` file in your repository root (usually where the `package.json` is):
#### Remote Deployment (Default)
```yaml theme={null}
runtime: "typescript"
```
#### Local Server (Beta)
```yaml theme={null}
runtime: "typescript"
target: "local"
```
<Note>
**Local servers are in beta** - When you set `target: "local"`, your server runs locally on user's machine but is accessible through Smithery's registry for easy discovery and connection by MCP clients.
</Note>
### 2. Configure package.json
Your `package.json` must include the `module` field pointing to your server entry point:
```json theme={null}
{
"name": "my-mcp-server",
"version": "1.0.0",
"type": "module",
"module": "src/index.ts", // Points to your server entry point
"scripts": {
"build": "npx smithery build",
"dev": "npx smithery dev"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.17.3",
"zod": "^3.25.46"
},
"devDependencies": {
"@smithery/cli": "^1.4.6"
}
}
```
<Note>
Install the CLI locally with:
```bash theme={null}
npm i -D @smithery/cli
```
The Smithery CLI externalizes your SDKs during bundling so your runtime uses the versions you install. If you see a warning about missing SDKs, add them to your dependencies (most servers need `@modelcontextprotocol/sdk` and `@smithery/sdk`).
</Note>
### 3. Ensure Proper Server Structure
Your TypeScript MCP server must export a default `createServer` function that returns the MCP server object. If you built your server following the [Getting Started guide](/getting_started), it should already have this structure.
```typescript theme={null}
// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
// Required: Export default createServer function
export default function createServer({ config }) {
// config contains user-provided settings (see configSchema below)
const server = new McpServer({
name: "Your Server Name",
version: "1.0.0",
});
// Register your tools here...
return server.server; // Must return the MCP server object
}
```
**Optional Configuration Schema**: If your server needs user configuration (API keys, settings, etc.), export a `configSchema`:
```typescript theme={null}
// Optional: If your server doesn't need configuration, omit this
export const configSchema = z.object({
apiKey: z.string().describe("Your API key"),
timeout: z.number().default(5000).describe("Request timeout in milliseconds"),
});
```
**Where it goes**: Export `configSchema` from the same file as your `createServer` function (typically `src/index.ts`).
**What it does**: Automatically generates [session configuration](/build/session-config) forms for users connecting to your server.
## OAuth
<Note>
OAuth is designed only for **remote servers**. OAuth is not available for local servers (`target: "local"`).
</Note>
If your entry module exports `oauth`, Smithery CLI auto-mounts the required OAuth endpoints for you during remote deployment.
### Export an OAuth provider
```typescript theme={null}
// src/index.ts
import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js"
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import type { OAuthProvider } from "@smithery/sdk"
import { MyProvider } from "./provider.js"
export default function createServer({ auth }: { auth: AuthInfo }) {
const server = new McpServer({ name: "My MCP", version: "1.0.0" })
// register tools...
return server.server
}
export const oauth: OAuthProvider = new MyProvider() // [!code highlight]
```
The CLI detects `oauth` and injects the auth routes automatically. For implementing `OAuthServerProvider`, see the [official MCP SDK authorization guide](https://modelcontextprotocol.io/docs/tutorials/security/authorization).
<Tip>
**You don't need to implement client registration.** Modern MCP clients use [Client ID Metadata Documents](https://modelcontextprotocol.io/specification/draft/basic/authorization#client-id-metadata-documents) (CIMD). Your server should advertise `client_id_metadata_document_supported: true` in its OAuth metadata — see the [spec requirements](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#implementation-requirements).
</Tip>
## Local Development
Test your server locally using the Smithery CLI:
```bash theme={null}
# Start development server with interactive playground
npm run dev
```
This opens the **Smithery interactive playground** where you can:
* Test your MCP server tools in real-time
* See tool responses and debug issues
* Validate your configuration schema
* Experiment with different inputs
## Advanced Build Configuration
For advanced use cases, you can customize the build process using a `smithery.config.js` file. This is useful for:
* Marking packages as external (to avoid bundling issues)
* Configuring minification, targets, and other build options
* Adding custom esbuild plugins
### Configuration File
Create `smithery.config.js` in your project root:
```javascript theme={null}
export default {
esbuild: {
// Mark problematic packages as external
external: ["playwright-core", "puppeteer-core"],
// Enable minification for production
minify: true,
// Set Node.js target version
target: "node18",
},
};
```
### Common Use Cases
**External Dependencies**: If you encounter bundling issues with packages like Playwright or native modules:
```javascript theme={null}
export default {
esbuild: {
external: ["playwright-core", "sharp", "@grpc/grpc-js"],
},
};
```
Configuration applies to both `build` and `dev` commands.
## Deploy
1. Push your code (including `smithery.yaml`) to GitHub
2. [Connect your GitHub](https://smithery.ai/new) to Smithery (or claim your server if already listed)
3. Navigate to the Deployments tab on your server page
4. Click Deploy to build and host your server
## Good to Know
<Accordion title="What happens under the hood">
**Remote Deployment**: When you deploy to Smithery's infrastructure:
1. Clone your repository
2. Parse your `smithery.yaml` to detect TypeScript runtime
3. Install dependencies with `npm ci`
4. Build your TypeScript code using the `module` entry point from your `package.json`
5. Package your server into a containerized HTTP service
6. Deploy the container to our hosting infrastructure
7. Send MCP `initialize` and `list_tools` messages with a dummy configuration to discover your server's capabilities
8. Make it available at `https://server.smithery.ai/your-server`
9. Handle load balancing, scaling, and monitoring
**Local Server (Beta)**: When you use `target: "local"`:
1. Your server runs locally on user's machine using `npm run dev`
2. Smithery registers your server in the registry for discovery
3. MCP clients can find and connect to your local server through Smithery
4. Your server remains under your control while being accessible to others
</Accordion>
## Troubleshooting
<Accordion title="Why does my deployment fail?">
Common issues and solutions:
**Remote Deployment Issues**:
* **Missing module field**: Ensure your `package.json` has the `module` field pointing to your entry point
* **Dependencies not found**: All dependencies must be listed in `dependencies` or `devDependencies`
* **Server doesn't build locally**: Before deploying, verify your server builds and runs locally:
```bash theme={null}
npm install
npm run build
```
If this fails, fix any TypeScript compilation errors or missing dependencies first
**Local Server Issues** (Beta):
* **Server not discoverable**: Ensure you have `target: "local"` in your `smithery.yaml`
* **Local server won't start**: Verify your server runs with `npm run dev` before expecting registry integration
* **Connection issues**: Make sure your local development environment allows the necessary network connections
</Accordion>
---
> To find navigation and other pages in this documentation, fetch the llms.txt file at: https://smithery.ai/docs/llms.txt
```
--------------------------------------------------------------------------------
/src/mcp/tools/project-discovery/discover_projs.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Project Discovery Plugin: Discover Projects
*
* Scans a directory (defaults to workspace root) to find Xcode project (.xcodeproj)
* and workspace (.xcworkspace) files.
*/
import * as z from 'zod';
import * as path from 'node:path';
import { log } from '../../../utils/logging/index.ts';
import { ToolResponse, createTextContent } from '../../../types/common.ts';
import { getDefaultFileSystemExecutor, getDefaultCommandExecutor } from '../../../utils/command.ts';
import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
// Constants
const DEFAULT_MAX_DEPTH = 5;
const SKIPPED_DIRS = new Set(['build', 'DerivedData', 'Pods', '.git', 'node_modules']);
// Type definition for Dirent-like objects returned by readdir with withFileTypes: true
interface DirentLike {
name: string;
isDirectory(): boolean;
isSymbolicLink(): boolean;
}
/**
* Recursively scans directories to find Xcode projects and workspaces.
*/
async function _findProjectsRecursive(
currentDirAbs: string,
workspaceRootAbs: string,
currentDepth: number,
maxDepth: number,
results: { projects: string[]; workspaces: string[] },
fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor(),
): Promise<void> {
// Explicit depth check (now simplified as maxDepth is always non-negative)
if (currentDepth >= maxDepth) {
log('debug', `Max depth ${maxDepth} reached at ${currentDirAbs}, stopping recursion.`);
return;
}
log('debug', `Scanning directory: ${currentDirAbs} at depth ${currentDepth}`);
const normalizedWorkspaceRoot = path.normalize(workspaceRootAbs);
try {
// Use the injected fileSystemExecutor
const entries = await fileSystemExecutor.readdir(currentDirAbs, { withFileTypes: true });
for (const rawEntry of entries) {
// Cast the unknown entry to DirentLike interface for type safety
const entry = rawEntry as DirentLike;
const absoluteEntryPath = path.join(currentDirAbs, entry.name);
const relativePath = path.relative(workspaceRootAbs, absoluteEntryPath);
// --- Skip conditions ---
if (entry.isSymbolicLink()) {
log('debug', `Skipping symbolic link: ${relativePath}`);
continue;
}
// Skip common build/dependency directories by name
if (entry.isDirectory() && SKIPPED_DIRS.has(entry.name)) {
log('debug', `Skipping standard directory: ${relativePath}`);
continue;
}
// Ensure entry is within the workspace root (security/sanity check)
if (!path.normalize(absoluteEntryPath).startsWith(normalizedWorkspaceRoot)) {
log(
'warn',
`Skipping entry outside workspace root: ${absoluteEntryPath} (Workspace: ${workspaceRootAbs})`,
);
continue;
}
// --- Process entries ---
if (entry.isDirectory()) {
let isXcodeBundle = false;
if (entry.name.endsWith('.xcodeproj')) {
results.projects.push(absoluteEntryPath); // Use absolute path
log('debug', `Found project: ${absoluteEntryPath}`);
isXcodeBundle = true;
} else if (entry.name.endsWith('.xcworkspace')) {
results.workspaces.push(absoluteEntryPath); // Use absolute path
log('debug', `Found workspace: ${absoluteEntryPath}`);
isXcodeBundle = true;
}
// Recurse into regular directories, but not into found project/workspace bundles
if (!isXcodeBundle) {
await _findProjectsRecursive(
absoluteEntryPath,
workspaceRootAbs,
currentDepth + 1,
maxDepth,
results,
fileSystemExecutor,
);
}
}
}
} catch (error) {
let code;
let message = 'Unknown error';
if (error instanceof Error) {
message = error.message;
if ('code' in error) {
code = error.code;
}
} else if (typeof error === 'object' && error !== null) {
if ('message' in error && typeof error.message === 'string') {
message = error.message;
}
if ('code' in error && typeof error.code === 'string') {
code = error.code;
}
} else {
message = String(error);
}
if (code === 'EPERM' || code === 'EACCES') {
log('debug', `Permission denied scanning directory: ${currentDirAbs}`);
} else {
log(
'warning',
`Error scanning directory ${currentDirAbs}: ${message} (Code: ${code ?? 'N/A'})`,
);
}
}
}
// Define schema as ZodObject
const discoverProjsSchema = z.object({
workspaceRoot: z.string().describe('The absolute path of the workspace root to scan within.'),
scanPath: z
.string()
.optional()
.describe('Optional: Path relative to workspace root to scan. Defaults to workspace root.'),
maxDepth: z
.number()
.int()
.nonnegative()
.optional()
.describe(`Optional: Maximum directory depth to scan. Defaults to ${DEFAULT_MAX_DEPTH}.`),
});
// Use z.infer for type safety
type DiscoverProjsParams = z.infer<typeof discoverProjsSchema>;
/**
* Business logic for discovering projects.
* Exported for testing purposes.
*/
export async function discover_projsLogic(
params: DiscoverProjsParams,
fileSystemExecutor: FileSystemExecutor,
): Promise<ToolResponse> {
// Apply defaults
const scanPath = params.scanPath ?? '.';
const maxDepth = params.maxDepth ?? DEFAULT_MAX_DEPTH;
const workspaceRoot = params.workspaceRoot;
const relativeScanPath = scanPath;
// Calculate and validate the absolute scan path
const requestedScanPath = path.resolve(workspaceRoot, relativeScanPath ?? '.');
let absoluteScanPath = requestedScanPath;
const normalizedWorkspaceRoot = path.normalize(workspaceRoot);
if (!path.normalize(absoluteScanPath).startsWith(normalizedWorkspaceRoot)) {
log(
'warn',
`Requested scan path '${relativeScanPath}' resolved outside workspace root '${workspaceRoot}'. Defaulting scan to workspace root.`,
);
absoluteScanPath = normalizedWorkspaceRoot;
}
const results = { projects: [], workspaces: [] };
log(
'info',
`Starting project discovery request: path=${absoluteScanPath}, maxDepth=${maxDepth}, workspace=${workspaceRoot}`,
);
try {
// Ensure the scan path exists and is a directory
const stats = await fileSystemExecutor.stat(absoluteScanPath);
if (!stats.isDirectory()) {
const errorMsg = `Scan path is not a directory: ${absoluteScanPath}`;
log('error', errorMsg);
// Return ToolResponse error format
return {
content: [createTextContent(errorMsg)],
isError: true,
};
}
} catch (error) {
let code;
let message = 'Unknown error accessing scan path';
// Type guards - refined
if (error instanceof Error) {
message = error.message;
// Check for code property specific to Node.js fs errors
if ('code' in error) {
code = error.code;
}
} else if (typeof error === 'object' && error !== null) {
if ('message' in error && typeof error.message === 'string') {
message = error.message;
}
if ('code' in error && typeof error.code === 'string') {
code = error.code;
}
} else {
message = String(error);
}
const errorMsg = `Failed to access scan path: ${absoluteScanPath}. Error: ${message}`;
log('error', `${errorMsg} - Code: ${code ?? 'N/A'}`);
return {
content: [createTextContent(errorMsg)],
isError: true,
};
}
// Start the recursive scan from the validated absolute path
await _findProjectsRecursive(
absoluteScanPath,
workspaceRoot,
0,
maxDepth,
results,
fileSystemExecutor,
);
log(
'info',
`Discovery finished. Found ${results.projects.length} projects and ${results.workspaces.length} workspaces.`,
);
const responseContent = [
createTextContent(
`Discovery finished. Found ${results.projects.length} projects and ${results.workspaces.length} workspaces.`,
),
];
// Sort results for consistent output
results.projects.sort();
results.workspaces.sort();
if (results.projects.length > 0) {
responseContent.push(
createTextContent(`Projects found:\n - ${results.projects.join('\n - ')}`),
);
}
if (results.workspaces.length > 0) {
responseContent.push(
createTextContent(`Workspaces found:\n - ${results.workspaces.join('\n - ')}`),
);
}
if (results.projects.length > 0 || results.workspaces.length > 0) {
responseContent.push(
createTextContent(
"Hint: Save a default with session-set-defaults { projectPath: '...' } or { workspacePath: '...' }.",
),
);
}
return {
content: responseContent,
isError: false,
};
}
export default {
name: 'discover_projs',
description:
'Scans a directory (defaults to workspace root) to find Xcode project (.xcodeproj) and workspace (.xcworkspace) files.',
schema: discoverProjsSchema.shape, // MCP SDK compatibility
annotations: {
title: 'Discover Projects',
readOnlyHint: true,
},
handler: createTypedTool(
discoverProjsSchema,
(params: DiscoverProjsParams) => {
return discover_projsLogic(params, getDefaultFileSystemExecutor());
},
getDefaultCommandExecutor,
),
};
```
--------------------------------------------------------------------------------
/src/utils/debugger/backends/lldb-cli-backend.ts:
--------------------------------------------------------------------------------
```typescript
import type { InteractiveProcess, InteractiveSpawner } from '../../execution/index.ts';
import { getDefaultInteractiveSpawner } from '../../execution/index.ts';
import type { DebuggerBackend } from './DebuggerBackend.ts';
import type { BreakpointInfo, BreakpointSpec, DebugExecutionState } from '../types.ts';
const DEFAULT_COMMAND_TIMEOUT_MS = 30_000;
const DEFAULT_STARTUP_TIMEOUT_MS = 10_000;
const LLDB_PROMPT = 'XCODEBUILDMCP_LLDB> ';
const COMMAND_SENTINEL = '__XCODEBUILDMCP_DONE__';
const COMMAND_SENTINEL_REGEX = new RegExp(`(^|\\r?\\n)${COMMAND_SENTINEL}(\\r?\\n)`);
class LldbCliBackend implements DebuggerBackend {
readonly kind = 'lldb-cli' as const;
private readonly spawner: InteractiveSpawner;
private readonly prompt = LLDB_PROMPT;
private readonly process: InteractiveProcess;
private buffer = '';
private pending: {
resolve: (output: string) => void;
reject: (error: Error) => void;
timeout: NodeJS.Timeout;
} | null = null;
private queue: Promise<unknown> = Promise.resolve();
private ready: Promise<void>;
private disposed = false;
constructor(spawner: InteractiveSpawner) {
this.spawner = spawner;
const lldbCommand = [
'xcrun',
'lldb',
'--no-lldbinit',
'-o',
`settings set prompt "${this.prompt}"`,
];
this.process = this.spawner(lldbCommand);
this.process.process.stdout?.on('data', (data: Buffer) => this.handleData(data));
this.process.process.stderr?.on('data', (data: Buffer) => this.handleData(data));
this.process.process.on('exit', (code, signal) => {
const detail = signal ? `signal ${signal}` : `code ${code ?? 'unknown'}`;
this.failPending(new Error(`LLDB process exited (${detail})`));
});
this.ready = this.initialize();
}
private async initialize(): Promise<void> {
// Prime the prompt by running a sentinel command we can parse reliably.
this.process.write(`script print("${COMMAND_SENTINEL}")\n`);
await this.waitForSentinel(DEFAULT_STARTUP_TIMEOUT_MS);
}
async waitUntilReady(): Promise<void> {
await this.ready;
}
async attach(opts: { pid: number; simulatorId: string; waitFor?: boolean }): Promise<void> {
const command = opts.waitFor
? `process attach --pid ${opts.pid} --waitfor`
: `process attach --pid ${opts.pid}`;
const output = await this.runCommand(command);
assertNoLldbError('attach', output);
}
async detach(): Promise<void> {
const output = await this.runCommand('process detach');
assertNoLldbError('detach', output);
}
async runCommand(command: string, opts?: { timeoutMs?: number }): Promise<string> {
return this.enqueue(async () => {
if (this.disposed) {
throw new Error('LLDB backend disposed');
}
await this.ready;
this.process.write(`${command}\n`);
this.process.write(`script print("${COMMAND_SENTINEL}")\n`);
const output = await this.waitForSentinel(opts?.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS);
return sanitizeOutput(output, this.prompt).trimEnd();
});
}
async resume(): Promise<void> {
return this.enqueue(async () => {
if (this.disposed) {
throw new Error('LLDB backend disposed');
}
await this.ready;
this.process.write('process continue\n');
});
}
async addBreakpoint(
spec: BreakpointSpec,
opts?: { condition?: string },
): Promise<BreakpointInfo> {
const command =
spec.kind === 'file-line'
? `breakpoint set --file "${spec.file}" --line ${spec.line}`
: `breakpoint set --name "${spec.name}"`;
const output = await this.runCommand(command);
assertNoLldbError('breakpoint', output);
const match = output.match(/Breakpoint\s+(\d+):/);
if (!match) {
throw new Error(`Unable to parse breakpoint id from output: ${output}`);
}
const id = Number(match[1]);
if (opts?.condition) {
const condition = formatConditionForLldb(opts.condition);
const modifyOutput = await this.runCommand(`breakpoint modify -c ${condition} ${id}`);
assertNoLldbError('breakpoint modify', modifyOutput);
}
return {
id,
spec,
rawOutput: output,
};
}
async removeBreakpoint(id: number): Promise<string> {
const output = await this.runCommand(`breakpoint delete ${id}`);
assertNoLldbError('breakpoint delete', output);
return output;
}
async getStack(opts?: { threadIndex?: number; maxFrames?: number }): Promise<string> {
let command = 'thread backtrace';
if (typeof opts?.maxFrames === 'number') {
command += ` -c ${opts.maxFrames}`;
}
if (typeof opts?.threadIndex === 'number') {
command += ` ${opts.threadIndex}`;
}
return this.runCommand(command);
}
async getVariables(opts?: { frameIndex?: number }): Promise<string> {
if (typeof opts?.frameIndex === 'number') {
await this.runCommand(`frame select ${opts.frameIndex}`);
}
return this.runCommand('frame variable');
}
async getExecutionState(opts?: { timeoutMs?: number }): Promise<DebugExecutionState> {
try {
const output = await this.runCommand('process status', {
timeoutMs: opts?.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS,
});
const normalized = output.toLowerCase();
if (/no process|exited|terminated/.test(normalized)) {
return { status: 'terminated', description: output.trim() };
}
if (/\bstopped\b/.test(normalized)) {
return {
status: 'stopped',
reason: parseStopReason(output),
description: output.trim(),
};
}
if (/\brunning\b/.test(normalized)) {
return { status: 'running', description: output.trim() };
}
if (/error:/.test(normalized)) {
return { status: 'unknown', description: output.trim() };
}
return { status: 'unknown', description: output.trim() };
} catch (error) {
return {
status: 'unknown',
description: error instanceof Error ? error.message : String(error),
};
}
}
async dispose(): Promise<void> {
if (this.disposed) return;
this.disposed = true;
this.failPending(new Error('LLDB backend disposed'));
this.process.dispose();
}
private enqueue<T>(work: () => Promise<T>): Promise<T> {
const next = this.queue.then(work, work) as Promise<T>;
this.queue = next.then(
() => undefined,
() => undefined,
);
return next;
}
private handleData(data: Buffer): void {
this.buffer += data.toString('utf8');
this.checkPending();
}
private waitForSentinel(timeoutMs: number): Promise<string> {
if (this.pending) {
return Promise.reject(new Error('LLDB command already pending'));
}
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.pending = null;
reject(new Error(`LLDB command timed out after ${timeoutMs}ms`));
}, timeoutMs);
this.pending = { resolve, reject, timeout };
this.checkPending();
});
}
private checkPending(): void {
if (!this.pending) return;
const sentinelMatch = this.buffer.match(COMMAND_SENTINEL_REGEX);
const sentinelIndex = sentinelMatch?.index;
const sentinelLength = sentinelMatch?.[0].length;
if (sentinelIndex == null || sentinelLength == null) return;
const output = this.buffer.slice(0, sentinelIndex);
const remainderStart = sentinelIndex + sentinelLength;
const promptIndex = this.buffer.indexOf(this.prompt, remainderStart);
if (promptIndex !== -1) {
this.buffer = this.buffer.slice(promptIndex + this.prompt.length);
} else {
this.buffer = this.buffer.slice(remainderStart);
}
const { resolve, timeout } = this.pending;
this.pending = null;
clearTimeout(timeout);
resolve(output);
}
private failPending(error: Error): void {
if (!this.pending) return;
const { reject, timeout } = this.pending;
this.pending = null;
clearTimeout(timeout);
reject(error);
}
}
function assertNoLldbError(context: string, output: string): void {
if (/error:/i.test(output)) {
throw new Error(`LLDB ${context} failed: ${output.trim()}`);
}
}
function sanitizeOutput(output: string, prompt: string): string {
const lines = output.split(/\r?\n/);
const filtered = lines.filter((line) => {
if (!line) return false;
if (line.startsWith(prompt)) return false;
if (line.includes(`script print("${COMMAND_SENTINEL}")`)) return false;
if (line.includes(COMMAND_SENTINEL)) return false;
return true;
});
return filtered.join('\n');
}
function formatConditionForLldb(condition: string): string {
const escaped = condition.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
return `"${escaped}"`;
}
function parseStopReason(output: string): string | undefined {
const match = output.match(/stop reason\s*=\s*(.+)/i);
if (!match) return undefined;
return match[1]?.trim() || undefined;
}
export async function createLldbCliBackend(
spawner: InteractiveSpawner = getDefaultInteractiveSpawner(),
): Promise<DebuggerBackend> {
const backend = new LldbCliBackend(spawner);
try {
await backend.waitUntilReady();
} catch (error) {
try {
await backend.dispose();
} catch {
// Best-effort cleanup; keep original error.
}
throw error;
}
return backend;
}
```