This is page 2 of 14. Use http://codebase.md/cameroncooke/xcodebuildmcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .axe-version ├── .claude │ └── agents │ └── xcodebuild-mcp-qa-tester.md ├── .cursor │ ├── BUGBOT.md │ └── environment.json ├── .cursorrules ├── .github │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ └── workflows │ ├── ci.yml │ ├── claude-code-review.yml │ ├── claude-dispatch.yml │ ├── claude.yml │ ├── droid-code-review.yml │ ├── README.md │ ├── release.yml │ └── sentry.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── mcp.json │ ├── settings.json │ └── tasks.json ├── AGENTS.md ├── banner.png ├── build-plugins │ ├── plugin-discovery.js │ ├── plugin-discovery.ts │ └── tsconfig.json ├── CHANGELOG.md ├── CLAUDE.md ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── docs │ ├── ARCHITECTURE.md │ ├── CODE_QUALITY.md │ ├── CONTRIBUTING.md │ ├── ESLINT_TYPE_SAFETY.md │ ├── MANUAL_TESTING.md │ ├── NODEJS_2025.md │ ├── PLUGIN_DEVELOPMENT.md │ ├── RELEASE_PROCESS.md │ ├── RELOADEROO_FOR_XCODEBUILDMCP.md │ ├── RELOADEROO_XCODEBUILDMCP_PRIMER.md │ ├── RELOADEROO.md │ ├── session_management_plan.md │ ├── session-aware-migration-todo.md │ ├── TEST_RUNNER_ENV_IMPLEMENTATION_PLAN.md │ ├── TESTING.md │ └── TOOLS.md ├── eslint.config.js ├── example_projects │ ├── .vscode │ │ └── launch.json │ ├── iOS │ │ ├── .cursor │ │ │ └── rules │ │ │ └── errors.mdc │ │ ├── .vscode │ │ │ └── settings.json │ │ ├── Makefile │ │ ├── MCPTest │ │ │ ├── Assets.xcassets │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── ContentView.swift │ │ │ ├── MCPTestApp.swift │ │ │ └── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ │ ├── MCPTest.xcodeproj │ │ │ ├── project.pbxproj │ │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── MCPTest.xcscheme │ │ └── MCPTestUITests │ │ └── MCPTestUITests.swift │ ├── iOS_Calculator │ │ ├── CalculatorApp │ │ │ ├── Assets.xcassets │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── CalculatorApp.swift │ │ │ └── CalculatorApp.xctestplan │ │ ├── CalculatorApp.xcodeproj │ │ │ ├── project.pbxproj │ │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── CalculatorApp.xcscheme │ │ ├── CalculatorApp.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ ├── CalculatorAppPackage │ │ │ ├── .gitignore │ │ │ ├── Package.swift │ │ │ ├── Sources │ │ │ │ └── CalculatorAppFeature │ │ │ │ ├── BackgroundEffect.swift │ │ │ │ ├── CalculatorButton.swift │ │ │ │ ├── CalculatorDisplay.swift │ │ │ │ ├── CalculatorInputHandler.swift │ │ │ │ ├── CalculatorService.swift │ │ │ │ └── ContentView.swift │ │ │ └── Tests │ │ │ └── CalculatorAppFeatureTests │ │ │ └── CalculatorServiceTests.swift │ │ ├── CalculatorAppTests │ │ │ └── CalculatorAppTests.swift │ │ └── Config │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ ├── Shared.xcconfig │ │ └── Tests.xcconfig │ ├── macOS │ │ ├── MCPTest │ │ │ ├── Assets.xcassets │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── ContentView.swift │ │ │ ├── MCPTest.entitlements │ │ │ ├── MCPTestApp.swift │ │ │ └── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ │ └── MCPTest.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ └── xcschemes │ │ └── MCPTest.xcscheme │ └── spm │ ├── .gitignore │ ├── Package.resolved │ ├── Package.swift │ ├── Sources │ │ ├── long-server │ │ │ └── main.swift │ │ ├── quick-task │ │ │ └── main.swift │ │ ├── spm │ │ │ └── main.swift │ │ └── TestLib │ │ └── TaskManager.swift │ └── Tests │ └── TestLibTests │ └── SimpleTests.swift ├── LICENSE ├── mcp-install-dark.png ├── package-lock.json ├── package.json ├── README.md ├── scripts │ ├── analysis │ │ └── tools-analysis.ts │ ├── bundle-axe.sh │ ├── check-code-patterns.js │ ├── release.sh │ ├── tools-cli.ts │ └── update-tools-docs.ts ├── server.json ├── smithery.yaml ├── src │ ├── core │ │ ├── __tests__ │ │ │ └── resources.test.ts │ │ ├── dynamic-tools.ts │ │ ├── plugin-registry.ts │ │ ├── plugin-types.ts │ │ └── resources.ts │ ├── doctor-cli.ts │ ├── index.ts │ ├── mcp │ │ ├── resources │ │ │ ├── __tests__ │ │ │ │ ├── devices.test.ts │ │ │ │ ├── doctor.test.ts │ │ │ │ └── simulators.test.ts │ │ │ ├── devices.ts │ │ │ ├── doctor.ts │ │ │ └── simulators.ts │ │ └── tools │ │ ├── device │ │ │ ├── __tests__ │ │ │ │ ├── build_device.test.ts │ │ │ │ ├── get_device_app_path.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── install_app_device.test.ts │ │ │ │ ├── launch_app_device.test.ts │ │ │ │ ├── list_devices.test.ts │ │ │ │ ├── re-exports.test.ts │ │ │ │ ├── stop_app_device.test.ts │ │ │ │ └── test_device.test.ts │ │ │ ├── build_device.ts │ │ │ ├── clean.ts │ │ │ ├── discover_projs.ts │ │ │ ├── get_app_bundle_id.ts │ │ │ ├── get_device_app_path.ts │ │ │ ├── index.ts │ │ │ ├── install_app_device.ts │ │ │ ├── launch_app_device.ts │ │ │ ├── list_devices.ts │ │ │ ├── list_schemes.ts │ │ │ ├── show_build_settings.ts │ │ │ ├── start_device_log_cap.ts │ │ │ ├── stop_app_device.ts │ │ │ ├── stop_device_log_cap.ts │ │ │ └── test_device.ts │ │ ├── discovery │ │ │ ├── __tests__ │ │ │ │ └── discover_tools.test.ts │ │ │ ├── discover_tools.ts │ │ │ └── index.ts │ │ ├── doctor │ │ │ ├── __tests__ │ │ │ │ ├── doctor.test.ts │ │ │ │ └── index.test.ts │ │ │ ├── doctor.ts │ │ │ ├── index.ts │ │ │ └── lib │ │ │ └── doctor.deps.ts │ │ ├── logging │ │ │ ├── __tests__ │ │ │ │ ├── index.test.ts │ │ │ │ ├── start_device_log_cap.test.ts │ │ │ │ ├── start_sim_log_cap.test.ts │ │ │ │ ├── stop_device_log_cap.test.ts │ │ │ │ └── stop_sim_log_cap.test.ts │ │ │ ├── index.ts │ │ │ ├── start_device_log_cap.ts │ │ │ ├── start_sim_log_cap.ts │ │ │ ├── stop_device_log_cap.ts │ │ │ └── stop_sim_log_cap.ts │ │ ├── macos │ │ │ ├── __tests__ │ │ │ │ ├── build_macos.test.ts │ │ │ │ ├── build_run_macos.test.ts │ │ │ │ ├── get_mac_app_path.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── launch_mac_app.test.ts │ │ │ │ ├── re-exports.test.ts │ │ │ │ ├── stop_mac_app.test.ts │ │ │ │ └── test_macos.test.ts │ │ │ ├── build_macos.ts │ │ │ ├── build_run_macos.ts │ │ │ ├── clean.ts │ │ │ ├── discover_projs.ts │ │ │ ├── get_mac_app_path.ts │ │ │ ├── get_mac_bundle_id.ts │ │ │ ├── index.ts │ │ │ ├── launch_mac_app.ts │ │ │ ├── list_schemes.ts │ │ │ ├── show_build_settings.ts │ │ │ ├── stop_mac_app.ts │ │ │ └── test_macos.ts │ │ ├── project-discovery │ │ │ ├── __tests__ │ │ │ │ ├── discover_projs.test.ts │ │ │ │ ├── get_app_bundle_id.test.ts │ │ │ │ ├── get_mac_bundle_id.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── list_schemes.test.ts │ │ │ │ └── show_build_settings.test.ts │ │ │ ├── discover_projs.ts │ │ │ ├── get_app_bundle_id.ts │ │ │ ├── get_mac_bundle_id.ts │ │ │ ├── index.ts │ │ │ ├── list_schemes.ts │ │ │ └── show_build_settings.ts │ │ ├── project-scaffolding │ │ │ ├── __tests__ │ │ │ │ ├── index.test.ts │ │ │ │ ├── scaffold_ios_project.test.ts │ │ │ │ └── scaffold_macos_project.test.ts │ │ │ ├── index.ts │ │ │ ├── scaffold_ios_project.ts │ │ │ └── scaffold_macos_project.ts │ │ ├── session-management │ │ │ ├── __tests__ │ │ │ │ ├── index.test.ts │ │ │ │ ├── session_clear_defaults.test.ts │ │ │ │ ├── session_set_defaults.test.ts │ │ │ │ └── session_show_defaults.test.ts │ │ │ ├── index.ts │ │ │ ├── session_clear_defaults.ts │ │ │ ├── session_set_defaults.ts │ │ │ └── session_show_defaults.ts │ │ ├── simulator │ │ │ ├── __tests__ │ │ │ │ ├── boot_sim.test.ts │ │ │ │ ├── build_run_sim.test.ts │ │ │ │ ├── build_sim.test.ts │ │ │ │ ├── get_sim_app_path.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── install_app_sim.test.ts │ │ │ │ ├── launch_app_logs_sim.test.ts │ │ │ │ ├── launch_app_sim.test.ts │ │ │ │ ├── list_sims.test.ts │ │ │ │ ├── open_sim.test.ts │ │ │ │ ├── record_sim_video.test.ts │ │ │ │ ├── screenshot.test.ts │ │ │ │ ├── stop_app_sim.test.ts │ │ │ │ └── test_sim.test.ts │ │ │ ├── boot_sim.ts │ │ │ ├── build_run_sim.ts │ │ │ ├── build_sim.ts │ │ │ ├── clean.ts │ │ │ ├── describe_ui.ts │ │ │ ├── discover_projs.ts │ │ │ ├── get_app_bundle_id.ts │ │ │ ├── get_sim_app_path.ts │ │ │ ├── index.ts │ │ │ ├── install_app_sim.ts │ │ │ ├── launch_app_logs_sim.ts │ │ │ ├── launch_app_sim.ts │ │ │ ├── list_schemes.ts │ │ │ ├── list_sims.ts │ │ │ ├── open_sim.ts │ │ │ ├── record_sim_video.ts │ │ │ ├── screenshot.ts │ │ │ ├── show_build_settings.ts │ │ │ ├── stop_app_sim.ts │ │ │ └── test_sim.ts │ │ ├── simulator-management │ │ │ ├── __tests__ │ │ │ │ ├── erase_sims.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── reset_sim_location.test.ts │ │ │ │ ├── set_sim_appearance.test.ts │ │ │ │ ├── set_sim_location.test.ts │ │ │ │ └── sim_statusbar.test.ts │ │ │ ├── boot_sim.ts │ │ │ ├── erase_sims.ts │ │ │ ├── index.ts │ │ │ ├── list_sims.ts │ │ │ ├── open_sim.ts │ │ │ ├── reset_sim_location.ts │ │ │ ├── set_sim_appearance.ts │ │ │ ├── set_sim_location.ts │ │ │ └── sim_statusbar.ts │ │ ├── swift-package │ │ │ ├── __tests__ │ │ │ │ ├── active-processes.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── swift_package_build.test.ts │ │ │ │ ├── swift_package_clean.test.ts │ │ │ │ ├── swift_package_list.test.ts │ │ │ │ ├── swift_package_run.test.ts │ │ │ │ ├── swift_package_stop.test.ts │ │ │ │ └── swift_package_test.test.ts │ │ │ ├── active-processes.ts │ │ │ ├── index.ts │ │ │ ├── swift_package_build.ts │ │ │ ├── swift_package_clean.ts │ │ │ ├── swift_package_list.ts │ │ │ ├── swift_package_run.ts │ │ │ ├── swift_package_stop.ts │ │ │ └── swift_package_test.ts │ │ ├── ui-testing │ │ │ ├── __tests__ │ │ │ │ ├── button.test.ts │ │ │ │ ├── describe_ui.test.ts │ │ │ │ ├── gesture.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── key_press.test.ts │ │ │ │ ├── key_sequence.test.ts │ │ │ │ ├── long_press.test.ts │ │ │ │ ├── screenshot.test.ts │ │ │ │ ├── swipe.test.ts │ │ │ │ ├── tap.test.ts │ │ │ │ ├── touch.test.ts │ │ │ │ └── type_text.test.ts │ │ │ ├── button.ts │ │ │ ├── describe_ui.ts │ │ │ ├── gesture.ts │ │ │ ├── index.ts │ │ │ ├── key_press.ts │ │ │ ├── key_sequence.ts │ │ │ ├── long_press.ts │ │ │ ├── screenshot.ts │ │ │ ├── swipe.ts │ │ │ ├── tap.ts │ │ │ ├── touch.ts │ │ │ └── type_text.ts │ │ └── utilities │ │ ├── __tests__ │ │ │ ├── clean.test.ts │ │ │ └── index.test.ts │ │ ├── clean.ts │ │ └── index.ts │ ├── server │ │ └── server.ts │ ├── test-utils │ │ └── mock-executors.ts │ ├── types │ │ └── common.ts │ └── utils │ ├── __tests__ │ │ ├── build-utils.test.ts │ │ ├── environment.test.ts │ │ ├── session-aware-tool-factory.test.ts │ │ ├── session-store.test.ts │ │ ├── simulator-utils.test.ts │ │ ├── test-runner-env-integration.test.ts │ │ └── typed-tool-factory.test.ts │ ├── axe │ │ └── index.ts │ ├── axe-helpers.ts │ ├── build │ │ └── index.ts │ ├── build-utils.ts │ ├── capabilities.ts │ ├── command.ts │ ├── CommandExecutor.ts │ ├── environment.ts │ ├── errors.ts │ ├── execution │ │ └── index.ts │ ├── FileSystemExecutor.ts │ ├── log_capture.ts │ ├── log-capture │ │ └── index.ts │ ├── logger.ts │ ├── logging │ │ └── index.ts │ ├── plugin-registry │ │ └── index.ts │ ├── responses │ │ └── index.ts │ ├── schema-helpers.ts │ ├── sentry.ts │ ├── session-store.ts │ ├── simulator-utils.ts │ ├── template │ │ └── index.ts │ ├── template-manager.ts │ ├── test │ │ └── index.ts │ ├── test-common.ts │ ├── tool-registry.ts │ ├── typed-tool-factory.ts │ ├── validation │ │ └── index.ts │ ├── validation.ts │ ├── version │ │ └── index.ts │ ├── video_capture.ts │ ├── video-capture │ │ └── index.ts │ ├── xcode.ts │ ├── xcodemake │ │ └── index.ts │ └── xcodemake.ts ├── tsconfig.json ├── tsconfig.test.json ├── tsup.config.ts └── vitest.config.ts ``` # Files -------------------------------------------------------------------------------- /src/mcp/tools/macos/stop_mac_app.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { log } from '../../../utils/logging/index.ts'; 3 | import { ToolResponse } from '../../../types/common.ts'; 4 | import type { CommandExecutor } from '../../../utils/execution/index.ts'; 5 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; 6 | import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; 7 | 8 | // Define schema as ZodObject 9 | const stopMacAppSchema = z.object({ 10 | appName: z 11 | .string() 12 | .optional() 13 | .describe('Name of the application to stop (e.g., "Calculator" or "MyApp")'), 14 | processId: z.number().optional().describe('Process ID (PID) of the application to stop'), 15 | }); 16 | 17 | // Use z.infer for type safety 18 | type StopMacAppParams = z.infer<typeof stopMacAppSchema>; 19 | 20 | export async function stop_mac_appLogic( 21 | params: StopMacAppParams, 22 | executor: CommandExecutor, 23 | ): Promise<ToolResponse> { 24 | if (!params.appName && !params.processId) { 25 | return { 26 | content: [ 27 | { 28 | type: 'text', 29 | text: 'Either appName or processId must be provided.', 30 | }, 31 | ], 32 | isError: true, 33 | }; 34 | } 35 | 36 | log( 37 | 'info', 38 | `Stopping macOS app: ${params.processId ? `PID ${params.processId}` : params.appName}`, 39 | ); 40 | 41 | try { 42 | let command: string[]; 43 | 44 | if (params.processId) { 45 | // Stop by process ID 46 | command = ['kill', String(params.processId)]; 47 | } else { 48 | // Stop by app name - use shell command with fallback for complex logic 49 | command = [ 50 | 'sh', 51 | '-c', 52 | `pkill -f "${params.appName}" || osascript -e 'tell application "${params.appName}" to quit'`, 53 | ]; 54 | } 55 | 56 | await executor(command, 'Stop macOS App'); 57 | 58 | return { 59 | content: [ 60 | { 61 | type: 'text', 62 | text: `✅ macOS app stopped successfully: ${params.processId ? `PID ${params.processId}` : params.appName}`, 63 | }, 64 | ], 65 | }; 66 | } catch (error) { 67 | const errorMessage = error instanceof Error ? error.message : String(error); 68 | log('error', `Error stopping macOS app: ${errorMessage}`); 69 | return { 70 | content: [ 71 | { 72 | type: 'text', 73 | text: `❌ Stop macOS app operation failed: ${errorMessage}`, 74 | }, 75 | ], 76 | isError: true, 77 | }; 78 | } 79 | } 80 | 81 | export default { 82 | name: 'stop_mac_app', 83 | description: 'Stops a running macOS application. Can stop by app name or process ID.', 84 | schema: stopMacAppSchema.shape, // MCP SDK compatibility 85 | handler: createTypedTool(stopMacAppSchema, stop_mac_appLogic, getDefaultCommandExecutor), 86 | }; 87 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/doctor/__tests__/index.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tests for doctor workflow metadata 3 | */ 4 | import { describe, it, expect } from 'vitest'; 5 | import { workflow } from '../index.ts'; 6 | 7 | describe('doctor workflow metadata', () => { 8 | describe('Workflow Structure', () => { 9 | it('should export workflow object with required properties', () => { 10 | expect(workflow).toHaveProperty('name'); 11 | expect(workflow).toHaveProperty('description'); 12 | expect(workflow).toHaveProperty('platforms'); 13 | expect(workflow).toHaveProperty('capabilities'); 14 | }); 15 | 16 | it('should have correct workflow name', () => { 17 | expect(workflow.name).toBe('System Doctor'); 18 | }); 19 | 20 | it('should have correct description', () => { 21 | expect(workflow.description).toBe( 22 | 'Debug tools and system doctor for troubleshooting XcodeBuildMCP server, development environment, and tool availability.', 23 | ); 24 | }); 25 | 26 | it('should have correct platforms array', () => { 27 | expect(workflow.platforms).toEqual(['system']); 28 | }); 29 | 30 | it('should have correct capabilities array', () => { 31 | expect(workflow.capabilities).toEqual([ 32 | 'doctor', 33 | 'server-diagnostics', 34 | 'troubleshooting', 35 | 'system-analysis', 36 | 'environment-validation', 37 | ]); 38 | }); 39 | }); 40 | 41 | describe('Workflow Validation', () => { 42 | it('should have valid string properties', () => { 43 | expect(typeof workflow.name).toBe('string'); 44 | expect(typeof workflow.description).toBe('string'); 45 | expect(workflow.name.length).toBeGreaterThan(0); 46 | expect(workflow.description.length).toBeGreaterThan(0); 47 | }); 48 | 49 | it('should have valid array properties', () => { 50 | expect(Array.isArray(workflow.platforms)).toBe(true); 51 | expect(Array.isArray(workflow.capabilities)).toBe(true); 52 | 53 | expect(workflow.platforms.length).toBeGreaterThan(0); 54 | expect(workflow.capabilities.length).toBeGreaterThan(0); 55 | }); 56 | 57 | it('should contain expected platform values', () => { 58 | expect(workflow.platforms).toContain('system'); 59 | }); 60 | 61 | it('should contain expected capability values', () => { 62 | expect(workflow.capabilities).toContain('doctor'); 63 | expect(workflow.capabilities).toContain('server-diagnostics'); 64 | expect(workflow.capabilities).toContain('troubleshooting'); 65 | expect(workflow.capabilities).toContain('system-analysis'); 66 | expect(workflow.capabilities).toContain('environment-validation'); 67 | }); 68 | 69 | it('should not have targets or projectTypes properties', () => { 70 | expect(workflow).not.toHaveProperty('targets'); 71 | expect(workflow).not.toHaveProperty('projectTypes'); 72 | }); 73 | }); 74 | }); 75 | ``` -------------------------------------------------------------------------------- /src/utils/environment.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Environment Detection Utilities 3 | * 4 | * Provides abstraction for environment detection to enable testability 5 | * while maintaining production functionality. 6 | */ 7 | 8 | import { execSync } from 'child_process'; 9 | import { log } from './logger.ts'; 10 | 11 | /** 12 | * Interface for environment detection abstraction 13 | */ 14 | export interface EnvironmentDetector { 15 | /** 16 | * Detects if the MCP server is running under Claude Code 17 | * @returns true if Claude Code is detected, false otherwise 18 | */ 19 | isRunningUnderClaudeCode(): boolean; 20 | } 21 | 22 | /** 23 | * Production implementation of environment detection 24 | */ 25 | export class ProductionEnvironmentDetector implements EnvironmentDetector { 26 | isRunningUnderClaudeCode(): boolean { 27 | // Disable Claude Code detection during tests for environment-agnostic testing 28 | if (process.env.NODE_ENV === 'test' || process.env.VITEST === 'true') { 29 | return false; 30 | } 31 | 32 | // Method 1: Check for Claude Code environment variables 33 | if (process.env.CLAUDECODE === '1' || process.env.CLAUDE_CODE_ENTRYPOINT === 'cli') { 34 | return true; 35 | } 36 | 37 | // Method 2: Check parent process name 38 | try { 39 | const parentPid = process.ppid; 40 | if (parentPid) { 41 | const parentCommand = execSync(`ps -o command= -p ${parentPid}`, { 42 | encoding: 'utf8', 43 | timeout: 1000, 44 | }).trim(); 45 | if (parentCommand.includes('claude')) { 46 | return true; 47 | } 48 | } 49 | } catch (error) { 50 | // If process detection fails, fall back to environment variables only 51 | log('debug', `Failed to detect parent process: ${error}`); 52 | } 53 | 54 | return false; 55 | } 56 | } 57 | 58 | /** 59 | * Default environment detector instance for production use 60 | */ 61 | export const defaultEnvironmentDetector = new ProductionEnvironmentDetector(); 62 | 63 | /** 64 | * Gets the default environment detector for production use 65 | */ 66 | export function getDefaultEnvironmentDetector(): EnvironmentDetector { 67 | return defaultEnvironmentDetector; 68 | } 69 | 70 | /** 71 | * Normalizes a set of user-provided environment variables by ensuring they are 72 | * prefixed with TEST_RUNNER_. Variables already prefixed are preserved. 73 | * 74 | * Example: 75 | * normalizeTestRunnerEnv({ FOO: '1', TEST_RUNNER_BAR: '2' }) 76 | * => { TEST_RUNNER_FOO: '1', TEST_RUNNER_BAR: '2' } 77 | */ 78 | export function normalizeTestRunnerEnv(vars: Record<string, string>): Record<string, string> { 79 | const normalized: Record<string, string> = {}; 80 | for (const [key, value] of Object.entries(vars ?? {})) { 81 | if (value == null) continue; 82 | const prefixedKey = key.startsWith('TEST_RUNNER_') ? key : `TEST_RUNNER_${key}`; 83 | normalized[prefixedKey] = value; 84 | } 85 | return normalized; 86 | } 87 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/device/stop_app_device.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Device Workspace Plugin: Stop App Device 3 | * 4 | * Stops an app running on a physical Apple device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). 5 | * Requires deviceId and processId. 6 | */ 7 | 8 | import { z } from 'zod'; 9 | import { ToolResponse } from '../../../types/common.ts'; 10 | import { log } from '../../../utils/logging/index.ts'; 11 | import type { CommandExecutor } from '../../../utils/execution/index.ts'; 12 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; 13 | import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; 14 | 15 | // Define schema as ZodObject 16 | const stopAppDeviceSchema = z.object({ 17 | deviceId: z.string().describe('UDID of the device (obtained from list_devices)'), 18 | processId: z.number().describe('Process ID (PID) of the app to stop'), 19 | }); 20 | 21 | // Use z.infer for type safety 22 | type StopAppDeviceParams = z.infer<typeof stopAppDeviceSchema>; 23 | 24 | export async function stop_app_deviceLogic( 25 | params: StopAppDeviceParams, 26 | executor: CommandExecutor, 27 | ): Promise<ToolResponse> { 28 | const { deviceId, processId } = params; 29 | 30 | log('info', `Stopping app with PID ${processId} on device ${deviceId}`); 31 | 32 | try { 33 | const result = await executor( 34 | [ 35 | 'xcrun', 36 | 'devicectl', 37 | 'device', 38 | 'process', 39 | 'terminate', 40 | '--device', 41 | deviceId, 42 | '--pid', 43 | processId.toString(), 44 | ], 45 | 'Stop app on device', 46 | true, // useShell 47 | undefined, // env 48 | ); 49 | 50 | if (!result.success) { 51 | return { 52 | content: [ 53 | { 54 | type: 'text', 55 | text: `Failed to stop app: ${result.error}`, 56 | }, 57 | ], 58 | isError: true, 59 | }; 60 | } 61 | 62 | return { 63 | content: [ 64 | { 65 | type: 'text', 66 | text: `✅ App stopped successfully\n\n${result.output}`, 67 | }, 68 | ], 69 | }; 70 | } catch (error) { 71 | const errorMessage = error instanceof Error ? error.message : String(error); 72 | log('error', `Error stopping app on device: ${errorMessage}`); 73 | return { 74 | content: [ 75 | { 76 | type: 'text', 77 | text: `Failed to stop app on device: ${errorMessage}`, 78 | }, 79 | ], 80 | isError: true, 81 | }; 82 | } 83 | } 84 | 85 | export default { 86 | name: 'stop_app_device', 87 | description: 'Stops a running app on a connected device.', 88 | schema: stopAppDeviceSchema.omit({ deviceId: true } as const).shape, 89 | handler: createSessionAwareTool<StopAppDeviceParams>({ 90 | internalSchema: stopAppDeviceSchema as unknown as z.ZodType<StopAppDeviceParams>, 91 | logicFunction: stop_app_deviceLogic, 92 | getExecutor: getDefaultCommandExecutor, 93 | requirements: [{ allOf: ['deviceId'], message: 'deviceId is required' }], 94 | }), 95 | }; 96 | ``` -------------------------------------------------------------------------------- /.cursor/BUGBOT.md: -------------------------------------------------------------------------------- ```markdown 1 | # Bugbot Review Guide for XcodeBuildMCP 2 | 3 | ## Project Snapshot 4 | 5 | XcodeBuildMCP is an MCP server exposing Xcode / Swift workflows as **tools** and **resources**. 6 | Stack: TypeScript · Node.js · plugin-based auto-discovery (`src/mcp/tools`, `src/mcp/resources`). 7 | 8 | For full details see [README.md](README.md) and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). 9 | 10 | --- 11 | 12 | ## 1. Security Checklist — Critical 13 | 14 | * No hard-coded secrets, tokens or DSNs. 15 | * All shell commands must flow through `CommandExecutor` with validated arguments (no direct `child_process` calls). 16 | * Paths must be sanitised via helpers in `src/utils/validation.ts`. 17 | * Sentry breadcrumbs / logs must **NOT** include user PII. 18 | 19 | --- 20 | 21 | ## 2. Architecture Checklist — Critical 22 | 23 | | Rule | Quick diff heuristic | 24 | |------|----------------------| 25 | | Dependency injection only | New `child_process` \| `fs` import ⇒ **critical** | 26 | | Handler / Logic split | `handler` > 20 LOC or contains branching ⇒ **critical** | 27 | | Plugin auto-registration | Manual `registerTool(...)` / `registerResource(...)` ⇒ **critical** | 28 | 29 | Positive pattern skeleton: 30 | 31 | ```ts 32 | // src/mcp/tools/foo-bar.ts 33 | export async function fooBarLogic( 34 | params: FooBarParams, 35 | exec: CommandExecutor = getDefaultCommandExecutor(), 36 | fs: FileSystemExecutor = getDefaultFileSystemExecutor(), 37 | ) { 38 | // ... 39 | } 40 | 41 | export const handler = (p: FooBarParams) => fooBarLogic(p); 42 | ``` 43 | 44 | --- 45 | 46 | ## 3. Testing Checklist 47 | 48 | * **Ban on Vitest mocking** (`vi.mock`, `vi.fn`, `vi.spyOn`, `.mock*`) ⇒ critical. Use `createMockExecutor` / `createMockFileSystemExecutor`. 49 | * Each tool must have tests covering happy-path **and** at least one failure path. 50 | * Avoid the `any` type unless justified with an inline comment. 51 | 52 | --- 53 | 54 | ## 4. Documentation Checklist 55 | 56 | * `docs/TOOLS.md` must exactly mirror the structure of `src/mcp/tools/**` (exclude `__tests__` and `*-shared`). 57 | *Diff heuristic*: if a PR adds/removes a tool but does **not** change `docs/TOOLS.md` ⇒ **warning**. 58 | * Update public docs when CLI parameters or tool names change. 59 | 60 | --- 61 | 62 | ## 5. Common Anti-Patterns (and fixes) 63 | 64 | | Anti-pattern | Preferred approach | 65 | |--------------|--------------------| 66 | | Complex logic in `handler` | Move to `*Logic` function | 67 | | Re-implementing logging | Use `src/utils/logger.ts` | 68 | | Direct `fs` / `child_process` usage | Inject `FileSystemExecutor` / `CommandExecutor` | 69 | | Chained re-exports | Export directly from source | 70 | 71 | --- 72 | 73 | ### How Bugbot Can Verify Rules 74 | 75 | 1. **Mocking violations**: search `*.test.ts` for `vi.` → critical. 76 | 2. **DI compliance**: search for direct `child_process` / `fs` imports outside executors. 77 | 3. **Docs accuracy**: compare `docs/TOOLS.md` against `src/mcp/tools/**`. 78 | 4. **Style**: ensure ESLint and Prettier pass (`npm run lint`, `npm run format:check`). 79 | 80 | --- 81 | 82 | Happy reviewing 🚀 ``` -------------------------------------------------------------------------------- /src/mcp/tools/session-management/__tests__/session_clear_defaults.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2 | import { sessionStore } from '../../../../utils/session-store.ts'; 3 | import plugin, { sessionClearDefaultsLogic } from '../session_clear_defaults.ts'; 4 | 5 | describe('session-clear-defaults tool', () => { 6 | beforeEach(() => { 7 | sessionStore.clear(); 8 | sessionStore.setDefaults({ 9 | scheme: 'MyScheme', 10 | projectPath: '/path/to/proj.xcodeproj', 11 | simulatorName: 'iPhone 16', 12 | deviceId: 'DEVICE-123', 13 | useLatestOS: true, 14 | arch: 'arm64', 15 | }); 16 | }); 17 | 18 | afterEach(() => { 19 | sessionStore.clear(); 20 | }); 21 | 22 | describe('Export Field Validation (Literal)', () => { 23 | it('should have correct name', () => { 24 | expect(plugin.name).toBe('session-clear-defaults'); 25 | }); 26 | 27 | it('should have correct description', () => { 28 | expect(plugin.description).toBe('Clear selected or all session defaults.'); 29 | }); 30 | 31 | it('should have handler function', () => { 32 | expect(typeof plugin.handler).toBe('function'); 33 | }); 34 | 35 | it('should have schema object', () => { 36 | expect(plugin.schema).toBeDefined(); 37 | expect(typeof plugin.schema).toBe('object'); 38 | }); 39 | }); 40 | 41 | describe('Handler Behavior', () => { 42 | it('should clear specific keys when provided', async () => { 43 | const result = await sessionClearDefaultsLogic({ keys: ['scheme', 'deviceId'] }); 44 | expect(result.isError).toBe(false); 45 | expect(result.content[0].text).toContain('Session defaults cleared'); 46 | 47 | const current = sessionStore.getAll(); 48 | expect(current.scheme).toBeUndefined(); 49 | expect(current.deviceId).toBeUndefined(); 50 | expect(current.projectPath).toBe('/path/to/proj.xcodeproj'); 51 | expect(current.simulatorName).toBe('iPhone 16'); 52 | expect(current.useLatestOS).toBe(true); 53 | expect(current.arch).toBe('arm64'); 54 | }); 55 | 56 | it('should clear all when all=true', async () => { 57 | const result = await sessionClearDefaultsLogic({ all: true }); 58 | expect(result.isError).toBe(false); 59 | expect(result.content[0].text).toBe('Session defaults cleared'); 60 | 61 | const current = sessionStore.getAll(); 62 | expect(Object.keys(current).length).toBe(0); 63 | }); 64 | 65 | it('should clear all when no params provided', async () => { 66 | const result = await sessionClearDefaultsLogic({}); 67 | expect(result.isError).toBe(false); 68 | const current = sessionStore.getAll(); 69 | expect(Object.keys(current).length).toBe(0); 70 | }); 71 | 72 | it('should validate keys enum', async () => { 73 | const result = (await plugin.handler({ keys: ['invalid' as any] })) as any; 74 | expect(result.isError).toBe(true); 75 | expect(result.content[0].text).toContain('Parameter validation failed'); 76 | expect(result.content[0].text).toContain('keys'); 77 | }); 78 | }); 79 | }); 80 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/device/install_app_device.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Device Workspace Plugin: Install App Device 3 | * 4 | * Installs an app on a physical Apple device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). 5 | * Requires deviceId and appPath. 6 | */ 7 | 8 | import { z } from 'zod'; 9 | import { ToolResponse } from '../../../types/common.ts'; 10 | import { log } from '../../../utils/logging/index.ts'; 11 | import type { CommandExecutor } from '../../../utils/execution/index.ts'; 12 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; 13 | import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; 14 | 15 | // Define schema as ZodObject 16 | const installAppDeviceSchema = z.object({ 17 | deviceId: z 18 | .string() 19 | .min(1, 'Device ID cannot be empty') 20 | .describe('UDID of the device (obtained from list_devices)'), 21 | appPath: z 22 | .string() 23 | .describe('Path to the .app bundle to install (full path to the .app directory)'), 24 | }); 25 | 26 | // Use z.infer for type safety 27 | type InstallAppDeviceParams = z.infer<typeof installAppDeviceSchema>; 28 | 29 | /** 30 | * Business logic for installing an app on a physical Apple device 31 | */ 32 | export async function install_app_deviceLogic( 33 | params: InstallAppDeviceParams, 34 | executor: CommandExecutor, 35 | ): Promise<ToolResponse> { 36 | const { deviceId, appPath } = params; 37 | 38 | log('info', `Installing app on device ${deviceId}`); 39 | 40 | try { 41 | const result = await executor( 42 | ['xcrun', 'devicectl', 'device', 'install', 'app', '--device', deviceId, appPath], 43 | 'Install app on device', 44 | true, // useShell 45 | undefined, // env 46 | ); 47 | 48 | if (!result.success) { 49 | return { 50 | content: [ 51 | { 52 | type: 'text', 53 | text: `Failed to install app: ${result.error}`, 54 | }, 55 | ], 56 | isError: true, 57 | }; 58 | } 59 | 60 | return { 61 | content: [ 62 | { 63 | type: 'text', 64 | text: `✅ App installed successfully on device ${deviceId}\n\n${result.output}`, 65 | }, 66 | ], 67 | }; 68 | } catch (error) { 69 | const errorMessage = error instanceof Error ? error.message : String(error); 70 | log('error', `Error installing app on device: ${errorMessage}`); 71 | return { 72 | content: [ 73 | { 74 | type: 'text', 75 | text: `Failed to install app on device: ${errorMessage}`, 76 | }, 77 | ], 78 | isError: true, 79 | }; 80 | } 81 | } 82 | 83 | export default { 84 | name: 'install_app_device', 85 | description: 'Installs an app on a connected device.', 86 | schema: installAppDeviceSchema.omit({ deviceId: true } as const).shape, 87 | handler: createSessionAwareTool<InstallAppDeviceParams>({ 88 | internalSchema: installAppDeviceSchema as unknown as z.ZodType<InstallAppDeviceParams>, 89 | logicFunction: install_app_deviceLogic, 90 | getExecutor: getDefaultCommandExecutor, 91 | requirements: [{ allOf: ['deviceId'], message: 'deviceId is required' }], 92 | }), 93 | }; 94 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/project-discovery/__tests__/index.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tests for project-discovery workflow metadata 3 | */ 4 | import { describe, it, expect } from 'vitest'; 5 | import { workflow } from '../index.ts'; 6 | 7 | describe('project-discovery workflow metadata', () => { 8 | describe('Workflow Structure', () => { 9 | it('should export workflow object with required properties', () => { 10 | expect(workflow).toHaveProperty('name'); 11 | expect(workflow).toHaveProperty('description'); 12 | expect(workflow).toHaveProperty('platforms'); 13 | expect(workflow).toHaveProperty('capabilities'); 14 | }); 15 | 16 | it('should have correct workflow name', () => { 17 | expect(workflow.name).toBe('Project Discovery'); 18 | }); 19 | 20 | it('should have correct description', () => { 21 | expect(workflow.description).toBe( 22 | 'Discover and examine Xcode projects, workspaces, and Swift packages. Analyze project structure, schemes, build settings, and bundle information.', 23 | ); 24 | }); 25 | 26 | it('should have correct platforms array', () => { 27 | expect(workflow.platforms).toEqual(['iOS', 'macOS', 'watchOS', 'tvOS', 'visionOS']); 28 | }); 29 | 30 | it('should have correct capabilities array', () => { 31 | expect(workflow.capabilities).toEqual([ 32 | 'project-analysis', 33 | 'scheme-discovery', 34 | 'build-settings', 35 | 'bundle-inspection', 36 | ]); 37 | }); 38 | }); 39 | 40 | describe('Workflow Validation', () => { 41 | it('should have valid string properties', () => { 42 | expect(typeof workflow.name).toBe('string'); 43 | expect(typeof workflow.description).toBe('string'); 44 | expect(workflow.name.length).toBeGreaterThan(0); 45 | expect(workflow.description.length).toBeGreaterThan(0); 46 | }); 47 | 48 | it('should have valid array properties', () => { 49 | expect(Array.isArray(workflow.platforms)).toBe(true); 50 | expect(Array.isArray(workflow.capabilities)).toBe(true); 51 | 52 | expect(workflow.platforms.length).toBeGreaterThan(0); 53 | expect(workflow.capabilities.length).toBeGreaterThan(0); 54 | }); 55 | 56 | it('should contain expected platform values', () => { 57 | expect(workflow.platforms).toContain('iOS'); 58 | expect(workflow.platforms).toContain('macOS'); 59 | expect(workflow.platforms).toContain('watchOS'); 60 | expect(workflow.platforms).toContain('tvOS'); 61 | expect(workflow.platforms).toContain('visionOS'); 62 | }); 63 | 64 | it('should contain expected capability values', () => { 65 | expect(workflow.capabilities).toContain('project-analysis'); 66 | expect(workflow.capabilities).toContain('scheme-discovery'); 67 | expect(workflow.capabilities).toContain('build-settings'); 68 | expect(workflow.capabilities).toContain('bundle-inspection'); 69 | }); 70 | 71 | it('should not have targets or projectTypes properties', () => { 72 | expect(workflow).not.toHaveProperty('targets'); 73 | expect(workflow).not.toHaveProperty('projectTypes'); 74 | }); 75 | }); 76 | }); 77 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator/launch_app_logs_sim.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { ToolResponse, createTextContent } from '../../../types/common.ts'; 3 | import { log } from '../../../utils/logging/index.ts'; 4 | import { startLogCapture } from '../../../utils/log-capture/index.ts'; 5 | import type { CommandExecutor } from '../../../utils/execution/index.ts'; 6 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; 7 | import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; 8 | 9 | export type LogCaptureFunction = ( 10 | params: { 11 | simulatorUuid: string; 12 | bundleId: string; 13 | captureConsole?: boolean; 14 | args?: string[]; 15 | }, 16 | executor: CommandExecutor, 17 | ) => Promise<{ sessionId: string; logFilePath: string; processes: unknown[]; error?: string }>; 18 | 19 | const launchAppLogsSimSchemaObject = z.object({ 20 | simulatorId: z.string().describe('UUID of the simulator to target'), 21 | bundleId: z 22 | .string() 23 | .describe("Bundle identifier of the app to launch (e.g., 'com.example.MyApp')"), 24 | args: z.array(z.string()).optional().describe('Additional arguments to pass to the app'), 25 | }); 26 | 27 | type LaunchAppLogsSimParams = z.infer<typeof launchAppLogsSimSchemaObject>; 28 | 29 | const publicSchemaObject = launchAppLogsSimSchemaObject.omit({ 30 | simulatorId: true, 31 | } as const); 32 | 33 | export async function launch_app_logs_simLogic( 34 | params: LaunchAppLogsSimParams, 35 | executor: CommandExecutor = getDefaultCommandExecutor(), 36 | logCaptureFunction: LogCaptureFunction = startLogCapture, 37 | ): Promise<ToolResponse> { 38 | log('info', `Starting app launch with logs for simulator ${params.simulatorId}`); 39 | 40 | const captureParams = { 41 | simulatorUuid: params.simulatorId, 42 | bundleId: params.bundleId, 43 | captureConsole: true, 44 | ...(params.args && params.args.length > 0 ? { args: params.args } : {}), 45 | } as const; 46 | 47 | const { sessionId, error } = await logCaptureFunction(captureParams, executor); 48 | if (error) { 49 | return { 50 | content: [createTextContent(`App was launched but log capture failed: ${error}`)], 51 | isError: true, 52 | }; 53 | } 54 | 55 | return { 56 | content: [ 57 | createTextContent( 58 | `App launched successfully in simulator ${params.simulatorId} with log capture enabled.\n\nLog capture session ID: ${sessionId}\n\nNext Steps:\n1. Interact with your app in the simulator.\n2. Use 'stop_and_get_simulator_log({ logSessionId: "${sessionId}" })' to stop capture and retrieve logs.`, 59 | ), 60 | ], 61 | isError: false, 62 | }; 63 | } 64 | 65 | export default { 66 | name: 'launch_app_logs_sim', 67 | description: 'Launches an app in an iOS simulator and captures its logs.', 68 | schema: publicSchemaObject.shape, 69 | handler: createSessionAwareTool<LaunchAppLogsSimParams>({ 70 | internalSchema: launchAppLogsSimSchemaObject, 71 | logicFunction: launch_app_logs_simLogic, 72 | getExecutor: getDefaultCommandExecutor, 73 | requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }], 74 | }), 75 | }; 76 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/macos/launch_mac_app.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * macOS Workspace Plugin: Launch macOS App 3 | * 4 | * Launches a macOS application using the 'open' command. 5 | * IMPORTANT: You MUST provide the appPath parameter. 6 | */ 7 | 8 | import { z } from 'zod'; 9 | import { log } from '../../../utils/logging/index.ts'; 10 | import { validateFileExists } from '../../../utils/validation/index.ts'; 11 | import { ToolResponse } from '../../../types/common.ts'; 12 | import type { CommandExecutor, FileSystemExecutor } from '../../../utils/execution/index.ts'; 13 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; 14 | import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; 15 | 16 | // Define schema as ZodObject 17 | const launchMacAppSchema = z.object({ 18 | appPath: z 19 | .string() 20 | .describe('Path to the macOS .app bundle to launch (full path to the .app directory)'), 21 | args: z.array(z.string()).optional().describe('Additional arguments to pass to the app'), 22 | }); 23 | 24 | // Use z.infer for type safety 25 | type LaunchMacAppParams = z.infer<typeof launchMacAppSchema>; 26 | 27 | export async function launch_mac_appLogic( 28 | params: LaunchMacAppParams, 29 | executor: CommandExecutor, 30 | fileSystem?: FileSystemExecutor, 31 | ): Promise<ToolResponse> { 32 | // Validate that the app file exists 33 | const fileExistsValidation = validateFileExists(params.appPath, fileSystem); 34 | if (!fileExistsValidation.isValid) { 35 | return fileExistsValidation.errorResponse!; 36 | } 37 | 38 | log('info', `Starting launch macOS app request for ${params.appPath}`); 39 | 40 | try { 41 | // Construct the command as string array for CommandExecutor 42 | const command = ['open', params.appPath]; 43 | 44 | // Add any additional arguments if provided 45 | if (params.args && Array.isArray(params.args) && params.args.length > 0) { 46 | command.push('--args', ...params.args); 47 | } 48 | 49 | // Execute the command using CommandExecutor 50 | await executor(command, 'Launch macOS App'); 51 | 52 | // Return success response 53 | return { 54 | content: [ 55 | { 56 | type: 'text', 57 | text: `✅ macOS app launched successfully: ${params.appPath}`, 58 | }, 59 | ], 60 | }; 61 | } catch (error) { 62 | // Handle errors 63 | const errorMessage = error instanceof Error ? error.message : String(error); 64 | log('error', `Error during launch macOS app operation: ${errorMessage}`); 65 | return { 66 | content: [ 67 | { 68 | type: 'text', 69 | text: `❌ Launch macOS app operation failed: ${errorMessage}`, 70 | }, 71 | ], 72 | isError: true, 73 | }; 74 | } 75 | } 76 | 77 | export default { 78 | name: 'launch_mac_app', 79 | description: 80 | "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.", 81 | schema: launchMacAppSchema.shape, // MCP SDK compatibility 82 | handler: createTypedTool(launchMacAppSchema, launch_mac_appLogic, getDefaultCommandExecutor), 83 | }; 84 | ``` -------------------------------------------------------------------------------- /src/types/common.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Common type definitions used across the server 3 | * 4 | * This module provides core type definitions and interfaces used throughout the codebase. 5 | * It establishes a consistent type system for platform identification, tool responses, 6 | * and other shared concepts. 7 | * 8 | * Responsibilities: 9 | * - Defining the XcodePlatform enum for platform identification 10 | * - Establishing the ToolResponse interface for standardized tool outputs 11 | * - Providing ToolResponseContent types for different response formats 12 | * - Supporting error handling with standardized error response types 13 | */ 14 | 15 | /** 16 | * Enum representing Xcode build platforms. 17 | */ 18 | export enum XcodePlatform { 19 | macOS = 'macOS', 20 | iOS = 'iOS', 21 | iOSSimulator = 'iOS Simulator', 22 | watchOS = 'watchOS', 23 | watchOSSimulator = 'watchOS Simulator', 24 | tvOS = 'tvOS', 25 | tvOSSimulator = 'tvOS Simulator', 26 | visionOS = 'visionOS', 27 | visionOSSimulator = 'visionOS Simulator', 28 | } 29 | 30 | /** 31 | * ToolResponse - Standard response format for tools 32 | * Compatible with MCP CallToolResult interface from the SDK 33 | */ 34 | export interface ToolResponse { 35 | content: ToolResponseContent[]; 36 | isError?: boolean; 37 | _meta?: Record<string, unknown>; 38 | [key: string]: unknown; // Index signature to match CallToolResult 39 | } 40 | 41 | /** 42 | * Contents that can be included in a tool response 43 | */ 44 | export type ToolResponseContent = 45 | | { 46 | type: 'text'; 47 | text: string; 48 | [key: string]: unknown; // Index signature to match ContentItem 49 | } 50 | | { 51 | type: 'image'; 52 | data: string; // Base64-encoded image data (without URI scheme prefix) 53 | mimeType: string; // e.g., 'image/png', 'image/jpeg' 54 | [key: string]: unknown; // Index signature to match ContentItem 55 | }; 56 | 57 | export function createTextContent(text: string): { type: 'text'; text: string } { 58 | return { type: 'text', text }; 59 | } 60 | 61 | export function createImageContent( 62 | data: string, 63 | mimeType: string, 64 | ): { type: 'image'; data: string; mimeType: string } { 65 | return { type: 'image', data, mimeType }; 66 | } 67 | 68 | /** 69 | * ValidationResult - Result of parameter validation operations 70 | */ 71 | export interface ValidationResult { 72 | isValid: boolean; 73 | errorResponse?: ToolResponse; 74 | warningResponse?: ToolResponse; 75 | } 76 | 77 | /** 78 | * CommandResponse - Generic result of command execution 79 | */ 80 | export interface CommandResponse { 81 | success: boolean; 82 | output: string; 83 | error?: string; 84 | process?: unknown; // ChildProcess from node:child_process 85 | } 86 | 87 | /** 88 | * Interface for shared build parameters 89 | */ 90 | export interface SharedBuildParams { 91 | workspacePath?: string; 92 | projectPath?: string; 93 | scheme: string; 94 | configuration: string; 95 | derivedDataPath?: string; 96 | extraArgs?: string[]; 97 | } 98 | 99 | /** 100 | * Interface for platform-specific build options 101 | */ 102 | export interface PlatformBuildOptions { 103 | platform: XcodePlatform; 104 | simulatorName?: string; 105 | simulatorId?: string; 106 | deviceId?: string; 107 | useLatestOS?: boolean; 108 | arch?: string; 109 | logPrefix: string; 110 | } 111 | ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Bug Report 2 | description: Report a bug or issue with XcodeBuildMCP 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to report an issue with XcodeBuildMCP! 10 | 11 | - type: textarea 12 | id: description 13 | attributes: 14 | label: Bug Description 15 | description: A description of the bug or issue you're experiencing. 16 | placeholder: When trying to build my iOS app using the AI assistant... 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: debug 22 | attributes: 23 | label: Debug Output 24 | description: Ask your agent "Run the XcodeBuildMCP `doctor` tool and return the output as markdown verbatim" and then copy paste it here. 25 | placeholder: | 26 | ``` 27 | XcodeBuildMCP Doctor 28 | 29 | Generated: 2025-08-11T17:42:29.812Z 30 | Server Version: 1.11.2 31 | 32 | ## System Information 33 | - platform: darwin 34 | - release: 25.0.0 35 | - arch: arm64 36 | ... 37 | ``` 38 | validations: 39 | required: true 40 | 41 | - type: input 42 | id: editor-client 43 | attributes: 44 | label: Editor/Client 45 | description: The editor or MCP client you're using 46 | placeholder: Cursor 0.49.1 47 | validations: 48 | required: true 49 | 50 | - type: input 51 | id: mcp-server-version 52 | attributes: 53 | label: MCP Server Version 54 | description: The version of XcodeBuildMCP you're using 55 | placeholder: 1.2.2 56 | validations: 57 | required: true 58 | 59 | - type: input 60 | id: llm 61 | attributes: 62 | label: LLM 63 | description: The AI model you're using 64 | placeholder: Claude 3.5 Sonnet 65 | validations: 66 | required: true 67 | 68 | - type: textarea 69 | id: mcp-config 70 | attributes: 71 | label: MCP Configuration 72 | description: Your MCP configuration file (if applicable) 73 | placeholder: | 74 | ```json 75 | { 76 | "mcpServers": { 77 | "XcodeBuildMCP": {...} 78 | } 79 | } 80 | ``` 81 | render: json 82 | 83 | - type: textarea 84 | id: steps 85 | attributes: 86 | label: Steps to Reproduce 87 | description: Steps to reproduce the behavior 88 | placeholder: | 89 | 1. What you asked the AI agent to do 90 | 2. What the AI agent attempted to do 91 | 3. What failed or didn't work as expected 92 | validations: 93 | required: true 94 | 95 | - type: textarea 96 | id: expected 97 | attributes: 98 | label: Expected Behavior 99 | description: What you expected to happen 100 | placeholder: The AI should have been able to... 101 | validations: 102 | required: true 103 | 104 | - type: textarea 105 | id: actual 106 | attributes: 107 | label: Actual Behavior 108 | description: What actually happened 109 | placeholder: Instead, the AI... 110 | validations: 111 | required: true 112 | 113 | - type: textarea 114 | id: error 115 | attributes: 116 | label: Error Messages 117 | description: Any error messages or unexpected output 118 | placeholder: Error message or output from the AI 119 | render: shell 120 | ``` -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "$schema": "https://raw.githubusercontent.com/microsoft/vscode/master/extensions/npm/schemas/v1.1.1/tasks.schema.json", 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "label": "build", 7 | "type": "npm", 8 | "script": "build", 9 | "group": { 10 | "kind": "build", 11 | "isDefault": true 12 | }, 13 | "presentation": { 14 | "echo": true, 15 | "reveal": "always", 16 | "focus": false, 17 | "panel": "shared", 18 | "showReuseMessage": true, 19 | "clear": false 20 | }, 21 | "problemMatcher": [ 22 | "$tsc" 23 | ] 24 | }, 25 | { 26 | "label": "run", 27 | "type": "npm", 28 | "script": "inspect", 29 | "group": "build", 30 | "presentation": { 31 | "echo": true, 32 | "reveal": "always", 33 | "focus": true, 34 | "panel": "new" 35 | } 36 | }, 37 | { 38 | "label": "test", 39 | "type": "npm", 40 | "script": "test", 41 | "group": { 42 | "kind": "test", 43 | "isDefault": true 44 | }, 45 | "presentation": { 46 | "echo": true, 47 | "reveal": "always", 48 | "focus": false, 49 | "panel": "shared" 50 | } 51 | }, 52 | { 53 | "label": "lint", 54 | "type": "npm", 55 | "script": "lint", 56 | "group": "build", 57 | "presentation": { 58 | "echo": true, 59 | "reveal": "silent", 60 | "focus": false, 61 | "panel": "shared" 62 | }, 63 | "problemMatcher": [ 64 | "$eslint-stylish" 65 | ] 66 | }, 67 | { 68 | "label": "lint:fix", 69 | "type": "npm", 70 | "script": "lint:fix", 71 | "group": "build" 72 | }, 73 | { 74 | "label": "format", 75 | "type": "npm", 76 | "script": "format", 77 | "group": "build" 78 | }, 79 | { 80 | "label": "typecheck (watch)", 81 | "type": "shell", 82 | "command": "npx tsc --noEmit --watch", 83 | "isBackground": true, 84 | "problemMatcher": [ 85 | "$tsc-watch" 86 | ], 87 | "group": "build" 88 | }, 89 | { 90 | "label": "dev (watch)", 91 | "type": "npm", 92 | "script": "dev", 93 | "isBackground": true, 94 | "group": "build", 95 | "presentation": { 96 | "panel": "dedicated", 97 | "reveal": "always" 98 | } 99 | }, 100 | { 101 | "label": "build: dev doctor", 102 | "dependsOn": [ 103 | "lint", 104 | "typecheck (watch)" 105 | ], 106 | "group": { 107 | "kind": "build", 108 | "isDefault": false 109 | } 110 | } 111 | ] 112 | } ``` -------------------------------------------------------------------------------- /src/mcp/tools/ui-testing/__tests__/index.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tests for ui-testing workflow metadata 3 | */ 4 | import { describe, it, expect } from 'vitest'; 5 | import { workflow } from '../index.ts'; 6 | 7 | describe('ui-testing workflow metadata', () => { 8 | describe('Workflow Structure', () => { 9 | it('should export workflow object with required properties', () => { 10 | expect(workflow).toHaveProperty('name'); 11 | expect(workflow).toHaveProperty('description'); 12 | expect(workflow).toHaveProperty('platforms'); 13 | expect(workflow).toHaveProperty('targets'); 14 | expect(workflow).toHaveProperty('capabilities'); 15 | }); 16 | 17 | it('should have correct workflow name', () => { 18 | expect(workflow.name).toBe('UI Testing & Automation'); 19 | }); 20 | 21 | it('should have correct description', () => { 22 | expect(workflow.description).toBe( 23 | 'UI automation and accessibility testing tools for iOS simulators. Perform gestures, interactions, screenshots, and UI analysis for automated testing workflows.', 24 | ); 25 | }); 26 | 27 | it('should have correct platforms array', () => { 28 | expect(workflow.platforms).toEqual(['iOS']); 29 | }); 30 | 31 | it('should have correct targets array', () => { 32 | expect(workflow.targets).toEqual(['simulator']); 33 | }); 34 | 35 | it('should have correct capabilities array', () => { 36 | expect(workflow.capabilities).toEqual([ 37 | 'ui-automation', 38 | 'gesture-simulation', 39 | 'screenshot-capture', 40 | 'accessibility-testing', 41 | 'ui-analysis', 42 | ]); 43 | }); 44 | }); 45 | 46 | describe('Workflow Validation', () => { 47 | it('should have valid string properties', () => { 48 | expect(typeof workflow.name).toBe('string'); 49 | expect(typeof workflow.description).toBe('string'); 50 | expect(workflow.name.length).toBeGreaterThan(0); 51 | expect(workflow.description.length).toBeGreaterThan(0); 52 | }); 53 | 54 | it('should have valid array properties', () => { 55 | expect(Array.isArray(workflow.platforms)).toBe(true); 56 | expect(Array.isArray(workflow.targets)).toBe(true); 57 | expect(Array.isArray(workflow.capabilities)).toBe(true); 58 | 59 | expect(workflow.platforms.length).toBeGreaterThan(0); 60 | expect(workflow.targets.length).toBeGreaterThan(0); 61 | expect(workflow.capabilities.length).toBeGreaterThan(0); 62 | }); 63 | 64 | it('should contain expected platform values', () => { 65 | expect(workflow.platforms).toContain('iOS'); 66 | }); 67 | 68 | it('should contain expected target values', () => { 69 | expect(workflow.targets).toContain('simulator'); 70 | }); 71 | 72 | it('should contain expected capability values', () => { 73 | expect(workflow.capabilities).toContain('ui-automation'); 74 | expect(workflow.capabilities).toContain('gesture-simulation'); 75 | expect(workflow.capabilities).toContain('screenshot-capture'); 76 | expect(workflow.capabilities).toContain('accessibility-testing'); 77 | expect(workflow.capabilities).toContain('ui-analysis'); 78 | }); 79 | 80 | it('should not have projectTypes property', () => { 81 | expect(workflow).not.toHaveProperty('projectTypes'); 82 | }); 83 | }); 84 | }); 85 | ``` -------------------------------------------------------------------------------- /.github/workflows/claude-code-review.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Claude Code Review 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | # Optional: Only run on specific file changes 7 | # paths: 8 | # - "src/**/*.ts" 9 | # - "src/**/*.tsx" 10 | # - "src/**/*.js" 11 | # - "src/**/*.jsx" 12 | 13 | jobs: 14 | claude-review: 15 | # Optional: Filter by PR author 16 | # if: | 17 | # github.event.pull_request.user.login == 'external-contributor' || 18 | # github.event.pull_request.user.login == 'new-developer' || 19 | # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' 20 | 21 | runs-on: ubuntu-latest 22 | permissions: 23 | contents: read 24 | pull-requests: read 25 | issues: read 26 | id-token: write 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v4 31 | with: 32 | fetch-depth: 1 33 | 34 | - name: Run Claude Code Review 35 | id: claude-review 36 | uses: anthropics/claude-code-action@beta 37 | with: 38 | claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} 39 | 40 | # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) 41 | # model: "claude-opus-4-20250514" 42 | 43 | # Direct prompt for automated review (no @claude mention needed) 44 | direct_prompt: | 45 | Please review this pull request and provide feedback on: 46 | - Code quality and best practices 47 | - Potential bugs or issues 48 | - Performance considerations 49 | - Security concerns 50 | - Test coverage 51 | 52 | Be constructive and helpful in your feedback. 53 | 54 | # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR 55 | # use_sticky_comment: true 56 | 57 | # Optional: Customize review based on file types 58 | # direct_prompt: | 59 | # Review this PR focusing on: 60 | # - For TypeScript files: Type safety and proper interface usage 61 | # - For API endpoints: Security, input validation, and error handling 62 | # - For React components: Performance, accessibility, and best practices 63 | # - For tests: Coverage, edge cases, and test quality 64 | 65 | # Optional: Different prompts for different authors 66 | # direct_prompt: | 67 | # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' || 'Please provide a thorough code review focusing on our coding standards and best practices.' }} 68 | 69 | # Optional: Add specific tools for running tests or linting 70 | # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)" 71 | 72 | # Optional: Skip review for certain conditions 73 | # if: | 74 | # !contains(github.event.pull_request.title, '[skip-review]') && 75 | # !contains(github.event.pull_request.title, '[WIP]') 76 | 77 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/swift-package/swift_package_build.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import path from 'node:path'; 3 | import { createErrorResponse } from '../../../utils/responses/index.ts'; 4 | import { log } from '../../../utils/logging/index.ts'; 5 | import type { CommandExecutor } from '../../../utils/execution/index.ts'; 6 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; 7 | import { ToolResponse } from '../../../types/common.ts'; 8 | import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; 9 | 10 | // Define schema as ZodObject 11 | const swiftPackageBuildSchema = z.object({ 12 | packagePath: z.string().describe('Path to the Swift package root (Required)'), 13 | targetName: z.string().optional().describe('Optional target to build'), 14 | configuration: z 15 | .enum(['debug', 'release']) 16 | .optional() 17 | .describe('Swift package configuration (debug, release)'), 18 | architectures: z.array(z.string()).optional().describe('Target architectures to build for'), 19 | parseAsLibrary: z.boolean().optional().describe('Build as library instead of executable'), 20 | }); 21 | 22 | // Use z.infer for type safety 23 | type SwiftPackageBuildParams = z.infer<typeof swiftPackageBuildSchema>; 24 | 25 | export async function swift_package_buildLogic( 26 | params: SwiftPackageBuildParams, 27 | executor: CommandExecutor, 28 | ): Promise<ToolResponse> { 29 | const resolvedPath = path.resolve(params.packagePath); 30 | const swiftArgs = ['build', '--package-path', resolvedPath]; 31 | 32 | if (params.configuration && params.configuration.toLowerCase() === 'release') { 33 | swiftArgs.push('-c', 'release'); 34 | } 35 | 36 | if (params.targetName) { 37 | swiftArgs.push('--target', params.targetName); 38 | } 39 | 40 | if (params.architectures) { 41 | for (const arch of params.architectures) { 42 | swiftArgs.push('--arch', arch); 43 | } 44 | } 45 | 46 | if (params.parseAsLibrary) { 47 | swiftArgs.push('-Xswiftc', '-parse-as-library'); 48 | } 49 | 50 | log('info', `Running swift ${swiftArgs.join(' ')}`); 51 | try { 52 | const result = await executor(['swift', ...swiftArgs], 'Swift Package Build', true, undefined); 53 | if (!result.success) { 54 | const errorMessage = result.error ?? result.output ?? 'Unknown error'; 55 | return createErrorResponse('Swift package build failed', errorMessage); 56 | } 57 | 58 | return { 59 | content: [ 60 | { type: 'text', text: '✅ Swift package build succeeded.' }, 61 | { 62 | type: 'text', 63 | text: '💡 Next: Run tests with swift_package_test or execute with swift_package_run', 64 | }, 65 | { type: 'text', text: result.output }, 66 | ], 67 | isError: false, 68 | }; 69 | } catch (error) { 70 | const message = error instanceof Error ? error.message : String(error); 71 | log('error', `Swift package build failed: ${message}`); 72 | return createErrorResponse('Failed to execute swift build', message); 73 | } 74 | } 75 | 76 | export default { 77 | name: 'swift_package_build', 78 | description: 'Builds a Swift Package with swift build', 79 | schema: swiftPackageBuildSchema.shape, // MCP SDK compatibility 80 | handler: createTypedTool( 81 | swiftPackageBuildSchema, 82 | swift_package_buildLogic, 83 | getDefaultCommandExecutor, 84 | ), 85 | }; 86 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/swift-package/swift_package_list.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Note: This tool shares the activeProcesses map with swift_package_run 2 | // Since both are in the same workflow directory, they can share state 3 | 4 | // Import the shared activeProcesses map from swift_package_run 5 | // This maintains the same behavior as the original implementation 6 | import { z } from 'zod'; 7 | import { ToolResponse, createTextContent } from '../../../types/common.ts'; 8 | import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; 9 | import { getDefaultCommandExecutor } from '../../../utils/command.ts'; 10 | 11 | interface ProcessInfo { 12 | executableName?: string; 13 | startedAt: Date; 14 | packagePath: string; 15 | } 16 | 17 | const activeProcesses = new Map<number, ProcessInfo>(); 18 | 19 | /** 20 | * Process list dependencies for dependency injection 21 | */ 22 | export interface ProcessListDependencies { 23 | processMap?: Map<number, ProcessInfo>; 24 | arrayFrom?: typeof Array.from; 25 | dateNow?: typeof Date.now; 26 | } 27 | 28 | /** 29 | * Swift package list business logic - extracted for testability and separation of concerns 30 | * @param params - Parameters (unused, but maintained for consistency) 31 | * @param dependencies - Injectable dependencies for testing 32 | * @returns ToolResponse with process list information 33 | */ 34 | export async function swift_package_listLogic( 35 | params?: unknown, 36 | dependencies?: ProcessListDependencies, 37 | ): Promise<ToolResponse> { 38 | const processMap = dependencies?.processMap ?? activeProcesses; 39 | const arrayFrom = dependencies?.arrayFrom ?? Array.from; 40 | const dateNow = dependencies?.dateNow ?? Date.now; 41 | 42 | const processes = arrayFrom(processMap.entries()); 43 | 44 | if (processes.length === 0) { 45 | return { 46 | content: [ 47 | createTextContent('ℹ️ No Swift Package processes currently running.'), 48 | createTextContent('💡 Use swift_package_run to start an executable.'), 49 | ], 50 | }; 51 | } 52 | 53 | const content = [createTextContent(`📋 Active Swift Package processes (${processes.length}):`)]; 54 | 55 | for (const [pid, info] of processes) { 56 | // Use logical OR instead of nullish coalescing to treat empty strings as falsy 57 | // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing 58 | const executableName = info.executableName || 'default'; 59 | const runtime = Math.max(1, Math.round((dateNow() - info.startedAt.getTime()) / 1000)); 60 | content.push( 61 | createTextContent( 62 | ` • PID ${pid}: ${executableName} (${info.packagePath}) - running ${runtime}s`, 63 | ), 64 | ); 65 | } 66 | 67 | content.push(createTextContent('💡 Use swift_package_stop with a PID to terminate a process.')); 68 | 69 | return { content }; 70 | } 71 | 72 | // Define schema as ZodObject (empty for this tool) 73 | const swiftPackageListSchema = z.object({}); 74 | 75 | // Use z.infer for type safety 76 | type SwiftPackageListParams = z.infer<typeof swiftPackageListSchema>; 77 | 78 | export default { 79 | name: 'swift_package_list', 80 | description: 'Lists currently running Swift Package processes', 81 | schema: swiftPackageListSchema.shape, // MCP SDK compatibility 82 | handler: createTypedTool( 83 | swiftPackageListSchema, 84 | (params: SwiftPackageListParams) => { 85 | return swift_package_listLogic(params); 86 | }, 87 | getDefaultCommandExecutor, 88 | ), 89 | }; 90 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/logging/__tests__/index.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tests for logging workflow metadata 3 | */ 4 | import { describe, it, expect } from 'vitest'; 5 | import { workflow } from '../index.ts'; 6 | 7 | describe('logging workflow metadata', () => { 8 | describe('Workflow Structure', () => { 9 | it('should export workflow object with required properties', () => { 10 | expect(workflow).toHaveProperty('name'); 11 | expect(workflow).toHaveProperty('description'); 12 | expect(workflow).toHaveProperty('platforms'); 13 | expect(workflow).toHaveProperty('targets'); 14 | expect(workflow).toHaveProperty('capabilities'); 15 | }); 16 | 17 | it('should have correct workflow name', () => { 18 | expect(workflow.name).toBe('Log Capture & Management'); 19 | }); 20 | 21 | it('should have correct description', () => { 22 | expect(workflow.description).toBe( 23 | 'Log capture and management tools for iOS simulators and physical devices. Start, stop, and analyze application and system logs during development and testing.', 24 | ); 25 | }); 26 | 27 | it('should have correct platforms array', () => { 28 | expect(workflow.platforms).toEqual(['iOS', 'watchOS', 'tvOS', 'visionOS']); 29 | }); 30 | 31 | it('should have correct targets array', () => { 32 | expect(workflow.targets).toEqual(['simulator', 'device']); 33 | }); 34 | 35 | it('should have correct capabilities array', () => { 36 | expect(workflow.capabilities).toEqual([ 37 | 'log-capture', 38 | 'log-analysis', 39 | 'debugging', 40 | 'monitoring', 41 | ]); 42 | }); 43 | }); 44 | 45 | describe('Workflow Validation', () => { 46 | it('should have valid string properties', () => { 47 | expect(typeof workflow.name).toBe('string'); 48 | expect(typeof workflow.description).toBe('string'); 49 | expect(workflow.name.length).toBeGreaterThan(0); 50 | expect(workflow.description.length).toBeGreaterThan(0); 51 | }); 52 | 53 | it('should have valid array properties', () => { 54 | expect(Array.isArray(workflow.platforms)).toBe(true); 55 | expect(Array.isArray(workflow.targets)).toBe(true); 56 | expect(Array.isArray(workflow.capabilities)).toBe(true); 57 | 58 | expect(workflow.platforms.length).toBeGreaterThan(0); 59 | expect(workflow.targets.length).toBeGreaterThan(0); 60 | expect(workflow.capabilities.length).toBeGreaterThan(0); 61 | }); 62 | 63 | it('should contain expected platform values', () => { 64 | expect(workflow.platforms).toContain('iOS'); 65 | expect(workflow.platforms).toContain('watchOS'); 66 | expect(workflow.platforms).toContain('tvOS'); 67 | expect(workflow.platforms).toContain('visionOS'); 68 | }); 69 | 70 | it('should contain expected target values', () => { 71 | expect(workflow.targets).toContain('simulator'); 72 | expect(workflow.targets).toContain('device'); 73 | }); 74 | 75 | it('should contain expected capability values', () => { 76 | expect(workflow.capabilities).toContain('log-capture'); 77 | expect(workflow.capabilities).toContain('log-analysis'); 78 | expect(workflow.capabilities).toContain('debugging'); 79 | expect(workflow.capabilities).toContain('monitoring'); 80 | }); 81 | 82 | it('should not have projectTypes property', () => { 83 | expect(workflow).not.toHaveProperty('projectTypes'); 84 | }); 85 | }); 86 | }); 87 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator-management/reset_sim_location.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { ToolResponse } from '../../../types/common.ts'; 3 | import { log } from '../../../utils/logging/index.ts'; 4 | import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; 5 | import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; 6 | 7 | // Define schema as ZodObject 8 | const resetSimulatorLocationSchema = z.object({ 9 | simulatorUuid: z 10 | .string() 11 | .describe('UUID of the simulator to use (obtained from list_simulators)'), 12 | }); 13 | 14 | // Use z.infer for type safety 15 | type ResetSimulatorLocationParams = z.infer<typeof resetSimulatorLocationSchema>; 16 | 17 | // Helper function to execute simctl commands and handle responses 18 | async function executeSimctlCommandAndRespond( 19 | params: ResetSimulatorLocationParams, 20 | simctlSubCommand: string[], 21 | operationDescriptionForXcodeCommand: string, 22 | successMessage: string, 23 | failureMessagePrefix: string, 24 | operationLogContext: string, 25 | executor: CommandExecutor, 26 | extraValidation?: () => ToolResponse | undefined, 27 | ): Promise<ToolResponse> { 28 | if (extraValidation) { 29 | const validationResult = extraValidation(); 30 | if (validationResult) { 31 | return validationResult; 32 | } 33 | } 34 | 35 | try { 36 | const command = ['xcrun', 'simctl', ...simctlSubCommand]; 37 | const result = await executor(command, operationDescriptionForXcodeCommand, true, {}); 38 | 39 | if (!result.success) { 40 | const fullFailureMessage = `${failureMessagePrefix}: ${result.error}`; 41 | log( 42 | 'error', 43 | `${fullFailureMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorUuid})`, 44 | ); 45 | return { 46 | content: [{ type: 'text', text: fullFailureMessage }], 47 | }; 48 | } 49 | 50 | log( 51 | 'info', 52 | `${successMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorUuid})`, 53 | ); 54 | return { 55 | content: [{ type: 'text', text: successMessage }], 56 | }; 57 | } catch (error) { 58 | const errorMessage = error instanceof Error ? error.message : String(error); 59 | const fullFailureMessage = `${failureMessagePrefix}: ${errorMessage}`; 60 | log( 61 | 'error', 62 | `Error during ${operationLogContext} for simulator ${params.simulatorUuid}: ${errorMessage}`, 63 | ); 64 | return { 65 | content: [{ type: 'text', text: fullFailureMessage }], 66 | }; 67 | } 68 | } 69 | 70 | export async function reset_sim_locationLogic( 71 | params: ResetSimulatorLocationParams, 72 | executor: CommandExecutor, 73 | ): Promise<ToolResponse> { 74 | log('info', `Resetting simulator ${params.simulatorUuid} location`); 75 | 76 | return executeSimctlCommandAndRespond( 77 | params, 78 | ['location', params.simulatorUuid, 'clear'], 79 | 'Reset Simulator Location', 80 | `Successfully reset simulator ${params.simulatorUuid} location.`, 81 | 'Failed to reset simulator location', 82 | 'reset simulator location', 83 | executor, 84 | ); 85 | } 86 | 87 | export default { 88 | name: 'reset_sim_location', 89 | description: "Resets the simulator's location to default.", 90 | schema: resetSimulatorLocationSchema.shape, // MCP SDK compatibility 91 | handler: createTypedTool( 92 | resetSimulatorLocationSchema, 93 | reset_sim_locationLogic, 94 | getDefaultCommandExecutor, 95 | ), 96 | }; 97 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/macos/__tests__/re-exports.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tests for macos-project re-export files 3 | * These files re-export tools from macos-workspace to avoid duplication 4 | */ 5 | import { describe, it, expect } from 'vitest'; 6 | 7 | // Import all re-export tools 8 | import testMacos from '../test_macos.ts'; 9 | import buildMacos from '../build_macos.ts'; 10 | import buildRunMacos from '../build_run_macos.ts'; 11 | import getMacAppPath from '../get_mac_app_path.ts'; 12 | 13 | describe('macos-project re-exports', () => { 14 | describe('test_macos re-export', () => { 15 | it('should re-export test_macos tool correctly', () => { 16 | expect(testMacos.name).toBe('test_macos'); 17 | expect(typeof testMacos.handler).toBe('function'); 18 | expect(testMacos.schema).toBeDefined(); 19 | expect(typeof testMacos.description).toBe('string'); 20 | }); 21 | }); 22 | 23 | describe('build_macos re-export', () => { 24 | it('should re-export build_macos tool correctly', () => { 25 | expect(buildMacos.name).toBe('build_macos'); 26 | expect(typeof buildMacos.handler).toBe('function'); 27 | expect(buildMacos.schema).toBeDefined(); 28 | expect(typeof buildMacos.description).toBe('string'); 29 | }); 30 | }); 31 | 32 | describe('build_run_macos re-export', () => { 33 | it('should re-export build_run_macos tool correctly', () => { 34 | expect(buildRunMacos.name).toBe('build_run_macos'); 35 | expect(typeof buildRunMacos.handler).toBe('function'); 36 | expect(buildRunMacos.schema).toBeDefined(); 37 | expect(typeof buildRunMacos.description).toBe('string'); 38 | }); 39 | }); 40 | 41 | describe('get_mac_app_path re-export', () => { 42 | it('should re-export get_mac_app_path tool correctly', () => { 43 | expect(getMacAppPath.name).toBe('get_mac_app_path'); 44 | expect(typeof getMacAppPath.handler).toBe('function'); 45 | expect(getMacAppPath.schema).toBeDefined(); 46 | expect(typeof getMacAppPath.description).toBe('string'); 47 | }); 48 | }); 49 | 50 | describe('All re-exports validation', () => { 51 | const reExports = [ 52 | { tool: testMacos, name: 'test_macos' }, 53 | { tool: buildMacos, name: 'build_macos' }, 54 | { tool: buildRunMacos, name: 'build_run_macos' }, 55 | { tool: getMacAppPath, name: 'get_mac_app_path' }, 56 | ]; 57 | 58 | it('should have all required tool properties', () => { 59 | reExports.forEach(({ tool, name }) => { 60 | expect(tool).toHaveProperty('name'); 61 | expect(tool).toHaveProperty('description'); 62 | expect(tool).toHaveProperty('schema'); 63 | expect(tool).toHaveProperty('handler'); 64 | expect(tool.name).toBe(name); 65 | }); 66 | }); 67 | 68 | it('should have callable handlers', () => { 69 | reExports.forEach(({ tool, name }) => { 70 | expect(typeof tool.handler).toBe('function'); 71 | expect(tool.handler.length).toBeGreaterThanOrEqual(0); 72 | }); 73 | }); 74 | 75 | it('should have valid schemas', () => { 76 | reExports.forEach(({ tool, name }) => { 77 | expect(tool.schema).toBeDefined(); 78 | expect(typeof tool.schema).toBe('object'); 79 | }); 80 | }); 81 | 82 | it('should have non-empty descriptions', () => { 83 | reExports.forEach(({ tool, name }) => { 84 | expect(typeof tool.description).toBe('string'); 85 | expect(tool.description.length).toBeGreaterThan(0); 86 | }); 87 | }); 88 | }); 89 | }); 90 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/macos/__tests__/index.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tests for macos-project workflow metadata 3 | */ 4 | import { describe, it, expect } from 'vitest'; 5 | import { workflow } from '../index.ts'; 6 | 7 | describe('macos-project workflow metadata', () => { 8 | describe('Workflow Structure', () => { 9 | it('should export workflow object with required properties', () => { 10 | expect(workflow).toHaveProperty('name'); 11 | expect(workflow).toHaveProperty('description'); 12 | expect(workflow).toHaveProperty('platforms'); 13 | expect(workflow).toHaveProperty('targets'); 14 | expect(workflow).toHaveProperty('projectTypes'); 15 | expect(workflow).toHaveProperty('capabilities'); 16 | }); 17 | 18 | it('should have correct workflow name', () => { 19 | expect(workflow.name).toBe('macOS Development'); 20 | }); 21 | 22 | it('should have correct description', () => { 23 | expect(workflow.description).toBe( 24 | 'Complete macOS development workflow for both .xcodeproj and .xcworkspace files. Build, test, deploy, and manage macOS applications.', 25 | ); 26 | }); 27 | 28 | it('should have correct platforms array', () => { 29 | expect(workflow.platforms).toEqual(['macOS']); 30 | }); 31 | 32 | it('should have correct targets array', () => { 33 | expect(workflow.targets).toEqual(['native']); 34 | }); 35 | 36 | it('should have correct projectTypes array', () => { 37 | expect(workflow.projectTypes).toEqual(['project', 'workspace']); 38 | }); 39 | 40 | it('should have correct capabilities array', () => { 41 | expect(workflow.capabilities).toEqual(['build', 'test', 'deploy', 'debug', 'app-management']); 42 | }); 43 | }); 44 | 45 | describe('Workflow Validation', () => { 46 | it('should have valid string properties', () => { 47 | expect(typeof workflow.name).toBe('string'); 48 | expect(typeof workflow.description).toBe('string'); 49 | expect(workflow.name.length).toBeGreaterThan(0); 50 | expect(workflow.description.length).toBeGreaterThan(0); 51 | }); 52 | 53 | it('should have valid array properties', () => { 54 | expect(Array.isArray(workflow.platforms)).toBe(true); 55 | expect(Array.isArray(workflow.targets)).toBe(true); 56 | expect(Array.isArray(workflow.projectTypes)).toBe(true); 57 | expect(Array.isArray(workflow.capabilities)).toBe(true); 58 | 59 | expect(workflow.platforms.length).toBeGreaterThan(0); 60 | expect(workflow.targets.length).toBeGreaterThan(0); 61 | expect(workflow.projectTypes.length).toBeGreaterThan(0); 62 | expect(workflow.capabilities.length).toBeGreaterThan(0); 63 | }); 64 | 65 | it('should contain expected platform values', () => { 66 | expect(workflow.platforms).toContain('macOS'); 67 | }); 68 | 69 | it('should contain expected target values', () => { 70 | expect(workflow.targets).toContain('native'); 71 | }); 72 | 73 | it('should contain expected project type values', () => { 74 | expect(workflow.projectTypes).toContain('project'); 75 | }); 76 | 77 | it('should contain expected capability values', () => { 78 | expect(workflow.capabilities).toContain('build'); 79 | expect(workflow.capabilities).toContain('test'); 80 | expect(workflow.capabilities).toContain('deploy'); 81 | expect(workflow.capabilities).toContain('debug'); 82 | expect(workflow.capabilities).toContain('app-management'); 83 | }); 84 | }); 85 | }); 86 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/device/build_device.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Device Shared Plugin: Build Device (Unified) 3 | * 4 | * Builds an app from a project or workspace for a physical Apple device. 5 | * Accepts mutually exclusive `projectPath` or `workspacePath`. 6 | */ 7 | 8 | import { z } from 'zod'; 9 | import { ToolResponse, XcodePlatform } from '../../../types/common.ts'; 10 | import { executeXcodeBuildCommand } from '../../../utils/build/index.ts'; 11 | import type { CommandExecutor } from '../../../utils/execution/index.ts'; 12 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; 13 | import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; 14 | import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts'; 15 | 16 | // Unified schema: XOR between projectPath and workspacePath 17 | const baseSchemaObject = z.object({ 18 | projectPath: z.string().optional().describe('Path to the .xcodeproj file'), 19 | workspacePath: z.string().optional().describe('Path to the .xcworkspace file'), 20 | scheme: z.string().describe('The scheme to build'), 21 | configuration: z.string().optional().describe('Build configuration (Debug, Release)'), 22 | derivedDataPath: z.string().optional().describe('Path to derived data directory'), 23 | extraArgs: z.array(z.string()).optional().describe('Additional arguments to pass to xcodebuild'), 24 | preferXcodebuild: z.boolean().optional().describe('Prefer xcodebuild over faster alternatives'), 25 | }); 26 | 27 | const baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject); 28 | 29 | const buildDeviceSchema = baseSchema 30 | .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, { 31 | message: 'Either projectPath or workspacePath is required.', 32 | }) 33 | .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), { 34 | message: 'projectPath and workspacePath are mutually exclusive. Provide only one.', 35 | }); 36 | 37 | export type BuildDeviceParams = z.infer<typeof buildDeviceSchema>; 38 | 39 | /** 40 | * Business logic for building device project or workspace. 41 | * Exported for direct testing and reuse. 42 | */ 43 | export async function buildDeviceLogic( 44 | params: BuildDeviceParams, 45 | executor: CommandExecutor, 46 | ): Promise<ToolResponse> { 47 | const processedParams = { 48 | ...params, 49 | configuration: params.configuration ?? 'Debug', // Default config 50 | }; 51 | 52 | return executeXcodeBuildCommand( 53 | processedParams, 54 | { 55 | platform: XcodePlatform.iOS, 56 | logPrefix: 'iOS Device Build', 57 | }, 58 | params.preferXcodebuild ?? false, 59 | 'build', 60 | executor, 61 | ); 62 | } 63 | 64 | export default { 65 | name: 'build_device', 66 | description: 'Builds an app for a connected device.', 67 | schema: baseSchemaObject.omit({ 68 | projectPath: true, 69 | workspacePath: true, 70 | scheme: true, 71 | configuration: true, 72 | } as const).shape, 73 | handler: createSessionAwareTool<BuildDeviceParams>({ 74 | internalSchema: buildDeviceSchema as unknown as z.ZodType<BuildDeviceParams>, 75 | logicFunction: buildDeviceLogic, 76 | getExecutor: getDefaultCommandExecutor, 77 | requirements: [ 78 | { allOf: ['scheme'], message: 'scheme is required' }, 79 | { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' }, 80 | ], 81 | exclusivePairs: [['projectPath', 'workspacePath']], 82 | }), 83 | }; 84 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "xcodebuildmcp", 3 | "version": "1.14.1", 4 | "mcpName": "com.xcodebuildmcp/XcodeBuildMCP", 5 | "iOSTemplateVersion": "v1.0.8", 6 | "macOSTemplateVersion": "v1.0.5", 7 | "main": "build/index.js", 8 | "type": "module", 9 | "bin": { 10 | "xcodebuildmcp": "build/index.js", 11 | "xcodebuildmcp-doctor": "build/doctor-cli.js" 12 | }, 13 | "scripts": { 14 | "build": "node -e \"const fs = require('fs'); const pkg = require('./package.json'); fs.writeFileSync('src/version.ts', \\`export const version = '\\${pkg.version}';\\nexport const iOSTemplateVersion = '\\${pkg.iOSTemplateVersion}';\\nexport const macOSTemplateVersion = '\\${pkg.macOSTemplateVersion}';\\n\\`)\" && tsup", 15 | "dev": "npm run build && tsup --watch", 16 | "bundle:axe": "scripts/bundle-axe.sh", 17 | "lint": "eslint 'src/**/*.{js,ts}'", 18 | "lint:fix": "eslint 'src/**/*.{js,ts}' --fix", 19 | "format": "prettier --write 'src/**/*.{js,ts}'", 20 | "format:check": "prettier --check 'src/**/*.{js,ts}'", 21 | "typecheck": "npx tsc --noEmit", 22 | "inspect": "npx @modelcontextprotocol/inspector node build/index.js", 23 | "doctor": "node build/doctor-cli.js", 24 | "tools": "npx tsx scripts/tools-cli.ts", 25 | "tools:list": "npx tsx scripts/tools-cli.ts list", 26 | "tools:static": "npx tsx scripts/tools-cli.ts static", 27 | "tools:count": "npx tsx scripts/tools-cli.ts count --static", 28 | "tools:analysis": "npx tsx scripts/analysis/tools-analysis.ts", 29 | "docs:update": "npx tsx scripts/update-tools-docs.ts", 30 | "docs:update:dry-run": "npx tsx scripts/update-tools-docs.ts --dry-run --verbose", 31 | "test": "vitest run", 32 | "test:watch": "vitest", 33 | "test:ui": "vitest --ui", 34 | "test:coverage": "vitest run --coverage" 35 | }, 36 | "files": [ 37 | "build", 38 | "bundled", 39 | "plugins" 40 | ], 41 | "keywords": [ 42 | "xcodebuild", 43 | "mcp", 44 | "modelcontextprotocol", 45 | "xcode", 46 | "ios", 47 | "macos", 48 | "simulator" 49 | ], 50 | "author": "Cameron Cooke", 51 | "license": "MIT", 52 | "description": "XcodeBuildMCP is a ModelContextProtocol server that provides tools for Xcode project management, simulator management, and app utilities.", 53 | "repository": { 54 | "type": "git", 55 | "url": "git+https://github.com/cameroncooke/XcodeBuildMCP.git" 56 | }, 57 | "homepage": "https://www.async-let.com/blog/xcodebuild-mcp/", 58 | "bugs": { 59 | "url": "https://github.com/cameroncooke/XcodeBuildMCP/issues" 60 | }, 61 | "dependencies": { 62 | "@camsoft/mcp-sdk": "^1.17.1", 63 | "@sentry/cli": "^2.43.1", 64 | "@sentry/node": "^10.5.0", 65 | "uuid": "^11.1.0", 66 | "zod": "^3.24.2" 67 | }, 68 | "devDependencies": { 69 | "@bacons/xcode": "^1.0.0-alpha.24", 70 | "@eslint/eslintrc": "^3.3.1", 71 | "@eslint/js": "^9.23.0", 72 | "@types/node": "^22.13.6", 73 | "@typescript-eslint/eslint-plugin": "^8.28.0", 74 | "@typescript-eslint/parser": "^8.28.0", 75 | "@vitest/coverage-v8": "^3.2.4", 76 | "@vitest/ui": "^3.2.4", 77 | "eslint": "^9.23.0", 78 | "eslint-config-prettier": "^10.1.1", 79 | "eslint-plugin-prettier": "^5.2.5", 80 | "playwright": "^1.53.0", 81 | "prettier": "^3.5.3", 82 | "ts-node": "^10.9.2", 83 | "tsup": "^8.5.0", 84 | "tsx": "^4.20.4", 85 | "typescript": "^5.8.2", 86 | "typescript-eslint": "^8.28.0", 87 | "vitest": "^3.2.4", 88 | "xcode": "^3.0.1" 89 | } 90 | } 91 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator-management/sim_statusbar.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { ToolResponse } from '../../../types/common.ts'; 3 | import { log } from '../../../utils/logging/index.ts'; 4 | import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; 5 | import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; 6 | 7 | // Define schema as ZodObject 8 | const simStatusbarSchema = z.object({ 9 | simulatorUuid: z 10 | .string() 11 | .describe('UUID of the simulator to use (obtained from list_simulators)'), 12 | dataNetwork: z 13 | .enum([ 14 | 'clear', 15 | 'hide', 16 | 'wifi', 17 | '3g', 18 | '4g', 19 | 'lte', 20 | 'lte-a', 21 | 'lte+', 22 | '5g', 23 | '5g+', 24 | '5g-uwb', 25 | '5g-uc', 26 | ]) 27 | .describe( 28 | 'Data network type to display in status bar. Use "clear" to reset all overrides. Valid values: clear, hide, wifi, 3g, 4g, lte, lte-a, lte+, 5g, 5g+, 5g-uwb, 5g-uc.', 29 | ), 30 | }); 31 | 32 | // Use z.infer for type safety 33 | type SimStatusbarParams = z.infer<typeof simStatusbarSchema>; 34 | 35 | export async function sim_statusbarLogic( 36 | params: SimStatusbarParams, 37 | executor: CommandExecutor, 38 | ): Promise<ToolResponse> { 39 | log( 40 | 'info', 41 | `Setting simulator ${params.simulatorUuid} status bar data network to ${params.dataNetwork}`, 42 | ); 43 | 44 | try { 45 | let command: string[]; 46 | let successMessage: string; 47 | 48 | if (params.dataNetwork === 'clear') { 49 | command = ['xcrun', 'simctl', 'status_bar', params.simulatorUuid, 'clear']; 50 | successMessage = `Successfully cleared status bar overrides for simulator ${params.simulatorUuid}`; 51 | } else { 52 | command = [ 53 | 'xcrun', 54 | 'simctl', 55 | 'status_bar', 56 | params.simulatorUuid, 57 | 'override', 58 | '--dataNetwork', 59 | params.dataNetwork, 60 | ]; 61 | successMessage = `Successfully set simulator ${params.simulatorUuid} status bar data network to ${params.dataNetwork}`; 62 | } 63 | 64 | const result = await executor(command, 'Set Status Bar', true, undefined); 65 | 66 | if (!result.success) { 67 | const failureMessage = `Failed to set status bar: ${result.error}`; 68 | log('error', `${failureMessage} (simulator: ${params.simulatorUuid})`); 69 | return { 70 | content: [{ type: 'text', text: failureMessage }], 71 | isError: true, 72 | }; 73 | } 74 | 75 | log('info', `${successMessage} (simulator: ${params.simulatorUuid})`); 76 | return { 77 | content: [{ type: 'text', text: successMessage }], 78 | }; 79 | } catch (error) { 80 | const errorMessage = error instanceof Error ? error.message : String(error); 81 | const failureMessage = `Failed to set status bar: ${errorMessage}`; 82 | log('error', `Error setting status bar for simulator ${params.simulatorUuid}: ${errorMessage}`); 83 | return { 84 | content: [{ type: 'text', text: failureMessage }], 85 | isError: true, 86 | }; 87 | } 88 | } 89 | 90 | export default { 91 | name: 'sim_statusbar', 92 | description: 93 | 'Sets the data network indicator in the iOS simulator status bar. Use "clear" to reset all overrides, or specify a network type (hide, wifi, 3g, 4g, lte, lte-a, lte+, 5g, 5g+, 5g-uwb, 5g-uc).', 94 | schema: simStatusbarSchema.shape, // MCP SDK compatibility 95 | handler: createTypedTool(simStatusbarSchema, sim_statusbarLogic, getDefaultCommandExecutor), 96 | }; 97 | ``` -------------------------------------------------------------------------------- /src/utils/axe-helpers.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * AXe Helper Functions 3 | * 4 | * This utility module provides functions to work with the bundled AXe tool. 5 | * Always uses the bundled version to ensure consistency. 6 | */ 7 | 8 | import { existsSync } from 'fs'; 9 | import { dirname, join } from 'path'; 10 | import { fileURLToPath } from 'url'; 11 | import { createTextResponse } from './validation.ts'; 12 | import { ToolResponse } from '../types/common.ts'; 13 | import type { CommandExecutor } from './execution/index.ts'; 14 | import { getDefaultCommandExecutor } from './execution/index.ts'; 15 | 16 | // Get bundled AXe path - always use the bundled version for consistency 17 | const __filename = fileURLToPath(import.meta.url); 18 | const __dirname = dirname(__filename); 19 | // In the npm package, build/index.js is at the same level as bundled/ 20 | // So we go up one level from build/ to get to the package root 21 | const bundledAxePath = join(__dirname, '..', 'bundled', 'axe'); 22 | 23 | /** 24 | * Get the path to the bundled axe binary 25 | */ 26 | export function getAxePath(): string | null { 27 | // Always use bundled version for consistency 28 | if (existsSync(bundledAxePath)) { 29 | return bundledAxePath; 30 | } 31 | return null; 32 | } 33 | 34 | /** 35 | * Get environment variables needed for bundled AXe to run 36 | */ 37 | export function getBundledAxeEnvironment(): Record<string, string> { 38 | // No special environment variables needed - bundled AXe binary 39 | // has proper @rpath configuration to find frameworks 40 | return {}; 41 | } 42 | 43 | /** 44 | * Check if bundled axe tool is available 45 | */ 46 | export function areAxeToolsAvailable(): boolean { 47 | return getAxePath() !== null; 48 | } 49 | 50 | export function createAxeNotAvailableResponse(): ToolResponse { 51 | return createTextResponse( 52 | 'Bundled axe tool not found. UI automation features are not available.\n\n' + 53 | 'This is likely an installation issue with the npm package.\n' + 54 | 'Please reinstall xcodebuildmcp or report this issue.', 55 | true, 56 | ); 57 | } 58 | 59 | /** 60 | * Compare two semver strings a and b. 61 | * Returns 1 if a > b, -1 if a < b, 0 if equal. 62 | */ 63 | function compareSemver(a: string, b: string): number { 64 | const pa = a.split('.').map((n) => parseInt(n, 10)); 65 | const pb = b.split('.').map((n) => parseInt(n, 10)); 66 | const len = Math.max(pa.length, pb.length); 67 | for (let i = 0; i < len; i++) { 68 | const da = Number.isFinite(pa[i]) ? pa[i] : 0; 69 | const db = Number.isFinite(pb[i]) ? pb[i] : 0; 70 | if (da > db) return 1; 71 | if (da < db) return -1; 72 | } 73 | return 0; 74 | } 75 | 76 | /** 77 | * Determine whether the bundled AXe meets a minimum version requirement. 78 | * Runs `axe --version` and parses a semantic version (e.g., "1.1.0"). 79 | * If AXe is missing or the version cannot be parsed, returns false. 80 | */ 81 | export async function isAxeAtLeastVersion( 82 | required: string, 83 | executor?: CommandExecutor, 84 | ): Promise<boolean> { 85 | const axePath = getAxePath(); 86 | if (!axePath) return false; 87 | 88 | const exec = executor ?? getDefaultCommandExecutor(); 89 | try { 90 | const res = await exec([axePath, '--version'], 'AXe Version', true); 91 | if (!res.success) return false; 92 | 93 | const output = res.output ?? ''; 94 | const versionMatch = output.match(/(\d+\.\d+\.\d+)/); 95 | if (!versionMatch) return false; 96 | 97 | const current = versionMatch[1]; 98 | return compareSemver(current, required) >= 0; 99 | } catch { 100 | return false; 101 | } 102 | } 103 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator/__tests__/index.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tests for simulator-project workflow metadata 3 | */ 4 | import { describe, it, expect } from 'vitest'; 5 | import { workflow } from '../index.ts'; 6 | 7 | describe('simulator-project workflow metadata', () => { 8 | describe('Workflow Structure', () => { 9 | it('should export workflow object with required properties', () => { 10 | expect(workflow).toHaveProperty('name'); 11 | expect(workflow).toHaveProperty('description'); 12 | expect(workflow).toHaveProperty('platforms'); 13 | expect(workflow).toHaveProperty('targets'); 14 | expect(workflow).toHaveProperty('projectTypes'); 15 | expect(workflow).toHaveProperty('capabilities'); 16 | }); 17 | 18 | it('should have correct workflow name', () => { 19 | expect(workflow.name).toBe('iOS Simulator Development'); 20 | }); 21 | 22 | it('should have correct description', () => { 23 | expect(workflow.description).toBe( 24 | 'Complete iOS development workflow for both .xcodeproj and .xcworkspace files targeting simulators. Build, test, deploy, and interact with iOS apps on simulators.', 25 | ); 26 | }); 27 | 28 | it('should have correct platforms array', () => { 29 | expect(workflow.platforms).toEqual(['iOS']); 30 | }); 31 | 32 | it('should have correct targets array', () => { 33 | expect(workflow.targets).toEqual(['simulator']); 34 | }); 35 | 36 | it('should have correct projectTypes array', () => { 37 | expect(workflow.projectTypes).toEqual(['project', 'workspace']); 38 | }); 39 | 40 | it('should have correct capabilities array', () => { 41 | expect(workflow.capabilities).toEqual(['build', 'test', 'deploy', 'debug', 'ui-automation']); 42 | }); 43 | }); 44 | 45 | describe('Workflow Validation', () => { 46 | it('should have valid string properties', () => { 47 | expect(typeof workflow.name).toBe('string'); 48 | expect(typeof workflow.description).toBe('string'); 49 | expect(workflow.name.length).toBeGreaterThan(0); 50 | expect(workflow.description.length).toBeGreaterThan(0); 51 | }); 52 | 53 | it('should have valid array properties', () => { 54 | expect(Array.isArray(workflow.platforms)).toBe(true); 55 | expect(Array.isArray(workflow.targets)).toBe(true); 56 | expect(Array.isArray(workflow.projectTypes)).toBe(true); 57 | expect(Array.isArray(workflow.capabilities)).toBe(true); 58 | 59 | expect(workflow.platforms.length).toBeGreaterThan(0); 60 | expect(workflow.targets.length).toBeGreaterThan(0); 61 | expect(workflow.projectTypes.length).toBeGreaterThan(0); 62 | expect(workflow.capabilities.length).toBeGreaterThan(0); 63 | }); 64 | 65 | it('should contain expected platform values', () => { 66 | expect(workflow.platforms).toContain('iOS'); 67 | }); 68 | 69 | it('should contain expected target values', () => { 70 | expect(workflow.targets).toContain('simulator'); 71 | }); 72 | 73 | it('should contain expected project type values', () => { 74 | expect(workflow.projectTypes).toContain('project'); 75 | }); 76 | 77 | it('should contain expected capability values', () => { 78 | expect(workflow.capabilities).toContain('build'); 79 | expect(workflow.capabilities).toContain('test'); 80 | expect(workflow.capabilities).toContain('deploy'); 81 | expect(workflow.capabilities).toContain('debug'); 82 | expect(workflow.capabilities).toContain('ui-automation'); 83 | }); 84 | }); 85 | }); 86 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/utilities/__tests__/index.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tests for utilities workflow metadata 3 | */ 4 | import { describe, it, expect } from 'vitest'; 5 | import { workflow } from '../index.ts'; 6 | 7 | describe('utilities workflow metadata', () => { 8 | describe('Workflow Structure', () => { 9 | it('should export workflow object with required properties', () => { 10 | expect(workflow).toHaveProperty('name'); 11 | expect(workflow).toHaveProperty('description'); 12 | expect(workflow).toHaveProperty('platforms'); 13 | expect(workflow).toHaveProperty('targets'); 14 | expect(workflow).toHaveProperty('projectTypes'); 15 | expect(workflow).toHaveProperty('capabilities'); 16 | }); 17 | 18 | it('should have correct workflow name', () => { 19 | expect(workflow.name).toBe('Project Utilities'); 20 | }); 21 | 22 | it('should have correct description', () => { 23 | expect(workflow.description).toBe( 24 | 'Essential project maintenance utilities for cleaning and managing existing projects. Provides clean operations for both .xcodeproj and .xcworkspace files.', 25 | ); 26 | }); 27 | 28 | it('should have correct platforms array', () => { 29 | expect(workflow.platforms).toEqual(['iOS', 'macOS']); 30 | }); 31 | 32 | it('should have correct targets array', () => { 33 | expect(workflow.targets).toEqual(['simulator', 'device', 'mac']); 34 | }); 35 | 36 | it('should have correct projectTypes array', () => { 37 | expect(workflow.projectTypes).toEqual(['project', 'workspace']); 38 | }); 39 | 40 | it('should have correct capabilities array', () => { 41 | expect(workflow.capabilities).toEqual(['project-cleaning', 'project-maintenance']); 42 | }); 43 | }); 44 | 45 | describe('Workflow Validation', () => { 46 | it('should have valid string properties', () => { 47 | expect(typeof workflow.name).toBe('string'); 48 | expect(typeof workflow.description).toBe('string'); 49 | expect(workflow.name.length).toBeGreaterThan(0); 50 | expect(workflow.description.length).toBeGreaterThan(0); 51 | }); 52 | 53 | it('should have valid array properties', () => { 54 | expect(Array.isArray(workflow.platforms)).toBe(true); 55 | expect(Array.isArray(workflow.targets)).toBe(true); 56 | expect(Array.isArray(workflow.projectTypes)).toBe(true); 57 | expect(Array.isArray(workflow.capabilities)).toBe(true); 58 | 59 | expect(workflow.platforms.length).toBeGreaterThan(0); 60 | expect(workflow.targets.length).toBeGreaterThan(0); 61 | expect(workflow.projectTypes.length).toBeGreaterThan(0); 62 | expect(workflow.capabilities.length).toBeGreaterThan(0); 63 | }); 64 | 65 | it('should contain expected platform values', () => { 66 | expect(workflow.platforms).toContain('iOS'); 67 | expect(workflow.platforms).toContain('macOS'); 68 | }); 69 | 70 | it('should contain expected target values', () => { 71 | expect(workflow.targets).toContain('simulator'); 72 | expect(workflow.targets).toContain('device'); 73 | expect(workflow.targets).toContain('mac'); 74 | }); 75 | 76 | it('should contain expected project type values', () => { 77 | expect(workflow.projectTypes).toContain('project'); 78 | expect(workflow.projectTypes).toContain('workspace'); 79 | }); 80 | 81 | it('should contain expected capability values', () => { 82 | expect(workflow.capabilities).toContain('project-cleaning'); 83 | expect(workflow.capabilities).toContain('project-maintenance'); 84 | }); 85 | }); 86 | }); 87 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/device/__tests__/re-exports.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tests for device-project re-export files 3 | * These files re-export tools from device-workspace to avoid duplication 4 | */ 5 | import { describe, it, expect } from 'vitest'; 6 | 7 | // Import all re-export tools 8 | import launchAppDevice from '../launch_app_device.ts'; 9 | import stopAppDevice from '../stop_app_device.ts'; 10 | import listDevices from '../list_devices.ts'; 11 | import installAppDevice from '../install_app_device.ts'; 12 | 13 | describe('device-project re-exports', () => { 14 | describe('launch_app_device re-export', () => { 15 | it('should re-export launch_app_device tool correctly', () => { 16 | expect(launchAppDevice.name).toBe('launch_app_device'); 17 | expect(typeof launchAppDevice.handler).toBe('function'); 18 | expect(launchAppDevice.schema).toBeDefined(); 19 | expect(typeof launchAppDevice.description).toBe('string'); 20 | }); 21 | }); 22 | 23 | describe('stop_app_device re-export', () => { 24 | it('should re-export stop_app_device tool correctly', () => { 25 | expect(stopAppDevice.name).toBe('stop_app_device'); 26 | expect(typeof stopAppDevice.handler).toBe('function'); 27 | expect(stopAppDevice.schema).toBeDefined(); 28 | expect(typeof stopAppDevice.description).toBe('string'); 29 | }); 30 | }); 31 | 32 | describe('list_devices re-export', () => { 33 | it('should re-export list_devices tool correctly', () => { 34 | expect(listDevices.name).toBe('list_devices'); 35 | expect(typeof listDevices.handler).toBe('function'); 36 | expect(listDevices.schema).toBeDefined(); 37 | expect(typeof listDevices.description).toBe('string'); 38 | }); 39 | }); 40 | 41 | describe('install_app_device re-export', () => { 42 | it('should re-export install_app_device tool correctly', () => { 43 | expect(installAppDevice.name).toBe('install_app_device'); 44 | expect(typeof installAppDevice.handler).toBe('function'); 45 | expect(installAppDevice.schema).toBeDefined(); 46 | expect(typeof installAppDevice.description).toBe('string'); 47 | }); 48 | }); 49 | 50 | describe('All re-exports validation', () => { 51 | const reExports = [ 52 | { tool: launchAppDevice, name: 'launch_app_device' }, 53 | { tool: stopAppDevice, name: 'stop_app_device' }, 54 | { tool: listDevices, name: 'list_devices' }, 55 | { tool: installAppDevice, name: 'install_app_device' }, 56 | ]; 57 | 58 | it('should have all required tool properties', () => { 59 | reExports.forEach(({ tool, name }) => { 60 | expect(tool).toHaveProperty('name'); 61 | expect(tool).toHaveProperty('description'); 62 | expect(tool).toHaveProperty('schema'); 63 | expect(tool).toHaveProperty('handler'); 64 | expect(tool.name).toBe(name); 65 | }); 66 | }); 67 | 68 | it('should have callable handlers', () => { 69 | reExports.forEach(({ tool, name }) => { 70 | expect(typeof tool.handler).toBe('function'); 71 | expect(tool.handler.length).toBeGreaterThanOrEqual(0); 72 | }); 73 | }); 74 | 75 | it('should have valid schemas', () => { 76 | reExports.forEach(({ tool, name }) => { 77 | expect(tool.schema).toBeDefined(); 78 | expect(typeof tool.schema).toBe('object'); 79 | }); 80 | }); 81 | 82 | it('should have non-empty descriptions', () => { 83 | reExports.forEach(({ tool, name }) => { 84 | expect(typeof tool.description).toBe('string'); 85 | expect(tool.description.length).toBeGreaterThan(0); 86 | }); 87 | }); 88 | }); 89 | }); 90 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator-management/set_sim_appearance.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { ToolResponse } from '../../../types/common.ts'; 3 | import { log } from '../../../utils/logging/index.ts'; 4 | import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; 5 | import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; 6 | 7 | // Define schema as ZodObject 8 | const setSimAppearanceSchema = z.object({ 9 | simulatorUuid: z 10 | .string() 11 | .describe('UUID of the simulator to use (obtained from list_simulators)'), 12 | mode: z.enum(['dark', 'light']).describe('The appearance mode to set (either "dark" or "light")'), 13 | }); 14 | 15 | // Use z.infer for type safety 16 | type SetSimAppearanceParams = z.infer<typeof setSimAppearanceSchema>; 17 | 18 | // Helper function to execute simctl commands and handle responses 19 | async function executeSimctlCommandAndRespond( 20 | params: SetSimAppearanceParams, 21 | simctlSubCommand: string[], 22 | operationDescriptionForXcodeCommand: string, 23 | successMessage: string, 24 | failureMessagePrefix: string, 25 | operationLogContext: string, 26 | extraValidation?: () => ToolResponse | undefined, 27 | executor: CommandExecutor = getDefaultCommandExecutor(), 28 | ): Promise<ToolResponse> { 29 | if (extraValidation) { 30 | const validationResult = extraValidation(); 31 | if (validationResult) { 32 | return validationResult; 33 | } 34 | } 35 | 36 | try { 37 | const command = ['xcrun', 'simctl', ...simctlSubCommand]; 38 | const result = await executor(command, operationDescriptionForXcodeCommand, true, undefined); 39 | 40 | if (!result.success) { 41 | const fullFailureMessage = `${failureMessagePrefix}: ${result.error}`; 42 | log( 43 | 'error', 44 | `${fullFailureMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorUuid})`, 45 | ); 46 | return { 47 | content: [{ type: 'text', text: fullFailureMessage }], 48 | }; 49 | } 50 | 51 | log( 52 | 'info', 53 | `${successMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorUuid})`, 54 | ); 55 | return { 56 | content: [{ type: 'text', text: successMessage }], 57 | }; 58 | } catch (error) { 59 | const errorMessage = error instanceof Error ? error.message : String(error); 60 | const fullFailureMessage = `${failureMessagePrefix}: ${errorMessage}`; 61 | log( 62 | 'error', 63 | `Error during ${operationLogContext} for simulator ${params.simulatorUuid}: ${errorMessage}`, 64 | ); 65 | return { 66 | content: [{ type: 'text', text: fullFailureMessage }], 67 | }; 68 | } 69 | } 70 | 71 | export async function set_sim_appearanceLogic( 72 | params: SetSimAppearanceParams, 73 | executor: CommandExecutor, 74 | ): Promise<ToolResponse> { 75 | log('info', `Setting simulator ${params.simulatorUuid} appearance to ${params.mode} mode`); 76 | 77 | return executeSimctlCommandAndRespond( 78 | params, 79 | ['ui', params.simulatorUuid, 'appearance', params.mode], 80 | 'Set Simulator Appearance', 81 | `Successfully set simulator ${params.simulatorUuid} appearance to ${params.mode} mode`, 82 | 'Failed to set simulator appearance', 83 | 'set simulator appearance', 84 | undefined, 85 | executor, 86 | ); 87 | } 88 | 89 | export default { 90 | name: 'set_sim_appearance', 91 | description: 'Sets the appearance mode (dark/light) of an iOS simulator.', 92 | schema: setSimAppearanceSchema.shape, // MCP SDK compatibility 93 | handler: createTypedTool( 94 | setSimAppearanceSchema, 95 | set_sim_appearanceLogic, 96 | getDefaultCommandExecutor, 97 | ), 98 | }; 99 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator/install_app_sim.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { ToolResponse } from '../../../types/common.ts'; 3 | import { log } from '../../../utils/logging/index.ts'; 4 | import { validateFileExists } from '../../../utils/validation/index.ts'; 5 | import type { CommandExecutor, FileSystemExecutor } from '../../../utils/execution/index.ts'; 6 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; 7 | import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; 8 | 9 | const installAppSimSchemaObject = z.object({ 10 | simulatorId: z.string().describe('UUID of the simulator to target'), 11 | appPath: z.string().describe('Path to the .app bundle to install'), 12 | }); 13 | 14 | type InstallAppSimParams = z.infer<typeof installAppSimSchemaObject>; 15 | 16 | const publicSchemaObject = installAppSimSchemaObject.omit({ 17 | simulatorId: true, 18 | } as const); 19 | 20 | export async function install_app_simLogic( 21 | params: InstallAppSimParams, 22 | executor: CommandExecutor, 23 | fileSystem?: FileSystemExecutor, 24 | ): Promise<ToolResponse> { 25 | const appPathExistsValidation = validateFileExists(params.appPath, fileSystem); 26 | if (!appPathExistsValidation.isValid) { 27 | return appPathExistsValidation.errorResponse!; 28 | } 29 | 30 | log('info', `Starting xcrun simctl install request for simulator ${params.simulatorId}`); 31 | 32 | try { 33 | const command = ['xcrun', 'simctl', 'install', params.simulatorId, params.appPath]; 34 | const result = await executor(command, 'Install App in Simulator', true, undefined); 35 | 36 | if (!result.success) { 37 | return { 38 | content: [ 39 | { 40 | type: 'text', 41 | text: `Install app in simulator operation failed: ${result.error}`, 42 | }, 43 | ], 44 | }; 45 | } 46 | 47 | let bundleId = ''; 48 | try { 49 | const bundleIdResult = await executor( 50 | ['defaults', 'read', `${params.appPath}/Info`, 'CFBundleIdentifier'], 51 | 'Extract Bundle ID', 52 | false, 53 | undefined, 54 | ); 55 | if (bundleIdResult.success) { 56 | bundleId = bundleIdResult.output.trim(); 57 | } 58 | } catch (error) { 59 | log('warning', `Could not extract bundle ID from app: ${error}`); 60 | } 61 | 62 | return { 63 | content: [ 64 | { 65 | type: 'text', 66 | text: `App installed successfully in simulator ${params.simulatorId}`, 67 | }, 68 | { 69 | type: 'text', 70 | text: `Next Steps: 71 | 1. Open the Simulator app: open_sim({}) 72 | 2. Launch the app: launch_app_sim({ simulatorId: "${params.simulatorId}"${ 73 | bundleId ? `, bundleId: "${bundleId}"` : ', bundleId: "YOUR_APP_BUNDLE_ID"' 74 | } })`, 75 | }, 76 | ], 77 | }; 78 | } catch (error) { 79 | const errorMessage = error instanceof Error ? error.message : String(error); 80 | log('error', `Error during install app in simulator operation: ${errorMessage}`); 81 | return { 82 | content: [ 83 | { 84 | type: 'text', 85 | text: `Install app in simulator operation failed: ${errorMessage}`, 86 | }, 87 | ], 88 | }; 89 | } 90 | } 91 | 92 | export default { 93 | name: 'install_app_sim', 94 | description: 'Installs an app in an iOS simulator.', 95 | schema: publicSchemaObject.shape, 96 | handler: createSessionAwareTool<InstallAppSimParams>({ 97 | internalSchema: installAppSimSchemaObject, 98 | logicFunction: install_app_simLogic, 99 | getExecutor: getDefaultCommandExecutor, 100 | requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }], 101 | }), 102 | }; 103 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/project-scaffolding/__tests__/index.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tests for project-scaffolding workflow metadata 3 | */ 4 | import { describe, it, expect } from 'vitest'; 5 | import { workflow } from '../index.ts'; 6 | 7 | describe('project-scaffolding workflow metadata', () => { 8 | describe('Workflow Structure', () => { 9 | it('should export workflow object with required properties', () => { 10 | expect(workflow).toHaveProperty('name'); 11 | expect(workflow).toHaveProperty('description'); 12 | expect(workflow).toHaveProperty('platforms'); 13 | expect(workflow).toHaveProperty('targets'); 14 | expect(workflow).toHaveProperty('projectTypes'); 15 | expect(workflow).toHaveProperty('capabilities'); 16 | }); 17 | 18 | it('should have correct workflow name', () => { 19 | expect(workflow.name).toBe('Project Scaffolding'); 20 | }); 21 | 22 | it('should have correct description', () => { 23 | expect(workflow.description).toBe( 24 | 'Tools for creating new iOS and macOS projects from templates. Bootstrap new applications with best practices, standard configurations, and modern project structures.', 25 | ); 26 | }); 27 | 28 | it('should have correct platforms array', () => { 29 | expect(workflow.platforms).toEqual(['iOS', 'macOS']); 30 | }); 31 | 32 | it('should have correct targets array', () => { 33 | expect(workflow.targets).toEqual(['simulator', 'device', 'mac']); 34 | }); 35 | 36 | it('should have correct projectTypes array', () => { 37 | expect(workflow.projectTypes).toEqual(['project']); 38 | }); 39 | 40 | it('should have correct capabilities array', () => { 41 | expect(workflow.capabilities).toEqual([ 42 | 'project-creation', 43 | 'template-generation', 44 | 'project-initialization', 45 | ]); 46 | }); 47 | }); 48 | 49 | describe('Workflow Validation', () => { 50 | it('should have valid string properties', () => { 51 | expect(typeof workflow.name).toBe('string'); 52 | expect(typeof workflow.description).toBe('string'); 53 | expect(workflow.name.length).toBeGreaterThan(0); 54 | expect(workflow.description.length).toBeGreaterThan(0); 55 | }); 56 | 57 | it('should have valid array properties', () => { 58 | expect(Array.isArray(workflow.platforms)).toBe(true); 59 | expect(Array.isArray(workflow.targets)).toBe(true); 60 | expect(Array.isArray(workflow.projectTypes)).toBe(true); 61 | expect(Array.isArray(workflow.capabilities)).toBe(true); 62 | 63 | expect(workflow.platforms.length).toBeGreaterThan(0); 64 | expect(workflow.targets.length).toBeGreaterThan(0); 65 | expect(workflow.projectTypes.length).toBeGreaterThan(0); 66 | expect(workflow.capabilities.length).toBeGreaterThan(0); 67 | }); 68 | 69 | it('should contain expected platform values', () => { 70 | expect(workflow.platforms).toContain('iOS'); 71 | expect(workflow.platforms).toContain('macOS'); 72 | }); 73 | 74 | it('should contain expected target values', () => { 75 | expect(workflow.targets).toContain('simulator'); 76 | expect(workflow.targets).toContain('device'); 77 | expect(workflow.targets).toContain('mac'); 78 | }); 79 | 80 | it('should contain expected project type values', () => { 81 | expect(workflow.projectTypes).toContain('project'); 82 | }); 83 | 84 | it('should contain expected capability values', () => { 85 | expect(workflow.capabilities).toContain('project-creation'); 86 | expect(workflow.capabilities).toContain('template-generation'); 87 | expect(workflow.capabilities).toContain('project-initialization'); 88 | }); 89 | }); 90 | }); 91 | ``` -------------------------------------------------------------------------------- /src/mcp/resources/__tests__/devices.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | import devicesResource, { devicesResourceLogic } from '../devices.ts'; 4 | import { createMockExecutor } from '../../../test-utils/mock-executors.ts'; 5 | 6 | describe('devices resource', () => { 7 | describe('Export Field Validation', () => { 8 | it('should export correct uri', () => { 9 | expect(devicesResource.uri).toBe('xcodebuildmcp://devices'); 10 | }); 11 | 12 | it('should export correct description', () => { 13 | expect(devicesResource.description).toBe( 14 | 'Connected physical Apple devices with their UUIDs, names, and connection status', 15 | ); 16 | }); 17 | 18 | it('should export correct mimeType', () => { 19 | expect(devicesResource.mimeType).toBe('text/plain'); 20 | }); 21 | 22 | it('should export handler function', () => { 23 | expect(typeof devicesResource.handler).toBe('function'); 24 | }); 25 | }); 26 | 27 | describe('Handler Functionality', () => { 28 | it('should handle successful device data retrieval with xctrace fallback', async () => { 29 | const mockExecutor = createMockExecutor({ 30 | success: true, 31 | output: `iPhone (12345-ABCDE-FGHIJ-67890) (13.0) 32 | iPad (98765-KLMNO-PQRST-43210) (14.0) 33 | My Device (11111-22222-33333-44444) (15.0)`, 34 | }); 35 | 36 | const result = await devicesResourceLogic(mockExecutor); 37 | 38 | expect(result.contents).toHaveLength(1); 39 | expect(result.contents[0].text).toContain('Device listing (xctrace output)'); 40 | expect(result.contents[0].text).toContain('iPhone'); 41 | expect(result.contents[0].text).toContain('iPad'); 42 | }); 43 | 44 | it('should handle command execution failure', async () => { 45 | const mockExecutor = createMockExecutor({ 46 | success: false, 47 | output: '', 48 | error: 'Command failed', 49 | }); 50 | 51 | const result = await devicesResourceLogic(mockExecutor); 52 | 53 | expect(result.contents).toHaveLength(1); 54 | expect(result.contents[0].text).toContain('Failed to list devices'); 55 | expect(result.contents[0].text).toContain('Command failed'); 56 | }); 57 | 58 | it('should handle spawn errors', async () => { 59 | const mockExecutor = createMockExecutor(new Error('spawn xcrun ENOENT')); 60 | 61 | const result = await devicesResourceLogic(mockExecutor); 62 | 63 | expect(result.contents).toHaveLength(1); 64 | expect(result.contents[0].text).toContain('Error retrieving device data'); 65 | expect(result.contents[0].text).toContain('spawn xcrun ENOENT'); 66 | }); 67 | 68 | it('should handle empty device data with xctrace fallback', async () => { 69 | const mockExecutor = createMockExecutor({ 70 | success: true, 71 | output: '', 72 | }); 73 | 74 | const result = await devicesResourceLogic(mockExecutor); 75 | 76 | expect(result.contents).toHaveLength(1); 77 | expect(result.contents[0].text).toContain('Device listing (xctrace output)'); 78 | expect(result.contents[0].text).toContain('Xcode 15 or later'); 79 | }); 80 | 81 | it('should handle device data with next steps guidance', async () => { 82 | const mockExecutor = createMockExecutor({ 83 | success: true, 84 | output: `iPhone 15 Pro (12345-ABCDE-FGHIJ-67890) (17.0)`, 85 | }); 86 | 87 | const result = await devicesResourceLogic(mockExecutor); 88 | 89 | expect(result.contents).toHaveLength(1); 90 | expect(result.contents[0].text).toContain('Device listing (xctrace output)'); 91 | expect(result.contents[0].text).toContain('iPhone 15 Pro'); 92 | }); 93 | }); 94 | }); 95 | ``` -------------------------------------------------------------------------------- /src/core/plugin-registry.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { PluginMeta, WorkflowGroup, WorkflowMeta } from './plugin-types.ts'; 2 | import { WORKFLOW_LOADERS, WorkflowName, WORKFLOW_METADATA } from './generated-plugins.ts'; 3 | 4 | export async function loadPlugins(): Promise<Map<string, PluginMeta>> { 5 | const plugins = new Map<string, PluginMeta>(); 6 | 7 | // Load all workflows and collect all their tools 8 | const workflowGroups = await loadWorkflowGroups(); 9 | 10 | for (const [, workflow] of workflowGroups.entries()) { 11 | for (const tool of workflow.tools) { 12 | if (tool?.name && typeof tool.handler === 'function') { 13 | plugins.set(tool.name, tool); 14 | } 15 | } 16 | } 17 | 18 | return plugins; 19 | } 20 | 21 | /** 22 | * Load workflow groups with metadata validation using generated loaders 23 | */ 24 | export async function loadWorkflowGroups(): Promise<Map<string, WorkflowGroup>> { 25 | const workflows = new Map<string, WorkflowGroup>(); 26 | 27 | for (const [workflowName, loader] of Object.entries(WORKFLOW_LOADERS)) { 28 | try { 29 | // Dynamic import with code-splitting 30 | const workflowModule = (await loader()) as { 31 | workflow?: WorkflowMeta; 32 | [key: string]: unknown; 33 | }; 34 | 35 | if (!workflowModule.workflow) { 36 | throw new Error(`Workflow metadata missing in ${workflowName}/index.js`); 37 | } 38 | 39 | // Validate required fields 40 | const workflowMeta = workflowModule.workflow as WorkflowMeta; 41 | if (!workflowMeta.name || typeof workflowMeta.name !== 'string') { 42 | throw new Error( 43 | `Invalid workflow.name in ${workflowName}/index.js: must be a non-empty string`, 44 | ); 45 | } 46 | 47 | if (!workflowMeta.description || typeof workflowMeta.description !== 'string') { 48 | throw new Error( 49 | `Invalid workflow.description in ${workflowName}/index.js: must be a non-empty string`, 50 | ); 51 | } 52 | 53 | workflows.set(workflowName, { 54 | workflow: workflowMeta, 55 | tools: await loadWorkflowTools(workflowModule), 56 | directoryName: workflowName, 57 | }); 58 | } catch (error) { 59 | throw new Error( 60 | `Failed to load workflow '${workflowName}': ${error instanceof Error ? error.message : 'Unknown error'}`, 61 | ); 62 | } 63 | } 64 | 65 | return workflows; 66 | } 67 | 68 | /** 69 | * Load workflow tools from the workflow module 70 | */ 71 | async function loadWorkflowTools(workflowModule: Record<string, unknown>): Promise<PluginMeta[]> { 72 | const tools: PluginMeta[] = []; 73 | 74 | // Load individual tool files from the workflow module 75 | for (const [key, value] of Object.entries(workflowModule)) { 76 | if (key !== 'workflow' && value && typeof value === 'object') { 77 | const tool = value as PluginMeta; 78 | if (tool.name && typeof tool.handler === 'function') { 79 | tools.push(tool); 80 | } 81 | } 82 | } 83 | 84 | return tools; 85 | } 86 | 87 | /** 88 | * Get workflow metadata by directory name using generated loaders 89 | */ 90 | export async function getWorkflowMetadata(directoryName: string): Promise<WorkflowMeta | null> { 91 | try { 92 | // First try to get from generated metadata (fast path) 93 | const metadata = WORKFLOW_METADATA[directoryName as WorkflowName]; 94 | if (metadata) { 95 | return metadata; 96 | } 97 | 98 | // Fall back to loading the actual module 99 | const loader = WORKFLOW_LOADERS[directoryName as WorkflowName]; 100 | if (loader) { 101 | const workflowModule = (await loader()) as { workflow?: WorkflowMeta }; 102 | return workflowModule.workflow ?? null; 103 | } 104 | 105 | return null; 106 | } catch { 107 | return null; 108 | } 109 | } 110 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator/__tests__/test_sim.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tests for test_sim plugin (session-aware version) 3 | * Follows CLAUDE.md guidance: dependency injection, no vi-mocks, literal validation. 4 | */ 5 | 6 | import { describe, it, expect, beforeEach } from 'vitest'; 7 | import { z } from 'zod'; 8 | import { sessionStore } from '../../../../utils/session-store.ts'; 9 | import testSim from '../test_sim.ts'; 10 | 11 | describe('test_sim tool', () => { 12 | beforeEach(() => { 13 | sessionStore.clear(); 14 | }); 15 | 16 | describe('Export Field Validation (Literal)', () => { 17 | it('should have correct name', () => { 18 | expect(testSim.name).toBe('test_sim'); 19 | }); 20 | 21 | it('should have concise description', () => { 22 | expect(testSim.description).toBe('Runs tests on an iOS simulator.'); 23 | }); 24 | 25 | it('should have handler function', () => { 26 | expect(typeof testSim.handler).toBe('function'); 27 | }); 28 | 29 | it('should expose only non-session fields in public schema', () => { 30 | const schema = z.object(testSim.schema); 31 | 32 | expect(schema.safeParse({}).success).toBe(true); 33 | expect( 34 | schema.safeParse({ 35 | derivedDataPath: '/tmp/derived', 36 | extraArgs: ['--quiet'], 37 | preferXcodebuild: true, 38 | testRunnerEnv: { FOO: 'BAR' }, 39 | }).success, 40 | ).toBe(true); 41 | 42 | expect(schema.safeParse({ derivedDataPath: 123 }).success).toBe(false); 43 | expect(schema.safeParse({ extraArgs: ['--ok', 42] }).success).toBe(false); 44 | expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false); 45 | expect(schema.safeParse({ testRunnerEnv: { FOO: 123 } }).success).toBe(false); 46 | 47 | const schemaKeys = Object.keys(testSim.schema).sort(); 48 | expect(schemaKeys).toEqual( 49 | ['derivedDataPath', 'extraArgs', 'preferXcodebuild', 'testRunnerEnv'].sort(), 50 | ); 51 | }); 52 | }); 53 | 54 | describe('Handler Requirements', () => { 55 | it('should require scheme when not provided', async () => { 56 | const result = await testSim.handler({}); 57 | 58 | expect(result.isError).toBe(true); 59 | expect(result.content[0].text).toContain('scheme is required'); 60 | }); 61 | 62 | it('should require project or workspace when scheme default exists', async () => { 63 | sessionStore.setDefaults({ scheme: 'MyScheme' }); 64 | 65 | const result = await testSim.handler({}); 66 | 67 | expect(result.isError).toBe(true); 68 | expect(result.content[0].text).toContain('Provide a project or workspace'); 69 | }); 70 | 71 | it('should require simulator identifier when scheme and project defaults exist', async () => { 72 | sessionStore.setDefaults({ 73 | scheme: 'MyScheme', 74 | projectPath: '/path/to/project.xcodeproj', 75 | }); 76 | 77 | const result = await testSim.handler({}); 78 | 79 | expect(result.isError).toBe(true); 80 | expect(result.content[0].text).toContain('Provide simulatorId or simulatorName'); 81 | }); 82 | 83 | it('should error when both simulatorId and simulatorName provided explicitly', async () => { 84 | sessionStore.setDefaults({ 85 | scheme: 'MyScheme', 86 | workspacePath: '/path/to/workspace.xcworkspace', 87 | }); 88 | 89 | const result = await testSim.handler({ 90 | simulatorId: 'SIM-UUID', 91 | simulatorName: 'iPhone 16', 92 | }); 93 | 94 | expect(result.isError).toBe(true); 95 | expect(result.content[0].text).toContain('Mutually exclusive parameters provided'); 96 | expect(result.content[0].text).toContain('simulatorId'); 97 | expect(result.content[0].text).toContain('simulatorName'); 98 | }); 99 | }); 100 | }); 101 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/swift-package/swift_package_stop.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.ts'; 3 | import { getProcess, removeProcess, type ProcessInfo } from './active-processes.ts'; 4 | import { ToolResponse } from '../../../types/common.ts'; 5 | 6 | // Define schema as ZodObject 7 | const swiftPackageStopSchema = z.object({ 8 | pid: z.number().describe('Process ID (PID) of the running executable'), 9 | }); 10 | 11 | // Use z.infer for type safety 12 | type SwiftPackageStopParams = z.infer<typeof swiftPackageStopSchema>; 13 | 14 | /** 15 | * Process manager interface for dependency injection 16 | */ 17 | export interface ProcessManager { 18 | getProcess: (pid: number) => ProcessInfo | undefined; 19 | removeProcess: (pid: number) => boolean; 20 | } 21 | 22 | /** 23 | * Default process manager implementation 24 | */ 25 | const defaultProcessManager: ProcessManager = { 26 | getProcess, 27 | removeProcess, 28 | }; 29 | 30 | /** 31 | * Get the default process manager instance 32 | */ 33 | export function getDefaultProcessManager(): ProcessManager { 34 | return defaultProcessManager; 35 | } 36 | 37 | /** 38 | * Create a mock process manager for testing 39 | */ 40 | export function createMockProcessManager(overrides?: Partial<ProcessManager>): ProcessManager { 41 | return { 42 | getProcess: () => undefined, 43 | removeProcess: () => true, 44 | ...overrides, 45 | }; 46 | } 47 | 48 | /** 49 | * Business logic for stopping a Swift Package executable 50 | */ 51 | export async function swift_package_stopLogic( 52 | params: SwiftPackageStopParams, 53 | processManager: ProcessManager = getDefaultProcessManager(), 54 | timeout: number = 5000, 55 | ): Promise<ToolResponse> { 56 | const processInfo = processManager.getProcess(params.pid); 57 | if (!processInfo) { 58 | return createTextResponse( 59 | `⚠️ No running process found with PID ${params.pid}. Use swift_package_run to check active processes.`, 60 | true, 61 | ); 62 | } 63 | 64 | try { 65 | processInfo.process.kill('SIGTERM'); 66 | 67 | // Give it time to terminate gracefully (configurable for testing) 68 | await new Promise((resolve) => { 69 | let terminated = false; 70 | 71 | processInfo.process.on('exit', () => { 72 | terminated = true; 73 | resolve(true); 74 | }); 75 | 76 | setTimeout(() => { 77 | if (!terminated) { 78 | processInfo.process.kill('SIGKILL'); 79 | } 80 | resolve(true); 81 | }, timeout); 82 | }); 83 | 84 | processManager.removeProcess(params.pid); 85 | 86 | return { 87 | content: [ 88 | { 89 | type: 'text', 90 | text: `✅ Stopped executable (was running since ${processInfo.startedAt.toISOString()})`, 91 | }, 92 | { 93 | type: 'text', 94 | text: `💡 Process terminated. You can now run swift_package_run again if needed.`, 95 | }, 96 | ], 97 | }; 98 | } catch (error) { 99 | const message = error instanceof Error ? error.message : String(error); 100 | return createErrorResponse('Failed to stop process', message); 101 | } 102 | } 103 | 104 | export default { 105 | name: 'swift_package_stop', 106 | description: 'Stops a running Swift Package executable started with swift_package_run', 107 | schema: swiftPackageStopSchema.shape, // MCP SDK compatibility 108 | async handler(args: Record<string, unknown>): Promise<ToolResponse> { 109 | // Validate parameters using Zod 110 | const parseResult = swiftPackageStopSchema.safeParse(args); 111 | if (!parseResult.success) { 112 | return createErrorResponse( 113 | 'Parameter validation failed', 114 | parseResult.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '), 115 | ); 116 | } 117 | 118 | return swift_package_stopLogic(parseResult.data); 119 | }, 120 | }; 121 | ``` -------------------------------------------------------------------------------- /src/core/resources.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Resource Management - MCP Resource handlers and URI management 3 | * 4 | * This module manages MCP resources, providing a unified interface for exposing 5 | * data through the Model Context Protocol resource system. Resources allow clients 6 | * to access data via URI references without requiring tool calls. 7 | * 8 | * Responsibilities: 9 | * - Loading resources from the plugin-based resource system 10 | * - Managing resource registration with the MCP server 11 | * - Providing fallback compatibility for clients without resource support 12 | */ 13 | 14 | import { McpServer } from '@camsoft/mcp-sdk/server/mcp.js'; 15 | import { ReadResourceResult } from '@camsoft/mcp-sdk/types.js'; 16 | import { log } from '../utils/logging/index.ts'; 17 | import type { CommandExecutor } from '../utils/execution/index.ts'; 18 | import { RESOURCE_LOADERS } from './generated-resources.ts'; 19 | 20 | /** 21 | * Resource metadata interface 22 | */ 23 | export interface ResourceMeta { 24 | uri: string; 25 | name: string; 26 | description: string; 27 | mimeType: string; 28 | handler: ( 29 | uri: URL, 30 | executor?: CommandExecutor, 31 | ) => Promise<{ 32 | contents: Array<{ text: string }>; 33 | }>; 34 | } 35 | 36 | /** 37 | * Load all resources using generated loaders 38 | * @returns Map of resource URI to resource metadata 39 | */ 40 | export async function loadResources(): Promise<Map<string, ResourceMeta>> { 41 | const resources = new Map<string, ResourceMeta>(); 42 | 43 | for (const [resourceName, loader] of Object.entries(RESOURCE_LOADERS)) { 44 | try { 45 | const resource = (await loader()) as ResourceMeta; 46 | 47 | if (!resource.uri || !resource.handler || typeof resource.handler !== 'function') { 48 | throw new Error(`Invalid resource structure for ${resourceName}`); 49 | } 50 | 51 | resources.set(resource.uri, resource); 52 | log('info', `Loaded resource: ${resourceName} (${resource.uri})`); 53 | } catch (error) { 54 | log( 55 | 'error', 56 | `Failed to load resource ${resourceName}: ${error instanceof Error ? error.message : String(error)}`, 57 | ); 58 | } 59 | } 60 | 61 | return resources; 62 | } 63 | 64 | /** 65 | * Register all resources with the MCP server if client supports resources 66 | * @param server The MCP server instance 67 | * @returns true if resources were registered, false if skipped due to client limitations 68 | */ 69 | export async function registerResources(server: McpServer): Promise<boolean> { 70 | const resources = await loadResources(); 71 | 72 | for (const [uri, resource] of Array.from(resources)) { 73 | // Create a handler wrapper that matches ReadResourceCallback signature 74 | const readCallback = async (resourceUri: URL): Promise<ReadResourceResult> => { 75 | const result = await resource.handler(resourceUri); 76 | // Transform the content to match MCP SDK expectations 77 | return { 78 | contents: result.contents.map((content) => ({ 79 | uri: resourceUri.toString(), 80 | text: content.text, 81 | mimeType: resource.mimeType, 82 | })), 83 | }; 84 | }; 85 | 86 | server.resource( 87 | resource.name, 88 | uri, 89 | { 90 | mimeType: resource.mimeType, 91 | title: resource.description, 92 | }, 93 | readCallback, 94 | ); 95 | 96 | log('info', `Registered resource: ${resource.name} at ${uri}`); 97 | } 98 | 99 | log('info', `Registered ${resources.size} resources`); 100 | return true; 101 | } 102 | 103 | /** 104 | * Get all available resource URIs 105 | * @returns Array of resource URI strings 106 | */ 107 | export async function getAvailableResources(): Promise<string[]> { 108 | const resources = await loadResources(); 109 | return Array.from(resources.keys()); 110 | } 111 | ``` -------------------------------------------------------------------------------- /example_projects/iOS_Calculator/CalculatorAppPackage/Sources/CalculatorAppFeature/ContentView.swift: -------------------------------------------------------------------------------- ```swift 1 | import SwiftUI 2 | 3 | public struct ContentView: View { 4 | @State private var calculatorService = CalculatorService() 5 | @State private var backgroundGradient = BackgroundState.normal 6 | 7 | private var inputHandler: CalculatorInputHandler { 8 | CalculatorInputHandler(service: calculatorService) 9 | } 10 | 11 | public var body: some View { 12 | GeometryReader { geometry in 13 | ZStack { 14 | // Dynamic gradient background 15 | AnimatedBackground(backgroundGradient: backgroundGradient) 16 | 17 | VStack(spacing: 0) { 18 | Spacer() 19 | 20 | // Display Section 21 | CalculatorDisplay( 22 | expressionDisplay: calculatorService.expressionDisplay, 23 | display: calculatorService.display, 24 | onDeleteLastDigit: { 25 | inputHandler.deleteLastDigit() 26 | } 27 | ) 28 | 29 | // Button Grid 30 | LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 12), count: 4), spacing: 12) { 31 | ForEach(calculatorButtons, id: \.self) { button in 32 | CalculatorButton( 33 | title: button, 34 | buttonType: buttonType(for: button), 35 | isWideButton: button == "0" 36 | ) { 37 | handleButtonPress(button) 38 | } 39 | } 40 | } 41 | .padding(.horizontal, 20) 42 | .padding(.bottom, max(geometry.safeAreaInsets.bottom, 20)) 43 | } 44 | } 45 | } 46 | } 47 | 48 | // Calculator button layout (proper grid with = button in correct position) 49 | private var calculatorButtons: [String] { 50 | [ 51 | "C", "±", "%", "÷", 52 | "7", "8", "9", "×", 53 | "4", "5", "6", "-", 54 | "1", "2", "3", "+", 55 | "", "0", ".", "=" 56 | ] 57 | } 58 | 59 | private func buttonType(for button: String) -> CalculatorButtonType { 60 | switch button { 61 | case "C", "±", "%": 62 | return .function 63 | case "÷", "×", "-", "+", "=": 64 | return .operation 65 | case "": 66 | return .hidden 67 | default: 68 | return .number 69 | } 70 | } 71 | 72 | private func handleButtonPress(_ button: String) { 73 | // Process input through the input handler 74 | inputHandler.handleInput(button) 75 | 76 | // Handle background state changes with modern animation 77 | withAnimation(.easeInOut(duration: 0.3)) { 78 | if button == "=" { 79 | backgroundGradient = calculatorService.hasError ? .error : .calculated 80 | 81 | // Reset to normal after a delay using structured concurrency 82 | Task { 83 | try await Task.sleep(for: .seconds(1.5)) 84 | await MainActor.run { 85 | withAnimation(.easeInOut(duration: 0.5)) { 86 | backgroundGradient = .normal 87 | } 88 | } 89 | } 90 | } else if button == "C" { 91 | backgroundGradient = .normal 92 | } 93 | } 94 | } 95 | 96 | public init() {} 97 | } 98 | 99 | 100 | #Preview { 101 | ContentView() 102 | } 103 | ``` -------------------------------------------------------------------------------- /src/utils/xcode.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Xcode Utilities - Core infrastructure for interacting with Xcode tools 3 | * 4 | * This utility module provides the foundation for all Xcode interactions across the codebase. 5 | * It offers platform-specific utilities, and common functionality that can be used by any module 6 | * requiring Xcode tool integration. 7 | * 8 | * Responsibilities: 9 | * - Constructing platform-specific destination strings (constructDestinationString) 10 | * 11 | * This file serves as the foundation layer for more specialized utilities like build-utils.ts, 12 | * which build upon these core functions to provide higher-level abstractions. 13 | */ 14 | 15 | import { log } from './logger.ts'; 16 | import { XcodePlatform } from '../types/common.ts'; 17 | 18 | // Re-export XcodePlatform for use in other modules 19 | export { XcodePlatform }; 20 | 21 | /** 22 | * Constructs a destination string for xcodebuild from platform and simulator parameters 23 | * @param platform The target platform 24 | * @param simulatorName Optional simulator name 25 | * @param simulatorId Optional simulator UUID 26 | * @param useLatest Whether to use the latest simulator version (primarily for named simulators) 27 | * @param arch Optional architecture for macOS builds (arm64 or x86_64) 28 | * @returns Properly formatted destination string for xcodebuild 29 | */ 30 | export function constructDestinationString( 31 | platform: XcodePlatform, 32 | simulatorName?: string, 33 | simulatorId?: string, 34 | useLatest: boolean = true, 35 | arch?: string, 36 | ): string { 37 | const isSimulatorPlatform = [ 38 | XcodePlatform.iOSSimulator, 39 | XcodePlatform.watchOSSimulator, 40 | XcodePlatform.tvOSSimulator, 41 | XcodePlatform.visionOSSimulator, 42 | ].includes(platform); 43 | 44 | // If ID is provided for a simulator, it takes precedence and uniquely identifies it. 45 | if (isSimulatorPlatform && simulatorId) { 46 | return `platform=${platform},id=${simulatorId}`; 47 | } 48 | 49 | // If name is provided for a simulator 50 | if (isSimulatorPlatform && simulatorName) { 51 | return `platform=${platform},name=${simulatorName}${useLatest ? ',OS=latest' : ''}`; 52 | } 53 | 54 | // If it's a simulator platform but neither ID nor name is provided (should be prevented by callers now) 55 | if (isSimulatorPlatform && !simulatorId && !simulatorName) { 56 | // Throw error as specific simulator is needed unless it's a generic build action 57 | // Allow fallback for generic simulator builds if needed, but generally require specifics for build/run 58 | log( 59 | 'warning', 60 | `Constructing generic destination for ${platform} without name or ID. This might not be specific enough.`, 61 | ); 62 | // Example: return `platform=${platform},name=Any ${platform} Device`; // Or similar generic target 63 | throw new Error(`Simulator name or ID is required for specific ${platform} operations`); 64 | } 65 | 66 | // Handle non-simulator platforms 67 | switch (platform) { 68 | case XcodePlatform.macOS: 69 | return arch ? `platform=macOS,arch=${arch}` : 'platform=macOS'; 70 | case XcodePlatform.iOS: 71 | return 'generic/platform=iOS'; 72 | case XcodePlatform.watchOS: 73 | return 'generic/platform=watchOS'; 74 | case XcodePlatform.tvOS: 75 | return 'generic/platform=tvOS'; 76 | case XcodePlatform.visionOS: 77 | return 'generic/platform=visionOS'; 78 | // No default needed as enum covers all cases unless extended 79 | // default: 80 | // throw new Error(`Unsupported platform for destination string: ${platform}`); 81 | } 82 | // Fallback just in case (shouldn't be reached with enum) 83 | log('error', `Reached unexpected point in constructDestinationString for platform: ${platform}`); 84 | return `platform=${platform}`; 85 | } 86 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/swift-package/swift_package_test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import path from 'node:path'; 3 | import type { CommandExecutor } from '../../../utils/execution/index.ts'; 4 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; 5 | import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.ts'; 6 | import { log } from '../../../utils/logging/index.ts'; 7 | import { ToolResponse } from '../../../types/common.ts'; 8 | import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; 9 | 10 | // Define schema as ZodObject 11 | const swiftPackageTestSchema = z.object({ 12 | packagePath: z.string().describe('Path to the Swift package root (Required)'), 13 | testProduct: z.string().optional().describe('Optional specific test product to run'), 14 | filter: z.string().optional().describe('Filter tests by name (regex pattern)'), 15 | configuration: z 16 | .enum(['debug', 'release']) 17 | .optional() 18 | .describe('Swift package configuration (debug, release)'), 19 | parallel: z.boolean().optional().describe('Run tests in parallel (default: true)'), 20 | showCodecov: z.boolean().optional().describe('Show code coverage (default: false)'), 21 | parseAsLibrary: z 22 | .boolean() 23 | .optional() 24 | .describe('Add -parse-as-library flag for @main support (default: false)'), 25 | }); 26 | 27 | // Use z.infer for type safety 28 | type SwiftPackageTestParams = z.infer<typeof swiftPackageTestSchema>; 29 | 30 | export async function swift_package_testLogic( 31 | params: SwiftPackageTestParams, 32 | executor: CommandExecutor, 33 | ): Promise<ToolResponse> { 34 | const resolvedPath = path.resolve(params.packagePath); 35 | const swiftArgs = ['test', '--package-path', resolvedPath]; 36 | 37 | if (params.configuration && params.configuration.toLowerCase() === 'release') { 38 | swiftArgs.push('-c', 'release'); 39 | } else if (params.configuration && params.configuration.toLowerCase() !== 'debug') { 40 | return createTextResponse("Invalid configuration. Use 'debug' or 'release'.", true); 41 | } 42 | 43 | if (params.testProduct) { 44 | swiftArgs.push('--test-product', params.testProduct); 45 | } 46 | 47 | if (params.filter) { 48 | swiftArgs.push('--filter', params.filter); 49 | } 50 | 51 | if (params.parallel === false) { 52 | swiftArgs.push('--no-parallel'); 53 | } 54 | 55 | if (params.showCodecov) { 56 | swiftArgs.push('--show-code-coverage'); 57 | } 58 | 59 | if (params.parseAsLibrary) { 60 | swiftArgs.push('-Xswiftc', '-parse-as-library'); 61 | } 62 | 63 | log('info', `Running swift ${swiftArgs.join(' ')}`); 64 | try { 65 | const result = await executor(['swift', ...swiftArgs], 'Swift Package Test', true, undefined); 66 | if (!result.success) { 67 | const errorMessage = result.error ?? result.output ?? 'Unknown error'; 68 | return createErrorResponse('Swift package tests failed', errorMessage); 69 | } 70 | 71 | return { 72 | content: [ 73 | { type: 'text', text: '✅ Swift package tests completed.' }, 74 | { 75 | type: 'text', 76 | text: '💡 Next: Execute your app with swift_package_run if tests passed', 77 | }, 78 | { type: 'text', text: result.output }, 79 | ], 80 | isError: false, 81 | }; 82 | } catch (error) { 83 | const message = error instanceof Error ? error.message : String(error); 84 | log('error', `Swift package test failed: ${message}`); 85 | return createErrorResponse('Failed to execute swift test', message); 86 | } 87 | } 88 | 89 | export default { 90 | name: 'swift_package_test', 91 | description: 'Runs tests for a Swift Package with swift test', 92 | schema: swiftPackageTestSchema.shape, // MCP SDK compatibility 93 | handler: createTypedTool( 94 | swiftPackageTestSchema, 95 | swift_package_testLogic, 96 | getDefaultCommandExecutor, 97 | ), 98 | }; 99 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/device/__tests__/index.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tests for device-project workflow metadata 3 | */ 4 | import { describe, it, expect } from 'vitest'; 5 | import { workflow } from '../index.ts'; 6 | 7 | describe('device-project workflow metadata', () => { 8 | describe('Workflow Structure', () => { 9 | it('should export workflow object with required properties', () => { 10 | expect(workflow).toHaveProperty('name'); 11 | expect(workflow).toHaveProperty('description'); 12 | expect(workflow).toHaveProperty('platforms'); 13 | expect(workflow).toHaveProperty('targets'); 14 | expect(workflow).toHaveProperty('projectTypes'); 15 | expect(workflow).toHaveProperty('capabilities'); 16 | }); 17 | 18 | it('should have correct workflow name', () => { 19 | expect(workflow.name).toBe('iOS Device Development'); 20 | }); 21 | 22 | it('should have correct description', () => { 23 | expect(workflow.description).toBe( 24 | 'Complete iOS development workflow for both .xcodeproj and .xcworkspace files targeting physical devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). Build, test, deploy, and debug apps on real hardware.', 25 | ); 26 | }); 27 | 28 | it('should have correct platforms array', () => { 29 | expect(workflow.platforms).toEqual(['iOS', 'watchOS', 'tvOS', 'visionOS']); 30 | }); 31 | 32 | it('should have correct targets array', () => { 33 | expect(workflow.targets).toEqual(['device']); 34 | }); 35 | 36 | it('should have correct projectTypes array', () => { 37 | expect(workflow.projectTypes).toEqual(['project', 'workspace']); 38 | }); 39 | 40 | it('should have correct capabilities array', () => { 41 | expect(workflow.capabilities).toEqual([ 42 | 'build', 43 | 'test', 44 | 'deploy', 45 | 'debug', 46 | 'log-capture', 47 | 'device-management', 48 | ]); 49 | }); 50 | }); 51 | 52 | describe('Workflow Validation', () => { 53 | it('should have valid string properties', () => { 54 | expect(typeof workflow.name).toBe('string'); 55 | expect(typeof workflow.description).toBe('string'); 56 | expect(workflow.name.length).toBeGreaterThan(0); 57 | expect(workflow.description.length).toBeGreaterThan(0); 58 | }); 59 | 60 | it('should have valid array properties', () => { 61 | expect(Array.isArray(workflow.platforms)).toBe(true); 62 | expect(Array.isArray(workflow.targets)).toBe(true); 63 | expect(Array.isArray(workflow.projectTypes)).toBe(true); 64 | expect(Array.isArray(workflow.capabilities)).toBe(true); 65 | 66 | expect(workflow.platforms.length).toBeGreaterThan(0); 67 | expect(workflow.targets.length).toBeGreaterThan(0); 68 | expect(workflow.projectTypes.length).toBeGreaterThan(0); 69 | expect(workflow.capabilities.length).toBeGreaterThan(0); 70 | }); 71 | 72 | it('should contain expected platform values', () => { 73 | expect(workflow.platforms).toContain('iOS'); 74 | expect(workflow.platforms).toContain('watchOS'); 75 | expect(workflow.platforms).toContain('tvOS'); 76 | expect(workflow.platforms).toContain('visionOS'); 77 | }); 78 | 79 | it('should contain expected target values', () => { 80 | expect(workflow.targets).toContain('device'); 81 | }); 82 | 83 | it('should contain expected project type values', () => { 84 | expect(workflow.projectTypes).toContain('project'); 85 | expect(workflow.projectTypes).toContain('workspace'); 86 | }); 87 | 88 | it('should contain expected capability values', () => { 89 | expect(workflow.capabilities).toContain('build'); 90 | expect(workflow.capabilities).toContain('test'); 91 | expect(workflow.capabilities).toContain('deploy'); 92 | expect(workflow.capabilities).toContain('debug'); 93 | expect(workflow.capabilities).toContain('log-capture'); 94 | expect(workflow.capabilities).toContain('device-management'); 95 | }); 96 | }); 97 | }); 98 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/swift-package/__tests__/index.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tests for swift-package workflow metadata 3 | */ 4 | import { describe, it, expect } from 'vitest'; 5 | import { workflow } from '../index.ts'; 6 | 7 | describe('swift-package workflow metadata', () => { 8 | describe('Workflow Structure', () => { 9 | it('should export workflow object with required properties', () => { 10 | expect(workflow).toHaveProperty('name'); 11 | expect(workflow).toHaveProperty('description'); 12 | expect(workflow).toHaveProperty('platforms'); 13 | expect(workflow).toHaveProperty('targets'); 14 | expect(workflow).toHaveProperty('projectTypes'); 15 | expect(workflow).toHaveProperty('capabilities'); 16 | }); 17 | 18 | it('should have correct workflow name', () => { 19 | expect(workflow.name).toBe('Swift Package Manager'); 20 | }); 21 | 22 | it('should have correct description', () => { 23 | expect(workflow.description).toBe( 24 | 'Swift Package Manager operations for building, testing, running, and managing Swift packages and dependencies. Complete SPM workflow support.', 25 | ); 26 | }); 27 | 28 | it('should have correct platforms array', () => { 29 | expect(workflow.platforms).toEqual(['iOS', 'macOS', 'watchOS', 'tvOS', 'visionOS', 'Linux']); 30 | }); 31 | 32 | it('should have correct targets array', () => { 33 | expect(workflow.targets).toEqual(['package']); 34 | }); 35 | 36 | it('should have correct projectTypes array', () => { 37 | expect(workflow.projectTypes).toEqual(['swift-package']); 38 | }); 39 | 40 | it('should have correct capabilities array', () => { 41 | expect(workflow.capabilities).toEqual([ 42 | 'build', 43 | 'test', 44 | 'run', 45 | 'clean', 46 | 'dependency-management', 47 | 'package-management', 48 | ]); 49 | }); 50 | }); 51 | 52 | describe('Workflow Validation', () => { 53 | it('should have valid string properties', () => { 54 | expect(typeof workflow.name).toBe('string'); 55 | expect(typeof workflow.description).toBe('string'); 56 | expect(workflow.name.length).toBeGreaterThan(0); 57 | expect(workflow.description.length).toBeGreaterThan(0); 58 | }); 59 | 60 | it('should have valid array properties', () => { 61 | expect(Array.isArray(workflow.platforms)).toBe(true); 62 | expect(Array.isArray(workflow.targets)).toBe(true); 63 | expect(Array.isArray(workflow.projectTypes)).toBe(true); 64 | expect(Array.isArray(workflow.capabilities)).toBe(true); 65 | 66 | expect(workflow.platforms.length).toBeGreaterThan(0); 67 | expect(workflow.targets.length).toBeGreaterThan(0); 68 | expect(workflow.projectTypes.length).toBeGreaterThan(0); 69 | expect(workflow.capabilities.length).toBeGreaterThan(0); 70 | }); 71 | 72 | it('should contain expected platform values', () => { 73 | expect(workflow.platforms).toContain('iOS'); 74 | expect(workflow.platforms).toContain('macOS'); 75 | expect(workflow.platforms).toContain('watchOS'); 76 | expect(workflow.platforms).toContain('tvOS'); 77 | expect(workflow.platforms).toContain('visionOS'); 78 | expect(workflow.platforms).toContain('Linux'); 79 | }); 80 | 81 | it('should contain expected target values', () => { 82 | expect(workflow.targets).toContain('package'); 83 | }); 84 | 85 | it('should contain expected project type values', () => { 86 | expect(workflow.projectTypes).toContain('swift-package'); 87 | }); 88 | 89 | it('should contain expected capability values', () => { 90 | expect(workflow.capabilities).toContain('build'); 91 | expect(workflow.capabilities).toContain('test'); 92 | expect(workflow.capabilities).toContain('run'); 93 | expect(workflow.capabilities).toContain('clean'); 94 | expect(workflow.capabilities).toContain('dependency-management'); 95 | expect(workflow.capabilities).toContain('package-management'); 96 | }); 97 | }); 98 | }); 99 | ``` -------------------------------------------------------------------------------- /src/utils/errors.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolResponse } from '../types/common.ts'; 2 | 3 | /** 4 | * Error Utilities - Type-safe error hierarchy for the application 5 | * 6 | * This utility module defines a structured error hierarchy for the application, 7 | * providing specialized error types for different failure scenarios. Using these 8 | * typed errors enables more precise error handling, improves debugging, and 9 | * provides better error messages to users. 10 | * 11 | * Responsibilities: 12 | * - Providing a base error class (XcodeBuildMCPError) for all application errors 13 | * - Defining specialized error subtypes for different error categories: 14 | * - ValidationError: Parameter validation failures 15 | * - SystemError: Underlying system/OS issues 16 | * - ConfigurationError: Application configuration problems 17 | * - SimulatorError: iOS simulator-specific failures 18 | * - AxeError: axe-specific errors 19 | * 20 | * The structured hierarchy allows error consumers to handle errors with the 21 | * appropriate level of specificity using instanceof checks or catch clauses. 22 | */ 23 | 24 | /** 25 | * Custom error types for XcodeBuildMCP 26 | */ 27 | 28 | /** 29 | * Base error class for XcodeBuildMCP errors 30 | */ 31 | export class XcodeBuildMCPError extends Error { 32 | constructor(message: string) { 33 | super(message); 34 | this.name = 'XcodeBuildMCPError'; 35 | // This is necessary for proper inheritance in TypeScript 36 | Object.setPrototypeOf(this, XcodeBuildMCPError.prototype); 37 | } 38 | } 39 | 40 | /** 41 | * Error thrown when validation of parameters fails 42 | */ 43 | export class ValidationError extends XcodeBuildMCPError { 44 | constructor( 45 | message: string, 46 | public paramName?: string, 47 | ) { 48 | super(message); 49 | this.name = 'ValidationError'; 50 | Object.setPrototypeOf(this, ValidationError.prototype); 51 | } 52 | } 53 | 54 | /** 55 | * Error thrown for system-level errors (file access, permissions, etc.) 56 | */ 57 | export class SystemError extends XcodeBuildMCPError { 58 | constructor( 59 | message: string, 60 | public originalError?: Error, 61 | ) { 62 | super(message); 63 | this.name = 'SystemError'; 64 | Object.setPrototypeOf(this, SystemError.prototype); 65 | } 66 | } 67 | 68 | /** 69 | * Error thrown for configuration issues 70 | */ 71 | export class ConfigurationError extends XcodeBuildMCPError { 72 | constructor(message: string) { 73 | super(message); 74 | this.name = 'ConfigurationError'; 75 | Object.setPrototypeOf(this, ConfigurationError.prototype); 76 | } 77 | } 78 | 79 | /** 80 | * Error thrown for simulator-specific errors 81 | */ 82 | export class SimulatorError extends XcodeBuildMCPError { 83 | constructor( 84 | message: string, 85 | public simulatorName?: string, 86 | public simulatorId?: string, 87 | ) { 88 | super(message); 89 | this.name = 'SimulatorError'; 90 | Object.setPrototypeOf(this, SimulatorError.prototype); 91 | } 92 | } 93 | 94 | /** 95 | * Error thrown for axe-specific errors 96 | */ 97 | export class AxeError extends XcodeBuildMCPError { 98 | constructor( 99 | message: string, 100 | public command?: string, // The axe command that failed 101 | public axeOutput?: string, // Output from axe 102 | public simulatorId?: string, 103 | ) { 104 | super(message); 105 | this.name = 'AxeError'; 106 | Object.setPrototypeOf(this, AxeError.prototype); 107 | } 108 | } 109 | 110 | // Helper to create a standard error response 111 | export function createErrorResponse(message: string, details?: string): ToolResponse { 112 | const detailText = details ? `\nDetails: ${details}` : ''; 113 | return { 114 | content: [ 115 | { 116 | type: 'text', 117 | text: `Error: ${message}${detailText}`, 118 | }, 119 | ], 120 | isError: true, 121 | }; 122 | } 123 | 124 | /** 125 | * Error class for missing dependencies 126 | */ 127 | export class DependencyError extends ConfigurationError { 128 | constructor( 129 | message: string, 130 | public details?: string, 131 | ) { 132 | super(message); 133 | this.name = 'DependencyError'; 134 | Object.setPrototypeOf(this, DependencyError.prototype); 135 | } 136 | } 137 | ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator-management/__tests__/reset_sim_location.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect } from 'vitest'; 2 | import { z } from 'zod'; 3 | import resetSimLocationPlugin, { reset_sim_locationLogic } from '../reset_sim_location.ts'; 4 | import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; 5 | 6 | describe('reset_sim_location plugin', () => { 7 | describe('Export Field Validation (Literal)', () => { 8 | it('should have correct name field', () => { 9 | expect(resetSimLocationPlugin.name).toBe('reset_sim_location'); 10 | }); 11 | 12 | it('should have correct description field', () => { 13 | expect(resetSimLocationPlugin.description).toBe( 14 | "Resets the simulator's location to default.", 15 | ); 16 | }); 17 | 18 | it('should have handler function', () => { 19 | expect(typeof resetSimLocationPlugin.handler).toBe('function'); 20 | }); 21 | 22 | it('should have correct schema validation', () => { 23 | const schema = z.object(resetSimLocationPlugin.schema); 24 | 25 | expect( 26 | schema.safeParse({ 27 | simulatorUuid: 'abc123', 28 | }).success, 29 | ).toBe(true); 30 | 31 | expect( 32 | schema.safeParse({ 33 | simulatorUuid: 123, 34 | }).success, 35 | ).toBe(false); 36 | 37 | expect(schema.safeParse({}).success).toBe(false); 38 | }); 39 | }); 40 | 41 | describe('Handler Behavior (Complete Literal Returns)', () => { 42 | it('should successfully reset simulator location', async () => { 43 | const mockExecutor = createMockExecutor({ 44 | success: true, 45 | output: 'Location reset successfully', 46 | }); 47 | 48 | const result = await reset_sim_locationLogic( 49 | { 50 | simulatorUuid: 'test-uuid-123', 51 | }, 52 | mockExecutor, 53 | ); 54 | 55 | expect(result).toEqual({ 56 | content: [ 57 | { 58 | type: 'text', 59 | text: 'Successfully reset simulator test-uuid-123 location.', 60 | }, 61 | ], 62 | }); 63 | }); 64 | 65 | it('should handle command failure', async () => { 66 | const mockExecutor = createMockExecutor({ 67 | success: false, 68 | error: 'Command failed', 69 | }); 70 | 71 | const result = await reset_sim_locationLogic( 72 | { 73 | simulatorUuid: 'test-uuid-123', 74 | }, 75 | mockExecutor, 76 | ); 77 | 78 | expect(result).toEqual({ 79 | content: [ 80 | { 81 | type: 'text', 82 | text: 'Failed to reset simulator location: Command failed', 83 | }, 84 | ], 85 | }); 86 | }); 87 | 88 | it('should handle exception during execution', async () => { 89 | const mockExecutor = createMockExecutor(new Error('Network error')); 90 | 91 | const result = await reset_sim_locationLogic( 92 | { 93 | simulatorUuid: 'test-uuid-123', 94 | }, 95 | mockExecutor, 96 | ); 97 | 98 | expect(result).toEqual({ 99 | content: [ 100 | { 101 | type: 'text', 102 | text: 'Failed to reset simulator location: Network error', 103 | }, 104 | ], 105 | }); 106 | }); 107 | 108 | it('should call correct command', async () => { 109 | let capturedCommand: string[] = []; 110 | let capturedLogPrefix: string | undefined; 111 | 112 | const mockExecutor = createMockExecutor({ 113 | success: true, 114 | output: 'Location reset successfully', 115 | }); 116 | 117 | // Create a wrapper to capture the command arguments 118 | const capturingExecutor = async (command: string[], logPrefix?: string) => { 119 | capturedCommand = command; 120 | capturedLogPrefix = logPrefix; 121 | return mockExecutor(command, logPrefix); 122 | }; 123 | 124 | await reset_sim_locationLogic( 125 | { 126 | simulatorUuid: 'test-uuid-123', 127 | }, 128 | capturingExecutor, 129 | ); 130 | 131 | expect(capturedCommand).toEqual(['xcrun', 'simctl', 'location', 'test-uuid-123', 'clear']); 132 | expect(capturedLogPrefix).toBe('Reset Simulator Location'); 133 | }); 134 | }); 135 | }); 136 | ``` -------------------------------------------------------------------------------- /src/utils/sentry.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Sentry instrumentation for XcodeBuildMCP 3 | * 4 | * This file initializes Sentry as early as possible in the application lifecycle. 5 | * It should be imported at the top of the main entry point file. 6 | */ 7 | 8 | import * as Sentry from '@sentry/node'; 9 | import { version } from '../version.ts'; 10 | import { execSync } from 'child_process'; 11 | 12 | // Inlined system info functions to avoid circular dependencies 13 | function getXcodeInfo(): { version: string; path: string; selectedXcode: string; error?: string } { 14 | try { 15 | const xcodebuildOutput = execSync('xcodebuild -version', { encoding: 'utf8' }).trim(); 16 | const version = xcodebuildOutput.split('\n').slice(0, 2).join(' - '); 17 | const path = execSync('xcode-select -p', { encoding: 'utf8' }).trim(); 18 | const selectedXcode = execSync('xcrun --find xcodebuild', { encoding: 'utf8' }).trim(); 19 | 20 | return { version, path, selectedXcode }; 21 | } catch (error) { 22 | return { 23 | version: 'Not available', 24 | path: 'Not available', 25 | selectedXcode: 'Not available', 26 | error: error instanceof Error ? error.message : String(error), 27 | }; 28 | } 29 | } 30 | 31 | function getEnvironmentVariables(): Record<string, string> { 32 | const relevantVars = [ 33 | 'INCREMENTAL_BUILDS_ENABLED', 34 | 'PATH', 35 | 'DEVELOPER_DIR', 36 | 'HOME', 37 | 'USER', 38 | 'TMPDIR', 39 | 'NODE_ENV', 40 | 'SENTRY_DISABLED', 41 | ]; 42 | 43 | const envVars: Record<string, string> = {}; 44 | relevantVars.forEach((varName) => { 45 | envVars[varName] = process.env[varName] ?? ''; 46 | }); 47 | 48 | Object.keys(process.env).forEach((key) => { 49 | if (key.startsWith('XCODEBUILDMCP_')) { 50 | envVars[key] = process.env[key] ?? ''; 51 | } 52 | }); 53 | 54 | return envVars; 55 | } 56 | 57 | function checkBinaryAvailability(binary: string): { available: boolean; version?: string } { 58 | try { 59 | execSync(`which ${binary}`, { stdio: 'ignore' }); 60 | } catch { 61 | return { available: false }; 62 | } 63 | 64 | let version: string | undefined; 65 | const versionCommands: Record<string, string> = { 66 | axe: 'axe --version', 67 | mise: 'mise --version', 68 | }; 69 | 70 | if (binary in versionCommands) { 71 | try { 72 | version = execSync(versionCommands[binary], { 73 | encoding: 'utf8', 74 | stdio: ['ignore', 'pipe', 'ignore'], 75 | }).trim(); 76 | } catch { 77 | // Version command failed, but binary exists 78 | } 79 | } 80 | 81 | return { available: true, version }; 82 | } 83 | 84 | Sentry.init({ 85 | dsn: 86 | process.env.SENTRY_DSN ?? 87 | 'https://798607831167c7b9fe2f2912f5d3c665@o4509258288332800.ingest.de.sentry.io/4509258293837904', 88 | 89 | // Setting this option to true will send default PII data to Sentry 90 | // For example, automatic IP address collection on events 91 | sendDefaultPii: true, 92 | 93 | // Set release version to match application version 94 | release: `xcodebuildmcp@${version}`, 95 | 96 | // Always report under production environment 97 | environment: 'production', 98 | 99 | // Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring 100 | // We recommend adjusting this value in production 101 | tracesSampleRate: 1.0, 102 | }); 103 | 104 | const axeAvailable = checkBinaryAvailability('axe'); 105 | const miseAvailable = checkBinaryAvailability('mise'); 106 | const envVars = getEnvironmentVariables(); 107 | const xcodeInfo = getXcodeInfo(); 108 | 109 | // Add additional context that might be helpful for debugging 110 | const tags: Record<string, string> = { 111 | nodeVersion: process.version, 112 | platform: process.platform, 113 | arch: process.arch, 114 | axeAvailable: axeAvailable.available ? 'true' : 'false', 115 | axeVersion: axeAvailable.version ?? 'Unknown', 116 | miseAvailable: miseAvailable.available ? 'true' : 'false', 117 | miseVersion: miseAvailable.version ?? 'Unknown', 118 | ...Object.fromEntries(Object.entries(envVars).map(([k, v]) => [`env_${k}`, v ?? ''])), 119 | xcodeVersion: xcodeInfo.version ?? 'Unknown', 120 | xcodePath: xcodeInfo.path ?? 'Unknown', 121 | }; 122 | 123 | Sentry.setTags(tags); 124 | ``` -------------------------------------------------------------------------------- /src/core/__tests__/resources.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, beforeEach } from 'vitest'; 2 | import { McpServer } from '@camsoft/mcp-sdk/server/mcp.js'; 3 | 4 | import { registerResources, getAvailableResources, loadResources } from '../resources.ts'; 5 | 6 | describe('resources', () => { 7 | let mockServer: McpServer; 8 | let registeredResources: Array<{ 9 | name: string; 10 | uri: string; 11 | metadata: { mimeType: string; title: string }; 12 | handler: any; 13 | }>; 14 | 15 | beforeEach(() => { 16 | registeredResources = []; 17 | // Create a mock MCP server using simple object structure 18 | mockServer = { 19 | resource: ( 20 | name: string, 21 | uri: string, 22 | metadata: { mimeType: string; title: string }, 23 | handler: any, 24 | ) => { 25 | registeredResources.push({ name, uri, metadata, handler }); 26 | }, 27 | } as unknown as McpServer; 28 | }); 29 | 30 | describe('Exports', () => { 31 | it('should export registerResources function', () => { 32 | expect(typeof registerResources).toBe('function'); 33 | }); 34 | 35 | it('should export getAvailableResources function', () => { 36 | expect(typeof getAvailableResources).toBe('function'); 37 | }); 38 | 39 | it('should export loadResources function', () => { 40 | expect(typeof loadResources).toBe('function'); 41 | }); 42 | }); 43 | 44 | describe('loadResources', () => { 45 | it('should load resources from generated loaders', async () => { 46 | const resources = await loadResources(); 47 | 48 | // Should have at least the simulators resource 49 | expect(resources.size).toBeGreaterThan(0); 50 | expect(resources.has('xcodebuildmcp://simulators')).toBe(true); 51 | }); 52 | 53 | it('should validate resource structure', async () => { 54 | const resources = await loadResources(); 55 | 56 | for (const [uri, resource] of resources) { 57 | expect(resource.uri).toBe(uri); 58 | expect(typeof resource.description).toBe('string'); 59 | expect(typeof resource.mimeType).toBe('string'); 60 | expect(typeof resource.handler).toBe('function'); 61 | } 62 | }); 63 | }); 64 | 65 | describe('registerResources', () => { 66 | it('should register all loaded resources with the server and return true', async () => { 67 | const result = await registerResources(mockServer); 68 | 69 | expect(result).toBe(true); 70 | 71 | // Should have registered at least one resource 72 | expect(registeredResources.length).toBeGreaterThan(0); 73 | 74 | // Check simulators resource was registered 75 | const simulatorsResource = registeredResources.find( 76 | (r) => r.uri === 'xcodebuildmcp://simulators', 77 | ); 78 | expect(typeof simulatorsResource?.handler).toBe('function'); 79 | expect(simulatorsResource?.metadata.title).toBe( 80 | 'Available iOS simulators with their UUIDs and states', 81 | ); 82 | expect(simulatorsResource?.metadata.mimeType).toBe('text/plain'); 83 | expect(simulatorsResource?.name).toBe('simulators'); 84 | }); 85 | 86 | it('should register resources with correct handlers', async () => { 87 | const result = await registerResources(mockServer); 88 | 89 | expect(result).toBe(true); 90 | 91 | const simulatorsResource = registeredResources.find( 92 | (r) => r.uri === 'xcodebuildmcp://simulators', 93 | ); 94 | expect(typeof simulatorsResource?.handler).toBe('function'); 95 | }); 96 | }); 97 | 98 | describe('getAvailableResources', () => { 99 | it('should return array of available resource URIs', async () => { 100 | const resources = await getAvailableResources(); 101 | 102 | expect(Array.isArray(resources)).toBe(true); 103 | expect(resources.length).toBeGreaterThan(0); 104 | expect(resources).toContain('xcodebuildmcp://simulators'); 105 | }); 106 | 107 | it('should return unique URIs', async () => { 108 | const resources = await getAvailableResources(); 109 | const uniqueResources = [...new Set(resources)]; 110 | 111 | expect(resources.length).toBe(uniqueResources.length); 112 | }); 113 | }); 114 | }); 115 | ```