This is page 1 of 5. Use http://codebase.md/stefan-nitu/mcp-xcode-server?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .claude │ └── settings.local.json ├── .github │ └── workflows │ └── ci.yml ├── .gitignore ├── .vscode │ └── settings.json ├── CLAUDE.md ├── CONTRIBUTING.md ├── docs │ ├── ARCHITECTURE.md │ ├── ERROR-HANDLING.md │ └── TESTING-PHILOSOPHY.md ├── examples │ └── screenshot-demo.js ├── jest.config.cjs ├── jest.e2e.config.cjs ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── scripts │ └── xcode-sync.swift ├── src │ ├── application │ │ └── ports │ │ ├── ArtifactPorts.ts │ │ ├── BuildPorts.ts │ │ ├── CommandPorts.ts │ │ ├── ConfigPorts.ts │ │ ├── LoggingPorts.ts │ │ ├── MappingPorts.ts │ │ ├── OutputFormatterPorts.ts │ │ ├── OutputParserPorts.ts │ │ └── SimulatorPorts.ts │ ├── cli.ts │ ├── config.ts │ ├── domain │ │ ├── errors │ │ │ └── DomainError.ts │ │ ├── services │ │ │ └── PlatformDetector.ts │ │ ├── shared │ │ │ └── Result.ts │ │ └── tests │ │ └── unit │ │ └── PlatformDetector.unit.test.ts │ ├── features │ │ ├── app-management │ │ │ ├── controllers │ │ │ │ └── InstallAppController.ts │ │ │ ├── domain │ │ │ │ ├── InstallRequest.ts │ │ │ │ └── InstallResult.ts │ │ │ ├── factories │ │ │ │ └── InstallAppControllerFactory.ts │ │ │ ├── index.ts │ │ │ ├── infrastructure │ │ │ │ └── AppInstallerAdapter.ts │ │ │ ├── tests │ │ │ │ ├── e2e │ │ │ │ │ ├── InstallAppController.e2e.test.ts │ │ │ │ │ └── InstallAppMCP.e2e.test.ts │ │ │ │ ├── integration │ │ │ │ │ └── InstallAppController.integration.test.ts │ │ │ │ └── unit │ │ │ │ ├── AppInstallerAdapter.unit.test.ts │ │ │ │ ├── InstallAppController.unit.test.ts │ │ │ │ ├── InstallAppUseCase.unit.test.ts │ │ │ │ ├── InstallRequest.unit.test.ts │ │ │ │ └── InstallResult.unit.test.ts │ │ │ └── use-cases │ │ │ └── InstallAppUseCase.ts │ │ ├── build │ │ │ ├── controllers │ │ │ │ └── BuildXcodeController.ts │ │ │ ├── domain │ │ │ │ ├── BuildDestination.ts │ │ │ │ ├── BuildIssue.ts │ │ │ │ ├── BuildRequest.ts │ │ │ │ ├── BuildResult.ts │ │ │ │ └── PlatformInfo.ts │ │ │ ├── factories │ │ │ │ └── BuildXcodeControllerFactory.ts │ │ │ ├── index.ts │ │ │ ├── infrastructure │ │ │ │ ├── BuildArtifactLocatorAdapter.ts │ │ │ │ ├── BuildDestinationMapperAdapter.ts │ │ │ │ ├── XcbeautifyFormatterAdapter.ts │ │ │ │ ├── XcbeautifyOutputParserAdapter.ts │ │ │ │ └── XcodeBuildCommandAdapter.ts │ │ │ ├── tests │ │ │ │ ├── e2e │ │ │ │ │ ├── BuildXcodeController.e2e.test.ts │ │ │ │ │ └── BuildXcodeMCP.e2e.test.ts │ │ │ │ ├── integration │ │ │ │ │ └── BuildXcodeController.integration.test.ts │ │ │ │ └── unit │ │ │ │ ├── BuildArtifactLocatorAdapter.unit.test.ts │ │ │ │ ├── BuildDestinationMapperAdapter.unit.test.ts │ │ │ │ ├── BuildIssue.unit.test.ts │ │ │ │ ├── BuildProjectUseCase.unit.test.ts │ │ │ │ ├── BuildRequest.unit.test.ts │ │ │ │ ├── BuildResult.unit.test.ts │ │ │ │ ├── BuildXcodeController.unit.test.ts │ │ │ │ ├── BuildXcodePresenter.unit.test.ts │ │ │ │ ├── PlatformInfo.unit.test.ts │ │ │ │ ├── XcbeautifyFormatterAdapter.unit.test.ts │ │ │ │ ├── XcbeautifyOutputParserAdapter.unit.test.ts │ │ │ │ └── XcodeBuildCommandAdapter.unit.test.ts │ │ │ └── use-cases │ │ │ └── BuildProjectUseCase.ts │ │ └── simulator │ │ ├── controllers │ │ │ ├── BootSimulatorController.ts │ │ │ ├── ListSimulatorsController.ts │ │ │ └── ShutdownSimulatorController.ts │ │ ├── domain │ │ │ ├── BootRequest.ts │ │ │ ├── BootResult.ts │ │ │ ├── ListSimulatorsRequest.ts │ │ │ ├── ListSimulatorsResult.ts │ │ │ ├── ShutdownRequest.ts │ │ │ ├── ShutdownResult.ts │ │ │ └── SimulatorState.ts │ │ ├── factories │ │ │ ├── BootSimulatorControllerFactory.ts │ │ │ ├── ListSimulatorsControllerFactory.ts │ │ │ └── ShutdownSimulatorControllerFactory.ts │ │ ├── index.ts │ │ ├── infrastructure │ │ │ ├── SimulatorControlAdapter.ts │ │ │ └── SimulatorLocatorAdapter.ts │ │ ├── tests │ │ │ ├── e2e │ │ │ │ ├── BootSimulatorController.e2e.test.ts │ │ │ │ ├── BootSimulatorMCP.e2e.test.ts │ │ │ │ ├── ListSimulatorsController.e2e.test.ts │ │ │ │ ├── ListSimulatorsMCP.e2e.test.ts │ │ │ │ ├── ShutdownSimulatorController.e2e.test.ts │ │ │ │ └── ShutdownSimulatorMCP.e2e.test.ts │ │ │ ├── integration │ │ │ │ ├── BootSimulatorController.integration.test.ts │ │ │ │ ├── ListSimulatorsController.integration.test.ts │ │ │ │ └── ShutdownSimulatorController.integration.test.ts │ │ │ └── unit │ │ │ ├── BootRequest.unit.test.ts │ │ │ ├── BootResult.unit.test.ts │ │ │ ├── BootSimulatorController.unit.test.ts │ │ │ ├── BootSimulatorUseCase.unit.test.ts │ │ │ ├── ListSimulatorsController.unit.test.ts │ │ │ ├── ListSimulatorsUseCase.unit.test.ts │ │ │ ├── ShutdownRequest.unit.test.ts │ │ │ ├── ShutdownResult.unit.test.ts │ │ │ ├── ShutdownSimulatorUseCase.unit.test.ts │ │ │ ├── SimulatorControlAdapter.unit.test.ts │ │ │ └── SimulatorLocatorAdapter.unit.test.ts │ │ └── use-cases │ │ ├── BootSimulatorUseCase.ts │ │ ├── ListSimulatorsUseCase.ts │ │ └── ShutdownSimulatorUseCase.ts │ ├── index.ts │ ├── infrastructure │ │ ├── repositories │ │ │ └── DeviceRepository.ts │ │ ├── services │ │ │ └── DependencyChecker.ts │ │ └── tests │ │ └── unit │ │ ├── DependencyChecker.unit.test.ts │ │ └── DeviceRepository.unit.test.ts │ ├── logger.ts │ ├── presentation │ │ ├── decorators │ │ │ └── DependencyCheckingDecorator.ts │ │ ├── formatters │ │ │ ├── ErrorFormatter.ts │ │ │ └── strategies │ │ │ ├── BuildIssuesStrategy.ts │ │ │ ├── DefaultErrorStrategy.ts │ │ │ ├── ErrorFormattingStrategy.ts │ │ │ └── OutputFormatterErrorStrategy.ts │ │ ├── interfaces │ │ │ ├── IDependencyChecker.ts │ │ │ ├── MCPController.ts │ │ │ └── MCPResponse.ts │ │ ├── presenters │ │ │ └── BuildXcodePresenter.ts │ │ └── tests │ │ └── unit │ │ ├── BuildIssuesStrategy.unit.test.ts │ │ ├── DefaultErrorStrategy.unit.test.ts │ │ ├── DependencyCheckingDecorator.unit.test.ts │ │ └── ErrorFormatter.unit.test.ts │ ├── shared │ │ ├── domain │ │ │ ├── AppPath.ts │ │ │ ├── DeviceId.ts │ │ │ ├── Platform.ts │ │ │ └── ProjectPath.ts │ │ ├── index.ts │ │ ├── infrastructure │ │ │ ├── ConfigProviderAdapter.ts │ │ │ └── ShellCommandExecutorAdapter.ts │ │ └── tests │ │ ├── mocks │ │ │ ├── promisifyExec.ts │ │ │ ├── selectiveExecMock.ts │ │ │ └── xcodebuildHelpers.ts │ │ ├── skipped │ │ │ ├── cli.e2e.test.skip │ │ │ ├── hook-e2e.test.skip │ │ │ ├── hook-path.e2e.test.skip │ │ │ └── hook.test.skip │ │ ├── types │ │ │ └── execTypes.ts │ │ ├── unit │ │ │ ├── AppPath.unit.test.ts │ │ │ ├── ConfigProviderAdapter.unit.test.ts │ │ │ ├── ProjectPath.unit.test.ts │ │ │ └── ShellCommandExecutorAdapter.unit.test.ts │ │ └── utils │ │ ├── gitResetTestArtifacts.ts │ │ ├── mockHelpers.ts │ │ ├── TestEnvironmentCleaner.ts │ │ ├── TestErrorInjector.ts │ │ ├── testHelpers.ts │ │ ├── TestProjectManager.ts │ │ └── TestSimulatorManager.ts │ ├── types.ts │ ├── utils │ │ ├── devices │ │ │ ├── Devices.ts │ │ │ ├── SimulatorApps.ts │ │ │ ├── SimulatorBoot.ts │ │ │ ├── SimulatorDevice.ts │ │ │ ├── SimulatorInfo.ts │ │ │ ├── SimulatorReset.ts │ │ │ └── SimulatorUI.ts │ │ ├── errors │ │ │ ├── index.ts │ │ │ └── xcbeautify-parser.ts │ │ ├── index.ts │ │ ├── LogManager.ts │ │ ├── LogManagerInstance.ts │ │ └── projects │ │ ├── SwiftBuild.ts │ │ ├── SwiftPackage.ts │ │ ├── SwiftPackageInfo.ts │ │ ├── Xcode.ts │ │ ├── XcodeArchive.ts │ │ ├── XcodeBuild.ts │ │ ├── XcodeErrors.ts │ │ ├── XcodeInfo.ts │ │ └── XcodeProject.ts │ └── utils.ts ├── test_artifacts │ ├── Test.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcuserdata │ │ └── stefan.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ ├── TestProjectSwiftTesting │ │ ├── TestProjectSwiftTesting │ │ │ ├── Assets.xcassets │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── ContentView.swift │ │ │ ├── Item.swift │ │ │ ├── TestProjectSwiftTesting.entitlements │ │ │ └── TestProjectSwiftTestingApp.swift │ │ ├── TestProjectSwiftTesting.xcodeproj │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcuserdata │ │ │ │ └── stefan.xcuserdatad │ │ │ │ └── UserInterfaceState.xcuserstate │ │ │ └── xcuserdata │ │ │ └── stefan.xcuserdatad │ │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ │ ├── TestProjectSwiftTestingTests │ │ │ └── TestProjectSwiftTestingTests.swift │ │ └── TestProjectSwiftTestingUITests │ │ ├── TestProjectSwiftTestingUITests.swift │ │ └── TestProjectSwiftTestingUITestsLaunchTests.swift │ ├── TestProjectWatchOS │ │ ├── TestProjectWatchOS │ │ │ ├── Assets.xcassets │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── ContentView.swift │ │ │ └── TestProjectWatchOSApp.swift │ │ ├── TestProjectWatchOS Watch App │ │ │ ├── Assets.xcassets │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── ContentView.swift │ │ │ └── TestProjectWatchOSApp.swift │ │ ├── TestProjectWatchOS Watch AppTests │ │ │ └── TestProjectWatchOS_Watch_AppTests.swift │ │ ├── TestProjectWatchOS Watch AppUITests │ │ │ ├── TestProjectWatchOS_Watch_AppUITests.swift │ │ │ └── TestProjectWatchOS_Watch_AppUITestsLaunchTests.swift │ │ ├── TestProjectWatchOS.xcodeproj │ │ │ ├── project.pbxproj │ │ │ └── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ ├── TestProjectWatchOSTests │ │ │ └── TestProjectWatchOSTests.swift │ │ └── TestProjectWatchOSUITests │ │ ├── TestProjectWatchOSUITests.swift │ │ └── TestProjectWatchOSUITestsLaunchTests.swift │ ├── TestProjectXCTest │ │ ├── TestProjectXCTest │ │ │ ├── Assets.xcassets │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── ContentView.swift │ │ │ ├── Item.swift │ │ │ ├── TestProjectXCTest.entitlements │ │ │ └── TestProjectXCTestApp.swift │ │ ├── TestProjectXCTest.xcodeproj │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcuserdata │ │ │ │ └── stefan.xcuserdatad │ │ │ │ └── UserInterfaceState.xcuserstate │ │ │ └── xcuserdata │ │ │ └── stefan.xcuserdatad │ │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ │ ├── TestProjectXCTestTests │ │ │ └── TestProjectXCTestTests.swift │ │ └── TestProjectXCTestUITests │ │ ├── TestProjectXCTestUITests.swift │ │ └── TestProjectXCTestUITestsLaunchTests.swift │ ├── TestSwiftPackageSwiftTesting │ │ ├── .gitignore │ │ ├── Package.swift │ │ ├── Sources │ │ │ ├── TestSwiftPackageSwiftTesting │ │ │ │ └── TestSwiftPackageSwiftTesting.swift │ │ │ └── TestSwiftPackageSwiftTestingExecutable │ │ │ └── main.swift │ │ └── Tests │ │ └── TestSwiftPackageSwiftTestingTests │ │ └── TestSwiftPackageSwiftTestingTests.swift │ └── TestSwiftPackageXCTest │ ├── .gitignore │ ├── Package.swift │ ├── Sources │ │ ├── TestSwiftPackageXCTest │ │ │ └── TestSwiftPackageXCTest.swift │ │ └── TestSwiftPackageXCTestExecutable │ │ └── main.swift │ └── Tests │ └── TestSwiftPackageXCTestTests │ └── TestSwiftPackageXCTestTests.swift ├── tsconfig.json └── XcodeProjectModifier ├── Package.resolved ├── Package.swift └── Sources └── XcodeProjectModifier └── main.swift ``` # Files -------------------------------------------------------------------------------- /test_artifacts/TestSwiftPackageSwiftTesting/.gitignore: -------------------------------------------------------------------------------- ``` 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestSwiftPackageXCTest/.gitignore: -------------------------------------------------------------------------------- ``` 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | node_modules/ 2 | dist/ 3 | coverage/ 4 | DerivedData/ 5 | *.log 6 | .DS_Store 7 | .env 8 | .env.local 9 | .env.*.local 10 | *.xcuserdata 11 | *.xcworkspace/xcuserdata/ 12 | *.xcodeproj/xcuserdata/ 13 | /build/ 14 | *.app 15 | *.ipa 16 | *.dSYM.zip 17 | *.dSYM 18 | 19 | # Swift Package Manager 20 | .build/ 21 | .swiftpm/ 22 | 23 | # XcodeProjectModifier build artifacts 24 | XcodeProjectModifier/.build/ 25 | XcodeProjectModifier/.swiftpm/ 26 | 27 | # Test artifacts 28 | test-results*.log 29 | test_artifacts/**/*.xcuserstate 30 | test_artifacts/**/*.xcuserdatad/ 31 | test_artifacts/**/xcuserdata/ 32 | 33 | # Temporary files 34 | *.tmp 35 | .tmp/ 36 | /tmp/ ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP Xcode Server 2 | 3 | [](https://github.com/stefan-nitu/mcp-xcode-server/actions/workflows/ci.yml) 4 | 5 | A Model Context Protocol (MCP) server that enables AI assistants to build, test, run, and manage Apple platform projects through natural language interactions. 6 | 7 | ## Version: 0.6.0 8 | 9 | ## Purpose 10 | 11 | This MCP server bridges the gap between AI assistants and Apple's development ecosystem. It allows AI tools like Claude to directly execute Xcode and Swift Package Manager commands, enabling automated development workflows without manual intervention. The server is designed for token efficiency, providing concise output while maintaining comprehensive error reporting and debugging capabilities. 12 | 13 | ## Why Use MCP Xcode Server? 14 | 15 | ### Key Advantages 16 | 17 | - **AI-Native Development**: Enables AI assistants to build, test, and run iOS/macOS apps directly 18 | - **Token Efficiency**: Optimized output shows only essential information (errors, warnings, test results) 19 | - **Smart Error Handling**: Parses build errors and provides actionable suggestions 20 | - **Visual Debugging**: Capture simulator screenshots to verify UI changes 21 | - **Automatic Simulator Management**: Intelligently reuses running simulators to save time 22 | - **Xcode Integration**: Auto-syncs file operations with Xcode projects via hooks 23 | - **Persistent Logging**: All operations saved to `~/.mcp-xcode-server/logs/` for debugging 24 | - **Multi-Platform**: Supports iOS, macOS, tvOS, watchOS, and visionOS from a single interface 25 | 26 | ### Use Cases 27 | 28 | - **Automated Testing**: AI can run your test suites and analyze failures 29 | - **Build Verification**: Quickly verify code changes compile across platforms 30 | - **UI Development**: Build and screenshot apps to verify visual changes 31 | - **Dependency Management**: Add, update, or remove Swift packages programmatically 32 | - **Cross-Platform Development**: Test the same code on multiple Apple platforms 33 | - **CI/CD Integration**: Automate build and test workflows through natural language 34 | 35 | ## Limitations 36 | 37 | ### What It Can't Do 38 | 39 | - **No SwiftUI Previews**: Xcode's live preview requires the full IDE 40 | - **No Interactive UI Testing**: Cannot simulate user interactions (taps, swipes) 41 | - **No Physical Devices**: Simulator-only for iOS/tvOS/watchOS/visionOS 42 | - **No Debugging**: No breakpoints, step-through debugging, or LLDB access 43 | - **No Xcode UI Features**: Project configuration, storyboard editing require Xcode 44 | - **Platform Requirements**: Requires macOS 14+, Xcode 16+, iOS 17+ simulators 45 | 46 | ### When You Still Need Xcode 47 | 48 | - Designing UI with Interface Builder or SwiftUI previews 49 | - Debugging with breakpoints and variable inspection 50 | - Profiling with Instruments 51 | - Managing certificates and provisioning profiles 52 | - Testing on physical devices 53 | - Using Xcode-specific features (Playgrounds, AR tools, etc.) 54 | 55 | ## Core Features 56 | 57 | ### Build & Test Automation 58 | - Build and run Xcode projects/workspaces 59 | - Execute Swift Package Manager packages 60 | - Run XCTest and Swift Testing suites 61 | - **Xcode projects**: Support for custom build configurations (Debug, Release, Beta, Staging, etc.) 62 | - **Swift packages**: Standard SPM configurations (Debug/Release only - SPM limitation) 63 | 64 | ### Simulator Management 65 | - List and boot simulators for any Apple platform 66 | - Capture screenshots for visual verification 67 | - Install/uninstall apps 68 | - Retrieve device logs with filtering 69 | 70 | ### Error Intelligence 71 | - **Compile Errors**: Shows file, line, column with error message 72 | - **Scheme Errors**: Suggests using `list_schemes` tool 73 | - **Code Signing**: Identifies certificate and provisioning issues 74 | - **Dependencies**: Detects missing modules and version conflicts 75 | 76 | ### File Sync Hooks 77 | - Automatically syncs file operations with Xcode projects 78 | - Intelligently assigns files to correct build phases (Sources, Resources, etc.) 79 | - Respects `.no-xcode-sync` opt-out files 80 | - Maintains proper group structure in Xcode 81 | 82 | ## Installation 83 | 84 | ### Prerequisites 85 | 86 | - macOS 14.0 or later 87 | - Xcode 16.0 or later 88 | - Node.js 18+ 89 | - Xcode Command Line Tools 90 | - Simulators for target platforms 91 | 92 | ### Quick Setup 93 | 94 | ```bash 95 | # Install globally 96 | npm install -g mcp-xcode-server 97 | 98 | # Run interactive setup 99 | mcp-xcode-server setup 100 | ``` 101 | 102 | The setup wizard will: 103 | - Configure the MCP server for Claude 104 | - Optionally set up Xcode sync hooks 105 | - Build necessary helper tools 106 | 107 | ### Manual Configuration 108 | 109 | Add to `~/.claude.json` (global) or `.claude/settings.json` (project): 110 | 111 | ```json 112 | { 113 | "mcpServers": { 114 | "mcp-xcode-server": { 115 | "type": "stdio", 116 | "command": "mcp-xcode-server", 117 | "args": ["serve"], 118 | "env": {} 119 | } 120 | } 121 | } 122 | ``` 123 | 124 | ## Available Tools 125 | 126 | ### Building 127 | 128 | - **`build_xcode`**: Build Xcode projects/workspaces (supports custom configurations) 129 | - **`build_swift_package`**: Build Swift packages (Debug/Release only per SPM spec) 130 | - **`run_xcode`**: Build and run on simulator/macOS 131 | - **`run_swift_package`**: Execute Swift package executables 132 | 133 | ### Testing 134 | 135 | - **`test_xcode`**: Run XCTest/Swift Testing suites 136 | - **`test_swift_package`**: Test Swift packages 137 | - Supports test filtering by class/method 138 | 139 | ### Project Information 140 | 141 | - **`list_schemes`**: Get available Xcode schemes 142 | - **`get_project_info`**: Comprehensive project details 143 | - **`list_targets`**: List all build targets 144 | - **`get_build_settings`**: Get scheme configuration 145 | 146 | ### Simulator Management 147 | 148 | - **`list_simulators`**: Show available devices 149 | - **`boot_simulator`**: Start a simulator 150 | - **`shutdown_simulator`**: Stop a simulator 151 | - **`view_simulator_screen`**: Capture screenshot 152 | 153 | ### App Management 154 | 155 | - **`install_app`**: Install app on simulator 156 | - **`uninstall_app`**: Remove app by bundle ID 157 | - **`get_device_logs`**: Retrieve filtered device logs 158 | 159 | ### Distribution 160 | 161 | - **`archive_project`**: Create .xcarchive 162 | - **`export_ipa`**: Export IPA from archive 163 | 164 | ### Maintenance 165 | 166 | - **`clean_build`**: Clean build artifacts/DerivedData 167 | - **`manage_dependencies`**: Add/remove/update Swift packages 168 | 169 | ## Platform Support 170 | 171 | | Platform | Simulator Required | Default Device | Min Version | 172 | |----------|-------------------|----------------|-------------| 173 | | iOS | Yes | iPhone 16 Pro | iOS 17+ | 174 | | macOS | No | Host machine | macOS 14+ | 175 | | tvOS | Yes | Apple TV | tvOS 17+ | 176 | | watchOS | Yes | Apple Watch Series 10 | watchOS 10+ | 177 | | visionOS | Yes | Apple Vision Pro | visionOS 1.0+ | 178 | 179 | ## Architecture 180 | 181 | The server follows Clean/Hexagonal Architecture with SOLID principles: 182 | 183 | ### Core Structure 184 | ``` 185 | src/ 186 | ├── features/ # Feature-based vertical slices 187 | │ ├── build/ # Build feature 188 | │ │ ├── domain/ # Build domain objects 189 | │ │ ├── use-cases/ 190 | │ │ ├── infrastructure/ 191 | │ │ ├── controllers/ 192 | │ │ └── factories/ 193 | │ ├── simulator/ # Simulator management 194 | │ │ └── ...same structure... 195 | │ └── app-management/ # App installation 196 | │ └── ...same structure... 197 | ├── shared/ # Cross-feature shared code 198 | │ ├── domain/ # Shared value objects 199 | │ └── infrastructure/ 200 | ├── application/ # Application layer ports 201 | │ └── ports/ # Interface definitions 202 | ├── presentation/ # MCP presentation layer 203 | │ ├── interfaces/ # MCP contracts 204 | │ └── formatters/ # Output formatting 205 | └── infrastructure/ # Shared infrastructure 206 | ├── repositories/ 207 | └── services/ 208 | ``` 209 | 210 | ### Key Design Principles 211 | - **Clean Architecture**: Dependency rule - inner layers know nothing about outer layers 212 | - **Domain-Driven Design**: Rich domain models with embedded validation 213 | - **Type Safety**: Full TypeScript with domain primitives and parse-don't-validate pattern 214 | - **Security First**: Path validation, command injection protection at boundaries 215 | - **Error Recovery**: Typed domain errors with graceful handling and helpful suggestions 216 | 217 | ## Logging 218 | 219 | All operations are logged to `~/.mcp-xcode-server/logs/`: 220 | - Daily folders (e.g., `2025-01-27/`) 221 | - 7-day automatic retention 222 | - Full xcodebuild/swift output preserved 223 | - Symlinks to latest logs for easy access 224 | 225 | ## Development 226 | 227 | ```bash 228 | # Build 229 | npm run build 230 | 231 | # Test 232 | npm test # All tests 233 | npm run test:unit # Unit tests only 234 | npm run test:e2e # End-to-end tests 235 | npm run test:coverage # With coverage 236 | 237 | # Development 238 | npm run dev # Build and run 239 | ``` 240 | 241 | ## Contributing 242 | 243 | Contributions welcome! Please ensure: 244 | - Tests pass (`npm test`) 245 | - Code follows SOLID principles 246 | - New tools include tests 247 | - Documentation updated 248 | 249 | ## License 250 | 251 | MIT 252 | 253 | ## Support 254 | 255 | - Report issues: [GitHub Issues](https://github.com/yourusername/mcp-xcode-server/issues) 256 | - Documentation: [MCP Protocol](https://modelcontextprotocol.io) ``` -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- ```markdown 1 | # MANDATORY INITIALIZATION - DO THIS IMMEDIATELY 2 | 3 | ## ⚠️ STOP - READ THIS FIRST ⚠️ 4 | 5 | **YOU MUST READ THESE DOCUMENTS IMMEDIATELY UPON STARTING ANY CONVERSATION ABOUT THIS PROJECT.** 6 | **DO NOT WAIT TO BE ASKED. DO NOT PROCEED WITHOUT READING THEM FIRST.** 7 | 8 | ### Required Documents (READ NOW): 9 | 1. `docs/TESTING-PHILOSOPHY.md` - Critical testing patterns and approaches 10 | 2. `docs/ARCHITECTURE.md` - Clean/Hexagonal Architecture structure 11 | 3. `docs/ERROR-HANDLING.md` - Error handling patterns and presentation conventions 12 | 13 | ### Verification Checklist: 14 | - [ ] I have read `docs/TESTING-PHILOSOPHY.md` completely 15 | - [ ] I have read `docs/ARCHITECTURE.md` completely 16 | - [ ] I have read `docs/ERROR-HANDLING.md` completely 17 | - [ ] I understand the testing philosophy (integration focus, proper mocking, behavior testing) 18 | - [ ] I understand the architecture layers (Domain, Application, Infrastructure, Presentation) 19 | - [ ] I understand error handling patterns (typed errors, emoji prefixes, separation of concerns) 20 | 21 | If you haven't read these documents yet, STOP and read them now using the Read tool. 22 | Only after reading all three documents should you proceed to help the user. 23 | 24 | ## Project Context 25 | 26 | This is an MCP (Model Context Protocol) server for Xcode operations. The codebase follows: 27 | - Clean/Hexagonal Architecture principles 28 | - Integration-focused testing (60% integration, 25% unit, 10% E2E, 5% static) 29 | - Parse-don't-validate pattern with domain validation 30 | - Domain primitives over primitive types 31 | - Type-safe enum comparisons (always use `SimulatorState.Booted`, never `'Booted'`) ``` -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- ```markdown 1 | # Contributing to MCP Apple Simulator 2 | 3 | ## Branching Strategy 4 | 5 | We use **GitHub Flow** - a simple, effective branching model: 6 | 7 | ### Branches 8 | - **`main`** - Always stable, production-ready code. Every commit should be deployable. 9 | - **`feature/*`** - Feature branches for new functionality 10 | - **`fix/*`** - Bug fix branches 11 | - **`docs/*`** - Documentation updates 12 | 13 | ### Workflow 14 | 15 | 1. **Create a feature branch** from main: 16 | ```bash 17 | git checkout main 18 | git pull origin main 19 | git checkout -b feature/your-feature-name 20 | ``` 21 | 22 | 2. **Make your changes** and commit: 23 | ```bash 24 | git add . 25 | git commit -m "feat: add new simulator tool" 26 | ``` 27 | 28 | 3. **Push and create a Pull Request**: 29 | ```bash 30 | git push origin feature/your-feature-name 31 | ``` 32 | 33 | 4. **After review and CI passes**, merge to main 34 | 35 | ### Commit Message Convention 36 | 37 | We use conventional commits: 38 | - `feat:` - New feature 39 | - `fix:` - Bug fix 40 | - `docs:` - Documentation changes 41 | - `test:` - Test additions or changes 42 | - `refactor:` - Code refactoring 43 | - `chore:` - Maintenance tasks 44 | 45 | ### Why No Develop Branch? 46 | 47 | We intentionally don't use a `develop` branch because: 48 | - **Simplicity** - Fewer branches to manage 49 | - **No sync issues** - No divergence between develop and main 50 | - **Continuous deployment** - Every merge to main is ready for users 51 | - **Local tool** - Users update when they want, not on a release schedule 52 | 53 | ### Pull Request Guidelines 54 | 55 | 1. **All tests must pass** - Run `npm test` locally first 56 | 2. **Update documentation** - If adding features, update README 57 | 3. **Small, focused PRs** - Easier to review and less likely to conflict 58 | 4. **Clean commit history** - Squash commits if needed 59 | 60 | ### Testing 61 | 62 | Before submitting a PR: 63 | ```bash 64 | npm run build # Build TypeScript 65 | npm run test:unit # Run unit tests 66 | npm run test:coverage # Check coverage (aim for >75%) 67 | ``` 68 | 69 | ### Code Style 70 | 71 | - TypeScript strict mode enabled 72 | - No `any` types without good reason 73 | - Follow existing patterns in the codebase 74 | - Use dependency injection for testability 75 | 76 | ### Questions? 77 | 78 | Open an issue for discussion before making large changes. ``` -------------------------------------------------------------------------------- /scripts/xcode-sync.swift: -------------------------------------------------------------------------------- ```swift 1 | ``` -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | } ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectSwiftTesting/TestProjectSwiftTesting/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectWatchOS/TestProjectWatchOS Watch App/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectWatchOS/TestProjectWatchOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectXCTest/TestProjectXCTest/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectSwiftTesting/TestProjectSwiftTesting/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectWatchOS/TestProjectWatchOS Watch App/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectWatchOS/TestProjectWatchOS/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectXCTest/TestProjectXCTest/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | ``` -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { exec } from 'child_process'; 2 | import { promisify } from 'util'; 3 | 4 | /** 5 | * Promisified version of exec for async/await usage 6 | */ 7 | export const execAsync = promisify(exec); ``` -------------------------------------------------------------------------------- /src/presentation/formatters/strategies/ErrorFormattingStrategy.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Strategy interface for formatting different types of errors 3 | */ 4 | export interface ErrorFormattingStrategy { 5 | canFormat(error: any): boolean; 6 | format(error: any): string; 7 | } ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectWatchOS/TestProjectWatchOS Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "watchos", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestSwiftPackageXCTest/Sources/TestSwiftPackageXCTest/TestSwiftPackageXCTest.swift: -------------------------------------------------------------------------------- ```swift 1 | // The Swift Programming Language 2 | // https://docs.swift.org/swift-book 3 | 4 | public struct TestSwiftPackageXCTest { 5 | public let text = "Hello, TestSwiftPackageXCTest!" 6 | 7 | public init() {} 8 | } 9 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestSwiftPackageSwiftTesting/Sources/TestSwiftPackageSwiftTesting/TestSwiftPackageSwiftTesting.swift: -------------------------------------------------------------------------------- ```swift 1 | // The Swift Programming Language 2 | // https://docs.swift.org/swift-book 3 | 4 | public struct TestSwiftPackageSwiftTesting { 5 | public let text = "Hello, TestSwiftPackageSwiftTesting!" 6 | 7 | public init() {} 8 | } 9 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectXCTest/TestProjectXCTest/Item.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // Item.swift 3 | // TestProjectXCTest 4 | // 5 | // Created by Stefan Dragos Nitu on 17/08/2025. 6 | // 7 | 8 | import Foundation 9 | import SwiftData 10 | 11 | @Model 12 | final class Item { 13 | var timestamp: Date 14 | 15 | init(timestamp: Date) { 16 | self.timestamp = timestamp 17 | } 18 | } 19 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectSwiftTesting/TestProjectSwiftTesting/Item.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // Item.swift 3 | // TestProjectSwiftTesting 4 | // 5 | // Created by Stefan Dragos Nitu on 17/08/2025. 6 | // 7 | 8 | import Foundation 9 | import SwiftData 10 | 11 | @Model 12 | final class Item { 13 | var timestamp: Date 14 | 15 | init(timestamp: Date) { 16 | self.timestamp = timestamp 17 | } 18 | } 19 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectWatchOS/TestProjectWatchOS/TestProjectWatchOSApp.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // TestProjectWatchOSApp.swift 3 | // TestProjectWatchOS 4 | // 5 | // Created by Stefan Dragos Nitu on 22/08/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct TestProjectWatchOSApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | ``` -------------------------------------------------------------------------------- /src/application/ports/ArtifactPorts.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Port interfaces for build artifact management 3 | * 4 | * These ports define how the application layer locates 5 | * and manages build artifacts (apps, frameworks, etc.) 6 | */ 7 | 8 | export interface IAppLocator { 9 | findApp(derivedDataPath: string): Promise<string | undefined>; 10 | } ``` -------------------------------------------------------------------------------- /src/shared/tests/mocks/xcodebuildHelpers.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Checks if a command is an xcodebuild command 3 | */ 4 | export function isXcodebuildCommand(cmd: string): boolean { 5 | return cmd.startsWith('xcodebuild') || 6 | cmd.includes(' xcodebuild ') || 7 | cmd.includes('|xcodebuild') || 8 | cmd.includes('&& xcodebuild'); 9 | } ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectWatchOS/TestProjectWatchOS Watch App/TestProjectWatchOSApp.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // TestProjectWatchOSApp.swift 3 | // TestProjectWatchOS Watch App 4 | // 5 | // Created by Stefan Dragos Nitu on 22/08/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct TestProjectWatchOS_Watch_AppApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | ``` -------------------------------------------------------------------------------- /src/presentation/interfaces/MCPResponse.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * MCP Framework Response Model 3 | * 4 | * Defines the response format expected by the MCP (Model Context Protocol) framework. 5 | * This is a presentation layer contract for formatting output to the MCP client. 6 | */ 7 | export interface MCPResponse { 8 | content: Array<{ 9 | type: string; 10 | text: string; 11 | }>; 12 | } ``` -------------------------------------------------------------------------------- /src/shared/tests/types/execTypes.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Node.js exec error with stdout/stderr attached 3 | * This is how Node.js exec actually behaves - it attaches stdout/stderr to the error 4 | */ 5 | export interface NodeExecError extends Error { 6 | code?: number; 7 | stdout?: string; 8 | stderr?: string; 9 | } 10 | 11 | /** 12 | * Mock call type for exec function 13 | */ 14 | export type ExecMockCall = [string, ...unknown[]]; ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectWatchOS/TestProjectWatchOSTests/TestProjectWatchOSTests.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // TestProjectWatchOSTests.swift 3 | // TestProjectWatchOSTests 4 | // 5 | // Created by Stefan Dragos Nitu on 22/08/2025. 6 | // 7 | 8 | import Testing 9 | @testable import TestProjectWatchOS 10 | 11 | struct TestProjectWatchOSTests { 12 | 13 | @Test func example() async throws { 14 | // Write your test here and use APIs like `#expect(...)` to check expected conditions. 15 | } 16 | 17 | } 18 | ``` -------------------------------------------------------------------------------- /src/application/ports/OutputFormatterPorts.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Port for formatting build output 3 | * This allows us to swap between different formatters (xcbeautify, raw, custom) 4 | */ 5 | export interface IOutputFormatter { 6 | /** 7 | * Format raw build output into a more readable format 8 | * @param rawOutput The raw output from xcodebuild 9 | * @returns Formatted output 10 | */ 11 | format(rawOutput: string): Promise<string>; 12 | } ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectWatchOS/TestProjectWatchOS Watch AppTests/TestProjectWatchOS_Watch_AppTests.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // TestProjectWatchOS_Watch_AppTests.swift 3 | // TestProjectWatchOS Watch AppTests 4 | // 5 | // Created by Stefan Dragos Nitu on 22/08/2025. 6 | // 7 | 8 | import Testing 9 | @testable import TestProjectWatchOS_Watch_App 10 | 11 | struct TestProjectWatchOS_Watch_AppTests { 12 | 13 | @Test func example() async throws { 14 | // Write your test here and use APIs like `#expect(...)` to check expected conditions. 15 | } 16 | 17 | } 18 | ``` -------------------------------------------------------------------------------- /src/application/ports/LoggingPorts.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Interface for log management operations 3 | * Cross-cutting concern used by multiple use cases 4 | */ 5 | export interface ILogManager { 6 | saveLog( 7 | operation: 'build' | 'test' | 'run' | 'archive' | 'clean', 8 | content: string, 9 | projectName?: string, 10 | metadata?: Record<string, any> 11 | ): string; 12 | 13 | saveDebugData( 14 | operation: string, 15 | data: any, 16 | projectName?: string 17 | ): string; 18 | } ``` -------------------------------------------------------------------------------- /src/domain/shared/Result.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Generic Result type for operations that can fail 3 | * Used throughout the domain layer to avoid throwing 4 | */ 5 | export type Result<T, E = Error> = 6 | | { success: true; value: T } 7 | | { success: false; error: E }; 8 | 9 | export const Result = { 10 | ok<T>(value: T): Result<T, never> { 11 | return { success: true, value }; 12 | }, 13 | 14 | fail<E>(error: E): Result<never, E> { 15 | return { success: false, error }; 16 | } 17 | }; ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectWatchOS/TestProjectWatchOS/ContentView.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // ContentView.swift 3 | // TestProjectWatchOS 4 | // 5 | // Created by Stefan Dragos Nitu on 22/08/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | var body: some View { 12 | VStack { 13 | Image(systemName: "globe") 14 | .imageScale(.large) 15 | .foregroundStyle(.tint) 16 | Text("Hello, world!") 17 | } 18 | .padding() 19 | } 20 | } 21 | 22 | #Preview { 23 | ContentView() 24 | } 25 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectWatchOS/TestProjectWatchOS Watch App/ContentView.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // ContentView.swift 3 | // TestProjectWatchOS Watch App 4 | // 5 | // Created by Stefan Dragos Nitu on 22/08/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | var body: some View { 12 | VStack { 13 | Image(systemName: "globe") 14 | .imageScale(.large) 15 | .foregroundStyle(.tint) 16 | Text("Hello, world!") 17 | } 18 | .padding() 19 | } 20 | } 21 | 22 | #Preview { 23 | ContentView() 24 | } 25 | ``` -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Domain Value Objects 2 | export { Platform } from './domain/Platform.js'; 3 | export { DeviceId } from './domain/DeviceId.js'; 4 | export { ProjectPath } from './domain/ProjectPath.js'; 5 | export { AppPath } from './domain/AppPath.js'; 6 | 7 | // Infrastructure 8 | export { ShellCommandExecutorAdapter } from './infrastructure/ShellCommandExecutorAdapter.js'; 9 | export { ConfigProviderAdapter } from './infrastructure/ConfigProviderAdapter.js'; ``` -------------------------------------------------------------------------------- /src/application/ports/OutputParserPorts.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BuildIssue } from '../../features/build/domain/BuildIssue.js'; 2 | 3 | /** 4 | * Port interface for parsing build output 5 | * This is an application-level abstraction 6 | */ 7 | 8 | export interface ParsedOutput { 9 | issues: BuildIssue[]; 10 | } 11 | 12 | export interface IOutputParser { 13 | /** 14 | * Parse build output and extract issues (errors/warnings) 15 | * Only parses - no formatting or adding messages 16 | */ 17 | parseBuildOutput(output: string): ParsedOutput; 18 | } ``` -------------------------------------------------------------------------------- /src/features/app-management/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Controllers 2 | export { InstallAppController } from './controllers/InstallAppController.js'; 3 | 4 | // Use Cases 5 | export { InstallAppUseCase } from './use-cases/InstallAppUseCase.js'; 6 | 7 | // Factories 8 | export { InstallAppControllerFactory } from './factories/InstallAppControllerFactory.js'; 9 | 10 | // Domain 11 | export { InstallRequest } from './domain/InstallRequest.js'; 12 | 13 | // Infrastructure 14 | export { AppInstallerAdapter } from './infrastructure/AppInstallerAdapter.js'; ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ES2022", 5 | "moduleResolution": "node", 6 | "lib": ["ES2022"], 7 | "outDir": "./dist", 8 | "rootDir": "./src", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "resolveJsonModule": true, 14 | "declaration": true, 15 | "declarationMap": true, 16 | "sourceMap": true 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "dist"] 20 | } ``` -------------------------------------------------------------------------------- /src/features/simulator/domain/BootRequest.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { DeviceId } from '../../../shared/domain/DeviceId.js'; 2 | 3 | /** 4 | * Value object representing a request to boot a simulator 5 | * 6 | * Encapsulates the device identifier (can be UUID or name) 7 | */ 8 | export class BootRequest { 9 | private constructor( 10 | public readonly deviceId: string 11 | ) { 12 | Object.freeze(this); 13 | } 14 | 15 | /** 16 | * Create a boot request from a DeviceId 17 | */ 18 | static create(deviceId: DeviceId): BootRequest { 19 | return new BootRequest(deviceId.toString()); 20 | } 21 | } ``` -------------------------------------------------------------------------------- /src/presentation/interfaces/IDependencyChecker.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Represents a missing dependency 3 | */ 4 | export interface MissingDependency { 5 | readonly name: string; 6 | readonly installCommand?: string; 7 | } 8 | 9 | /** 10 | * Checks for system dependencies required by MCP tools 11 | */ 12 | export interface IDependencyChecker { 13 | /** 14 | * Check if the specified dependencies are available 15 | * @param dependencies List of dependency names to check 16 | * @returns List of missing dependencies 17 | */ 18 | check(dependencies: string[]): Promise<MissingDependency[]>; 19 | } ``` -------------------------------------------------------------------------------- /.claude/settings.local.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "permissions": { 3 | "allow": [ 4 | "Bash(npm test:*)", 5 | "WebSearch", 6 | "Bash(npm run build:*)", 7 | "Bash(npm run test:coverage:*)", 8 | "WebFetch(domain:gist.github.com)", 9 | "WebFetch(domain:github.com)", 10 | "Bash(npm run test:e2e:*)", 11 | "Bash(npm run:*)", 12 | "Bash(npx jest:*)", 13 | "Bash(timeout 60 npx jest:*)", 14 | "Bash(grep:*)", 15 | "WebFetch(domain:docs.anthropic.com)", 16 | "Bash(xcodebuild:*)" 17 | ], 18 | "deny": [], 19 | "ask": [] 20 | } 21 | } ``` -------------------------------------------------------------------------------- /src/application/ports/CommandPorts.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Port interfaces for command execution 3 | * 4 | * These are general infrastructure ports that can be used 5 | * by any use case that needs to execute external commands. 6 | */ 7 | 8 | export interface ExecutionResult { 9 | stdout: string; 10 | stderr: string; 11 | exitCode: number; 12 | } 13 | 14 | export interface ExecutionOptions { 15 | maxBuffer?: number; 16 | timeout?: number; 17 | shell?: string; 18 | } 19 | 20 | export interface ICommandExecutor { 21 | execute( 22 | command: string, 23 | options?: ExecutionOptions 24 | ): Promise<ExecutionResult>; 25 | } ``` -------------------------------------------------------------------------------- /src/features/simulator/domain/ShutdownRequest.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { DeviceId } from '../../../shared/domain/DeviceId.js'; 2 | 3 | /** 4 | * Value object representing a request to shutdown a simulator 5 | * 6 | * Encapsulates the device identifier (can be UUID or name) 7 | */ 8 | export class ShutdownRequest { 9 | private constructor( 10 | public readonly deviceId: string 11 | ) { 12 | Object.freeze(this); 13 | } 14 | 15 | /** 16 | * Create a shutdown request from a DeviceId 17 | */ 18 | static create(deviceId: DeviceId): ShutdownRequest { 19 | return new ShutdownRequest(deviceId.toString()); 20 | } 21 | } ``` -------------------------------------------------------------------------------- /src/application/ports/MappingPorts.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Port interfaces for mapping between domain and infrastructure concepts 3 | */ 4 | 5 | import { BuildDestination } from '../../features/build/domain/BuildDestination.js'; 6 | 7 | /** 8 | * Maps BuildDestination to xcodebuild-specific options 9 | */ 10 | export interface IBuildDestinationMapper { 11 | /** 12 | * Map a domain BuildDestination to xcodebuild destination string and settings 13 | */ 14 | toXcodeBuildOptions(destination: BuildDestination): Promise<{ 15 | destination: string; 16 | additionalSettings?: string[]; 17 | }>; 18 | } ``` -------------------------------------------------------------------------------- /src/presentation/formatters/strategies/OutputFormatterErrorStrategy.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { OutputFormatterError } from '../../../features/build/domain/BuildResult.js'; 2 | import { ErrorFormattingStrategy } from './ErrorFormattingStrategy.js'; 3 | 4 | /** 5 | * Formats output formatter errors (e.g., xcbeautify not installed) 6 | */ 7 | export class OutputFormatterErrorStrategy implements ErrorFormattingStrategy { 8 | canFormat(error: any): boolean { 9 | return error instanceof OutputFormatterError; 10 | } 11 | 12 | format(error: OutputFormatterError): string { 13 | return `xcbeautify failed: ${error.stderr}`; 14 | } 15 | } ``` -------------------------------------------------------------------------------- /src/features/simulator/domain/ListSimulatorsRequest.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Platform } from '../../../shared/domain/Platform.js'; 2 | import { SimulatorState } from './SimulatorState.js'; 3 | 4 | /** 5 | * Value object for list simulators request 6 | */ 7 | export class ListSimulatorsRequest { 8 | constructor( 9 | public readonly platform?: Platform, 10 | public readonly state?: SimulatorState, 11 | public readonly name?: string 12 | ) {} 13 | 14 | static create( 15 | platform?: Platform, 16 | state?: SimulatorState, 17 | name?: string 18 | ): ListSimulatorsRequest { 19 | return new ListSimulatorsRequest(platform, state, name); 20 | } 21 | } ``` -------------------------------------------------------------------------------- /src/application/ports/BuildPorts.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Port interfaces specific to build operations 3 | * 4 | * These ports define how the application layer 5 | * interacts with build-specific infrastructure. 6 | */ 7 | 8 | // Options for the build command builder (infrastructure concerns) 9 | export interface BuildCommandOptions { 10 | scheme: string; 11 | configuration?: string; 12 | destination: string; // Already mapped destination string 13 | additionalSettings?: string[]; 14 | derivedDataPath?: string; 15 | } 16 | 17 | export interface IBuildCommand { 18 | build( 19 | projectPath: string, 20 | isWorkspace: boolean, 21 | options: BuildCommandOptions 22 | ): string; 23 | } ``` -------------------------------------------------------------------------------- /XcodeProjectModifier/Package.swift: -------------------------------------------------------------------------------- ```swift 1 | // swift-tools-version: 6.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "XcodeProjectModifier", 6 | platforms: [.macOS(.v10_15)], 7 | dependencies: [ 8 | .package(url: "https://github.com/tuist/XcodeProj.git", from: "8.0.0"), 9 | .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0") 10 | ], 11 | targets: [ 12 | .executableTarget( 13 | name: "XcodeProjectModifier", 14 | dependencies: [ 15 | "XcodeProj", 16 | .product(name: "ArgumentParser", package: "swift-argument-parser") 17 | ] 18 | ) 19 | ] 20 | ) ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectWatchOS/TestProjectWatchOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | } 30 | ], 31 | "info" : { 32 | "author" : "xcode", 33 | "version" : 1 34 | } 35 | } 36 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestSwiftPackageXCTest/Tests/TestSwiftPackageXCTestTests/TestSwiftPackageXCTestTests.swift: -------------------------------------------------------------------------------- ```swift 1 | import XCTest 2 | @testable import TestSwiftPackageXCTest 3 | 4 | final class LegacyTests: XCTestCase { 5 | func testExample() throws { 6 | let spm = TestSwiftPackageXCTest() 7 | XCTAssertEqual(spm.text, "Hello, TestSwiftPackageXCTest!") 8 | } 9 | 10 | func testFailingTest() async throws { 11 | // This test is designed to fail for MCP testing 12 | XCTFail("Test MCP failing test reporting") 13 | } 14 | 15 | func testAnotherFailure() async throws { 16 | // Another failing test to verify multiple failures are handled 17 | XCTAssertEqual(42, 100, "Expected 42 to equal 100 but it doesn't") 18 | } 19 | } 20 | ``` -------------------------------------------------------------------------------- /src/application/ports/ConfigPorts.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Port interface for configuration access 3 | * This is an application-level abstraction 4 | */ 5 | 6 | export interface IConfigProvider { 7 | /** 8 | * Get the path for DerivedData 9 | * @param projectPath Optional project path to generate project-specific derived data path 10 | */ 11 | getDerivedDataPath(projectPath?: string): string; 12 | 13 | /** 14 | * Get timeout for build operations in milliseconds 15 | */ 16 | getBuildTimeout(): number; 17 | 18 | /** 19 | * Check if xcbeautify is enabled 20 | */ 21 | isXcbeautifyEnabled(): boolean; 22 | 23 | /** 24 | * Get any custom build settings 25 | */ 26 | getCustomBuildSettings(): Record<string, string>; 27 | } ``` -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { homedir } from 'os'; 2 | import path from 'path'; 3 | 4 | /** 5 | * Configuration for MCP Xcode Server 6 | */ 7 | export const config = { 8 | /** 9 | * Base path for MCP-Xcode DerivedData 10 | * Uses Xcode's standard location but in MCP-Xcode subfolder 11 | */ 12 | derivedDataBasePath: path.join(homedir(), 'Library', 'Developer', 'Xcode', 'DerivedData', 'MCP-Xcode'), 13 | 14 | /** 15 | * Get DerivedData path for a specific project 16 | */ 17 | getDerivedDataPath(projectPath: string): string { 18 | const projectName = path.basename(projectPath, path.extname(projectPath)); 19 | return path.join(this.derivedDataBasePath, projectName); 20 | } 21 | }; ``` -------------------------------------------------------------------------------- /test_artifacts/TestSwiftPackageSwiftTesting/Sources/TestSwiftPackageSwiftTestingExecutable/main.swift: -------------------------------------------------------------------------------- ```swift 1 | import Foundation 2 | 3 | // Simple executable for testing 4 | print("TestSwiftPackageSwiftTesting Executable Running") 5 | print("Arguments: \(CommandLine.arguments)") 6 | print("Current Date: \(Date())") 7 | 8 | // Test argument handling 9 | if CommandLine.arguments.count > 1 { 10 | print("Received \(CommandLine.arguments.count - 1) arguments:") 11 | for (index, arg) in CommandLine.arguments.dropFirst().enumerated() { 12 | print(" Arg \(index + 1): \(arg)") 13 | } 14 | } 15 | 16 | // Test exit codes 17 | if CommandLine.arguments.contains("--fail") { 18 | print("Error: Requested failure via --fail flag") 19 | exit(1) 20 | } 21 | 22 | print("Execution completed successfully") 23 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestSwiftPackageXCTest/Sources/TestSwiftPackageXCTestExecutable/main.swift: -------------------------------------------------------------------------------- ```swift 1 | import Foundation 2 | 3 | // Simple executable for testing 4 | print("TestSwiftPackageXCTestExecutable Executable Running") 5 | print("Arguments: \(CommandLine.arguments)") 6 | print("Current Date: \(Date())") 7 | 8 | // Test argument handling 9 | if CommandLine.arguments.count > 1 { 10 | print("Received \(CommandLine.arguments.count - 1) arguments:") 11 | for (index, arg) in CommandLine.arguments.dropFirst().enumerated() { 12 | print(" Arg \(index + 1): \(arg)") 13 | } 14 | } 15 | 16 | // Test exit codes 17 | if CommandLine.arguments.contains("--fail") { 18 | print("Error: Requested failure via --fail flag") 19 | exit(1) 20 | } 21 | 22 | print("Execution completed successfully") 23 | ``` -------------------------------------------------------------------------------- /src/features/app-management/infrastructure/AppInstallerAdapter.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { IAppInstaller } from '../../../application/ports/SimulatorPorts.js'; 2 | import { ICommandExecutor } from '../../../application/ports/CommandPorts.js'; 3 | 4 | /** 5 | * Installs apps on simulators using xcrun simctl 6 | */ 7 | export class AppInstallerAdapter implements IAppInstaller { 8 | constructor(private executor: ICommandExecutor) {} 9 | 10 | async installApp(appPath: string, simulatorId: string): Promise<void> { 11 | const result = await this.executor.execute( 12 | `xcrun simctl install "${simulatorId}" "${appPath}"` 13 | ); 14 | 15 | if (result.exitCode !== 0) { 16 | throw new Error(result.stderr || 'Failed to install app'); 17 | } 18 | } 19 | } ``` -------------------------------------------------------------------------------- /src/presentation/formatters/strategies/DefaultErrorStrategy.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ErrorFormattingStrategy } from './ErrorFormattingStrategy.js'; 2 | 3 | /** 4 | * Default strategy for plain error messages 5 | */ 6 | export class DefaultErrorStrategy implements ErrorFormattingStrategy { 7 | canFormat(_error: any): boolean { 8 | return true; // Always can format as fallback 9 | } 10 | 11 | format(error: any): string { 12 | if (error && error.message) { 13 | // Clean up common prefixes 14 | let message = error.message; 15 | message = message.replace(/^Error:\s*/i, ''); 16 | message = message.replace(/^Invalid arguments:\s*/i, ''); 17 | message = message.replace(/^Validation failed:\s*/i, ''); 18 | return message; 19 | } 20 | return 'An error occurred'; 21 | } 22 | } ``` -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Type definitions for Apple Simulator MCP Server 3 | */ 4 | 5 | // Re-export Platform from domain for backward compatibility 6 | // TODO: Update all imports to use domain directly 7 | export { Platform } from './shared/domain/Platform.js'; 8 | 9 | export interface SimulatorDevice { 10 | udid: string; 11 | name: string; 12 | state: 'Booted' | 'Shutdown' | 'Creating' | 'Booting' | 'ShuttingDown'; 13 | deviceTypeIdentifier: string; 14 | runtime: string; 15 | isAvailable?: boolean; 16 | } 17 | 18 | export interface TestResult { 19 | success: boolean; 20 | output: string; 21 | errors?: string; 22 | testCount?: number; 23 | failureCount?: number; 24 | } 25 | 26 | export interface Tool { 27 | execute(args: any): Promise<any>; 28 | getToolDefinition(): any; 29 | } ``` -------------------------------------------------------------------------------- /jest.e2e.config.cjs: -------------------------------------------------------------------------------- ``` 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | testTimeout: 120000, 6 | transform: { 7 | '^.+\\.ts$': ['ts-jest', { 8 | tsconfig: { 9 | module: 'commonjs', 10 | esModuleInterop: true, 11 | allowSyntheticDefaultImports: true, 12 | }, 13 | }], 14 | }, 15 | moduleNameMapper: { 16 | '^(\\.{1,2}/.*)\\.js$': '$1', 17 | }, 18 | testMatch: [ 19 | '**/*.e2e.test.ts' 20 | ], 21 | collectCoverageFrom: [ 22 | 'src/**/*.ts', 23 | '!src/**/*.d.ts', 24 | '!src/**/*.test.ts', 25 | '!src/**/tests/**/*', 26 | '!src/__tests__/**/*', 27 | '!src/index.ts', 28 | ], 29 | coverageDirectory: 'coverage', 30 | coverageReporters: ['text', 'lcov', 'html'], 31 | }; ``` -------------------------------------------------------------------------------- /src/infrastructure/repositories/DeviceRepository.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ICommandExecutor } from '../../application/ports/CommandPorts.js'; 2 | 3 | export interface RawDevice { 4 | udid: string; 5 | name: string; 6 | state: string; 7 | isAvailable: boolean; 8 | deviceTypeIdentifier?: string; 9 | dataPath?: string; 10 | dataPathSize?: number; 11 | logPath?: string; 12 | } 13 | 14 | export interface DeviceList { 15 | [runtime: string]: RawDevice[]; 16 | } 17 | 18 | /** 19 | * Repository for accessing simulator device information 20 | */ 21 | export class DeviceRepository { 22 | constructor(private executor: ICommandExecutor) {} 23 | 24 | async getAllDevices(): Promise<DeviceList> { 25 | const result = await this.executor.execute('xcrun simctl list devices --json'); 26 | const data = JSON.parse(result.stdout); 27 | return data.devices as DeviceList; 28 | } 29 | } ``` -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- ``` 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | testTimeout: 10000, 6 | transform: { 7 | '^.+\\.ts$': ['ts-jest', { 8 | tsconfig: { 9 | module: 'commonjs', 10 | esModuleInterop: true, 11 | allowSyntheticDefaultImports: true, 12 | }, 13 | }], 14 | }, 15 | moduleNameMapper: { 16 | '^(\\.{1,2}/.*)\\.js$': '$1', 17 | }, 18 | testMatch: [ 19 | '**/*.unit.test.ts', 20 | '**/*.integration.test.ts' 21 | ], 22 | collectCoverageFrom: [ 23 | 'src/**/*.ts', 24 | '!src/**/*.d.ts', 25 | '!src/**/*.test.ts', 26 | '!src/**/tests/**/*', 27 | '!src/__tests__/**/*', 28 | '!src/index.ts', 29 | ], 30 | coverageDirectory: 'coverage', 31 | coverageReporters: ['text', 'lcov', 'html'], 32 | }; ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectXCTest/TestProjectXCTest/TestProjectXCTestApp.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // TestProjectXCTestApp.swift 3 | // TestProjectXCTest 4 | // 5 | // Created by Stefan Dragos Nitu on 17/08/2025. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftData 10 | 11 | @main 12 | struct TestProjectXCTestApp: App { 13 | var sharedModelContainer: ModelContainer = { 14 | let schema = Schema([ 15 | Item.self, 16 | ]) 17 | let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) 18 | 19 | do { 20 | return try ModelContainer(for: schema, configurations: [modelConfiguration]) 21 | } catch { 22 | fatalError("Could not create ModelContainer: \(error)") 23 | } 24 | }() 25 | 26 | var body: some Scene { 27 | WindowGroup { 28 | ContentView() 29 | } 30 | .modelContainer(sharedModelContainer) 31 | } 32 | } 33 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectSwiftTesting/TestProjectSwiftTesting/TestProjectSwiftTestingApp.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // TestProjectSwiftTestingApp.swift 3 | // TestProjectSwiftTesting 4 | // 5 | // Created by Stefan Dragos Nitu on 17/08/2025. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftData 10 | 11 | @main 12 | struct TestProjectSwiftTestingApp: App { 13 | var sharedModelContainer: ModelContainer = { 14 | let schema = Schema([ 15 | Item.self, 16 | ]) 17 | let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) 18 | 19 | do { 20 | return try ModelContainer(for: schema, configurations: [modelConfiguration]) 21 | } catch { 22 | fatalError("Could not create ModelContainer: \(error)") 23 | } 24 | }() 25 | 26 | var body: some Scene { 27 | WindowGroup { 28 | ContentView() 29 | } 30 | .modelContainer(sharedModelContainer) 31 | } 32 | } 33 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectSwiftTesting/TestProjectSwiftTestingTests/TestProjectSwiftTestingTests.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // TestProjectSwiftTestingTests.swift 3 | // TestProjectSwiftTestingTests 4 | // 5 | // Created by Stefan Dragos Nitu on 17/08/2025. 6 | // 7 | 8 | import Testing 9 | 10 | struct TestProjectSwiftTestingTests { 11 | 12 | @Test func example() async throws { 13 | // Write your test here and use APIs like `#expect(...)` to check expected conditions. 14 | #expect(true) 15 | } 16 | 17 | @Test func testFailingTest() async throws { 18 | // This test intentionally fails to test failure reporting 19 | #expect(false, "This test is designed to fail for testing MCP failure reporting") 20 | } 21 | 22 | @Test func testAnotherFailure() async throws { 23 | // Another failing test to verify multiple failures are handled 24 | let result = 42 25 | #expect(result == 100, "Expected result to be 100 but got \(result)") 26 | } 27 | 28 | } 29 | ``` -------------------------------------------------------------------------------- /src/shared/tests/mocks/promisifyExec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { ExecOptions, ExecException } from 'child_process'; 2 | 3 | type ExecCallback = (error: ExecException | null, stdout: string, stderr: string) => void; 4 | type ExecFunction = (command: string, options: ExecOptions, callback: ExecCallback) => void; 5 | 6 | /** 7 | * Creates a promisified version of exec that matches Node's util.promisify behavior 8 | * Returns {stdout, stderr} on success, attaches them to error on failure 9 | */ 10 | export function createPromisifiedExec(execFn: ExecFunction) { 11 | return (cmd: string, options?: ExecOptions) => 12 | new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { 13 | execFn(cmd, options || {}, (error, stdout, stderr) => { 14 | if (error) { 15 | Object.assign(error, { stdout, stderr }); 16 | reject(error); 17 | } else { 18 | resolve({ stdout, stderr }); 19 | } 20 | }); 21 | }); 22 | } ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectXCTest/TestProjectXCTestUITests/TestProjectXCTestUITestsLaunchTests.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // TestProjectXCTestUITestsLaunchTests.swift 3 | // TestProjectXCTestUITests 4 | // 5 | // Created by Stefan Dragos Nitu on 17/08/2025. 6 | // 7 | 8 | import XCTest 9 | 10 | final class TestProjectXCTestUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | @MainActor 21 | func testLaunch() throws { 22 | let app = XCUIApplication() 23 | app.launch() 24 | 25 | // Insert steps here to perform after app launch but before taking a screenshot, 26 | // such as logging into a test account or navigating somewhere in the app 27 | 28 | let attachment = XCTAttachment(screenshot: app.screenshot()) 29 | attachment.name = "Launch Screen" 30 | attachment.lifetime = .keepAlways 31 | add(attachment) 32 | } 33 | } 34 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectWatchOS/TestProjectWatchOSUITests/TestProjectWatchOSUITestsLaunchTests.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // TestProjectWatchOSUITestsLaunchTests.swift 3 | // TestProjectWatchOSUITests 4 | // 5 | // Created by Stefan Dragos Nitu on 22/08/2025. 6 | // 7 | 8 | import XCTest 9 | 10 | final class TestProjectWatchOSUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | @MainActor 21 | func testLaunch() throws { 22 | let app = XCUIApplication() 23 | app.launch() 24 | 25 | // Insert steps here to perform after app launch but before taking a screenshot, 26 | // such as logging into a test account or navigating somewhere in the app 27 | 28 | let attachment = XCTAttachment(screenshot: app.screenshot()) 29 | attachment.name = "Launch Screen" 30 | attachment.lifetime = .keepAlways 31 | add(attachment) 32 | } 33 | } 34 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectSwiftTesting/TestProjectSwiftTestingUITests/TestProjectSwiftTestingUITestsLaunchTests.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // TestProjectSwiftTestingUITestsLaunchTests.swift 3 | // TestProjectSwiftTestingUITests 4 | // 5 | // Created by Stefan Dragos Nitu on 17/08/2025. 6 | // 7 | 8 | import XCTest 9 | 10 | final class TestProjectSwiftTestingUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | @MainActor 21 | func testLaunch() throws { 22 | let app = XCUIApplication() 23 | app.launch() 24 | 25 | // Insert steps here to perform after app launch but before taking a screenshot, 26 | // such as logging into a test account or navigating somewhere in the app 27 | 28 | let attachment = XCTAttachment(screenshot: app.screenshot()) 29 | attachment.name = "Launch Screen" 30 | attachment.lifetime = .keepAlways 31 | add(attachment) 32 | } 33 | } 34 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectWatchOS/TestProjectWatchOS Watch AppUITests/TestProjectWatchOS_Watch_AppUITestsLaunchTests.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // TestProjectWatchOS_Watch_AppUITestsLaunchTests.swift 3 | // TestProjectWatchOS Watch AppUITests 4 | // 5 | // Created by Stefan Dragos Nitu on 22/08/2025. 6 | // 7 | 8 | import XCTest 9 | 10 | final class TestProjectWatchOS_Watch_AppUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | @MainActor 21 | func testLaunch() throws { 22 | let app = XCUIApplication() 23 | app.launch() 24 | 25 | // Insert steps here to perform after app launch but before taking a screenshot, 26 | // such as logging into a test account or navigating somewhere in the app 27 | 28 | let attachment = XCTAttachment(screenshot: app.screenshot()) 29 | attachment.name = "Launch Screen" 30 | attachment.lifetime = .keepAlways 31 | add(attachment) 32 | } 33 | } 34 | ``` -------------------------------------------------------------------------------- /src/features/simulator/factories/ListSimulatorsControllerFactory.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ListSimulatorsController } from '../controllers/ListSimulatorsController.js'; 2 | import { ListSimulatorsUseCase } from '../use-cases/ListSimulatorsUseCase.js'; 3 | import { DeviceRepository } from '../../../infrastructure/repositories/DeviceRepository.js'; 4 | import { ShellCommandExecutorAdapter } from '../../../shared/infrastructure/ShellCommandExecutorAdapter.js'; 5 | import { exec } from 'child_process'; 6 | import { promisify } from 'util'; 7 | 8 | /** 9 | * Factory for creating ListSimulatorsController with all dependencies 10 | */ 11 | export class ListSimulatorsControllerFactory { 12 | static create(): ListSimulatorsController { 13 | const execAsync = promisify(exec); 14 | const executor = new ShellCommandExecutorAdapter(execAsync); 15 | const deviceRepository = new DeviceRepository(executor); 16 | const useCase = new ListSimulatorsUseCase(deviceRepository); 17 | 18 | return new ListSimulatorsController(useCase); 19 | } 20 | } ``` -------------------------------------------------------------------------------- /src/domain/services/PlatformDetector.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Platform } from '../../shared/domain/Platform.js'; 2 | import { BuildDestination } from '../../features/build/domain/BuildDestination.js'; 3 | 4 | /** 5 | * Domain Service: Detects platform from build destination 6 | * 7 | * Pure domain logic - no external dependencies 8 | * Used to determine which platform a build destination targets 9 | */ 10 | export class PlatformDetector { 11 | /** 12 | * Extract platform from a build destination 13 | */ 14 | static fromDestination(destination: BuildDestination): Platform { 15 | if (destination.startsWith('iOS')) return Platform.iOS; 16 | if (destination.startsWith('macOS')) return Platform.macOS; 17 | if (destination.startsWith('tvOS')) return Platform.tvOS; 18 | if (destination.startsWith('watchOS')) return Platform.watchOS; 19 | if (destination.startsWith('visionOS')) return Platform.visionOS; 20 | 21 | // Default to iOS if unknown (shouldn't happen with proper validation) 22 | return Platform.iOS; 23 | } 24 | } ``` -------------------------------------------------------------------------------- /src/presentation/interfaces/MCPController.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { MCPResponse } from './MCPResponse.js'; 2 | 3 | /** 4 | * Interface for MCP Tool Controllers 5 | * 6 | * All MCP controllers must implement this interface to ensure 7 | * consistent tool definition and execution patterns 8 | */ 9 | export interface MCPController { 10 | /** MCP tool name (e.g., 'build_xcode', 'install_app') */ 11 | readonly name: string; 12 | 13 | /** Human-readable description of what the tool does */ 14 | readonly description: string; 15 | 16 | /** JSON Schema for input validation */ 17 | readonly inputSchema: object; 18 | 19 | /** 20 | * Get the complete MCP tool definition 21 | * Used by the MCP server to register the tool 22 | */ 23 | getToolDefinition(): { 24 | name: string; 25 | description: string; 26 | inputSchema: object; 27 | }; 28 | 29 | /** 30 | * Execute the tool with given arguments 31 | * @param args - Unknown input that will be validated 32 | * @returns MCP-formatted response with content array 33 | */ 34 | execute(args: unknown): Promise<MCPResponse>; 35 | } ``` -------------------------------------------------------------------------------- /src/utils/projects/XcodeErrors.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Error types for Xcode operations 3 | */ 4 | export enum XcodeErrorType { 5 | ProjectNotFound = 'PROJECT_NOT_FOUND', 6 | InvalidProjectType = 'INVALID_PROJECT_TYPE', 7 | UnknownError = 'UNKNOWN_ERROR' 8 | } 9 | 10 | /** 11 | * Custom error class for Xcode operations 12 | */ 13 | export class XcodeError extends Error { 14 | constructor( 15 | public readonly type: XcodeErrorType, 16 | public readonly path: string, 17 | message?: string 18 | ) { 19 | super(message || XcodeError.getDefaultMessage(type, path)); 20 | this.name = 'XcodeError'; 21 | } 22 | 23 | private static getDefaultMessage(type: XcodeErrorType, path: string): string { 24 | switch (type) { 25 | case XcodeErrorType.ProjectNotFound: 26 | return `No Xcode project or Swift package found at: ${path}`; 27 | case XcodeErrorType.InvalidProjectType: 28 | return `Invalid project type at: ${path}`; 29 | case XcodeErrorType.UnknownError: 30 | return `Unknown error opening project at: ${path}`; 31 | } 32 | } 33 | } ``` -------------------------------------------------------------------------------- /src/utils/devices/SimulatorReset.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { execAsync } from '../../utils.js'; 2 | import { createModuleLogger } from '../../logger.js'; 3 | 4 | const logger = createModuleLogger('SimulatorReset'); 5 | 6 | /** 7 | * Utility class for resetting simulator state 8 | * Single responsibility: Reset a simulator (erase all content and settings) 9 | */ 10 | export class SimulatorReset { 11 | /** 12 | * Reset a simulator by erasing all content and settings 13 | * Equivalent to "Device > Erase All Content and Settings" in Simulator app 14 | */ 15 | async reset(deviceId: string): Promise<void> { 16 | try { 17 | logger.info({ deviceId }, 'Resetting simulator - erasing all content and settings'); 18 | await execAsync(`xcrun simctl erase "${deviceId}"`); 19 | logger.debug({ deviceId }, 'Successfully reset simulator'); 20 | } catch (error: any) { 21 | logger.error({ error: error.message, deviceId }, 'Failed to reset simulator'); 22 | throw new Error(`Failed to reset simulator: ${error.message}`); 23 | } 24 | } 25 | } ``` -------------------------------------------------------------------------------- /test_artifacts/TestSwiftPackageSwiftTesting/Tests/TestSwiftPackageSwiftTestingTests/TestSwiftPackageSwiftTestingTests.swift: -------------------------------------------------------------------------------- ```swift 1 | import Testing 2 | @testable import TestSwiftPackageSwiftTesting 3 | 4 | struct ModernTests { 5 | @Test func testExample() async throws { 6 | // Simple test to verify the module can be imported and tested 7 | let spm = TestSwiftPackageSwiftTesting() 8 | #expect(spm.text == "Hello, TestSwiftPackageSwiftTesting!") 9 | } 10 | 11 | @Test func testFailingTest() async throws { 12 | // This test is designed to fail for MCP testing 13 | #expect(1 == 2, """ 14 | Test MCP failing test reporting. 15 | This is a multi-line message to test 16 | how Swift Testing handles longer error descriptions. 17 | Line 4 of the message. 18 | Line 5 with special characters: @#$%^&*() 19 | """) 20 | } 21 | 22 | @Test func testAnotherFailure() async throws { 23 | // Another failing test to verify multiple failures are handled 24 | let result = 42 25 | #expect(result == 100, "Expected result to be 100 but got \(result)") 26 | } 27 | } 28 | ``` -------------------------------------------------------------------------------- /src/presentation/formatters/ErrorFormatter.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ErrorFormattingStrategy } from './strategies/ErrorFormattingStrategy.js'; 2 | import { BuildIssuesStrategy } from './strategies/BuildIssuesStrategy.js'; 3 | import { OutputFormatterErrorStrategy } from './strategies/OutputFormatterErrorStrategy.js'; 4 | import { DefaultErrorStrategy } from './strategies/DefaultErrorStrategy.js'; 5 | 6 | /** 7 | * Main error formatter that uses strategies 8 | */ 9 | export class ErrorFormatter { 10 | private static strategies: ErrorFormattingStrategy[] = [ 11 | new BuildIssuesStrategy(), 12 | new OutputFormatterErrorStrategy(), 13 | new DefaultErrorStrategy() // Must be last - catches all other errors 14 | ]; 15 | 16 | /** 17 | * Format any error into a user-friendly message 18 | */ 19 | static format(error: Error | any): string { 20 | for (const strategy of this.strategies) { 21 | if (strategy.canFormat(error)) { 22 | return strategy.format(error); 23 | } 24 | } 25 | 26 | // Shouldn't reach here due to DefaultErrorStrategy 27 | return 'Unknown error'; 28 | } 29 | } ``` -------------------------------------------------------------------------------- /src/features/app-management/domain/InstallRequest.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { AppPath } from '../../../shared/domain/AppPath.js'; 2 | import { DeviceId } from '../../../shared/domain/DeviceId.js'; 3 | 4 | /** 5 | * Domain Value Object: Represents an app installation request 6 | * 7 | * Contains all the data needed to install an app: 8 | * - What: appPath (the .app bundle to install) 9 | * - Where: simulatorId (optional - uses booted simulator if not specified) 10 | */ 11 | export class InstallRequest { 12 | private constructor( 13 | public readonly appPath: AppPath, 14 | public readonly simulatorId?: DeviceId 15 | ) {} 16 | 17 | /** 18 | * Create an InstallRequest from raw inputs 19 | * Validates the inputs and creates an immutable request object 20 | */ 21 | static create( 22 | appPath: unknown, 23 | simulatorId?: unknown 24 | ): InstallRequest { 25 | // Validate app path using AppPath value object 26 | const validatedAppPath = AppPath.create(appPath); 27 | 28 | // Validate simulator ID if provided 29 | const validatedDeviceId = DeviceId.createOptional(simulatorId); 30 | 31 | return new InstallRequest(validatedAppPath, validatedDeviceId); 32 | } 33 | } ``` -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Export device management 2 | export { Devices, devices } from './devices/Devices.js'; 3 | export { SimulatorDevice } from './devices/SimulatorDevice.js'; 4 | 5 | // Export individual simulator components for testing 6 | export { SimulatorBoot } from './devices/SimulatorBoot.js'; 7 | export { SimulatorReset } from './devices/SimulatorReset.js'; 8 | export { SimulatorApps } from './devices/SimulatorApps.js'; 9 | export { SimulatorUI } from './devices/SimulatorUI.js'; 10 | export { SimulatorInfo } from './devices/SimulatorInfo.js'; 11 | 12 | // Export Xcode project management 13 | export { Xcode, xcode } from './projects/Xcode.js'; 14 | export { XcodeProject } from './projects/XcodeProject.js'; 15 | export { SwiftPackage } from './projects/SwiftPackage.js'; 16 | 17 | // Export Xcode components for testing 18 | export { XcodeBuild } from './projects/XcodeBuild.js'; 19 | export { XcodeArchive } from './projects/XcodeArchive.js'; 20 | export { XcodeInfo } from './projects/XcodeInfo.js'; 21 | export { SwiftBuild } from './projects/SwiftBuild.js'; 22 | export { SwiftPackageInfo } from './projects/SwiftPackageInfo.js'; ``` -------------------------------------------------------------------------------- /src/domain/errors/DomainError.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Base domain error classes that ensure consistent error messages 3 | * Each domain object can extend these for type-safe, consistent errors 4 | */ 5 | 6 | export abstract class DomainError extends Error { 7 | constructor(message: string) { 8 | super(message); 9 | this.name = this.constructor.name; 10 | } 11 | } 12 | 13 | // Common validation error patterns with consistent messages 14 | export abstract class DomainEmptyError extends DomainError { 15 | constructor(fieldDisplayName: string) { 16 | super(`${fieldDisplayName} cannot be empty`); 17 | } 18 | } 19 | 20 | export abstract class DomainRequiredError extends DomainError { 21 | constructor(fieldDisplayName: string) { 22 | super(`${fieldDisplayName} is required`); 23 | } 24 | } 25 | 26 | export abstract class DomainInvalidTypeError extends DomainError { 27 | constructor(fieldDisplayName: string, expectedType: string) { 28 | super(`${fieldDisplayName} must be a ${expectedType}`); 29 | } 30 | } 31 | 32 | export abstract class DomainInvalidFormatError extends DomainError { 33 | // This one varies by context, so just pass the message 34 | constructor(message: string) { 35 | super(message); 36 | } 37 | } ``` -------------------------------------------------------------------------------- /test_artifacts/TestSwiftPackageXCTest/Package.swift: -------------------------------------------------------------------------------- ```swift 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "TestSwiftPackageXCTest", 8 | products: [ 9 | // Products define the executables and libraries a package produces, making them visible to other packages. 10 | .library( 11 | name: "TestSwiftPackageXCTest", 12 | targets: ["TestSwiftPackageXCTest"]), 13 | .executable( 14 | name: "TestSwiftPackageXCTestExecutable", 15 | targets: ["TestSwiftPackageXCTestExecutable"]) 16 | ], 17 | targets: [ 18 | // Targets are the basic building blocks of a package, defining a module or a test suite. 19 | // Targets can depend on other targets in this package and products from dependencies. 20 | .target( 21 | name: "TestSwiftPackageXCTest"), 22 | .executableTarget( 23 | name: "TestSwiftPackageXCTestExecutable"), 24 | .testTarget( 25 | name: "TestSwiftPackageXCTestTests", 26 | dependencies: ["TestSwiftPackageXCTest"] 27 | ), 28 | ] 29 | ) 30 | ``` -------------------------------------------------------------------------------- /src/features/simulator/domain/ListSimulatorsResult.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Platform } from '../../../shared/domain/Platform.js'; 2 | import { SimulatorState } from './SimulatorState.js'; 3 | 4 | export interface SimulatorInfo { 5 | udid: string; 6 | name: string; 7 | state: SimulatorState; 8 | platform: string; 9 | runtime: string; 10 | } 11 | 12 | // Base class for all list simulators errors 13 | export abstract class ListSimulatorsError extends Error {} 14 | 15 | // Specific error types 16 | export class SimulatorListParseError extends ListSimulatorsError { 17 | constructor() { 18 | super('Failed to parse simulator list: not valid JSON'); 19 | this.name = 'SimulatorListParseError'; 20 | } 21 | } 22 | 23 | /** 24 | * Result of listing simulators operation 25 | */ 26 | export class ListSimulatorsResult { 27 | private constructor( 28 | public readonly simulators: SimulatorInfo[], 29 | public readonly error?: Error 30 | ) {} 31 | 32 | static success(simulators: SimulatorInfo[]): ListSimulatorsResult { 33 | return new ListSimulatorsResult(simulators); 34 | } 35 | 36 | static failed(error: Error): ListSimulatorsResult { 37 | return new ListSimulatorsResult([], error); 38 | } 39 | 40 | get isSuccess(): boolean { 41 | return !this.error; 42 | } 43 | 44 | get count(): number { 45 | return this.simulators.length; 46 | } 47 | } ``` -------------------------------------------------------------------------------- /test_artifacts/TestSwiftPackageSwiftTesting/Package.swift: -------------------------------------------------------------------------------- ```swift 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "TestSwiftPackageSwiftTesting", 8 | products: [ 9 | // Products define the executables and libraries a package produces, making them visible to other packages. 10 | .library( 11 | name: "TestSwiftPackageSwiftTesting", 12 | targets: ["TestSwiftPackageSwiftTesting"]), 13 | .executable( 14 | name: "TestSwiftPackageSwiftTestingExecutable", 15 | targets: ["TestSwiftPackageSwiftTestingExecutable"]) 16 | ], 17 | targets: [ 18 | // Targets are the basic building blocks of a package, defining a module or a test suite. 19 | // Targets can depend on other targets in this package and products from dependencies. 20 | .target( 21 | name: "TestSwiftPackageSwiftTesting"), 22 | .executableTarget( 23 | name: "TestSwiftPackageSwiftTestingExecutable"), 24 | .testTarget( 25 | name: "TestSwiftPackageSwiftTestingTests", 26 | dependencies: ["TestSwiftPackageSwiftTesting"] 27 | ), 28 | ] 29 | ) 30 | ``` -------------------------------------------------------------------------------- /src/features/simulator/infrastructure/SimulatorControlAdapter.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ISimulatorControl } from '../../../application/ports/SimulatorPorts.js'; 2 | import { ICommandExecutor } from '../../../application/ports/CommandPorts.js'; 3 | 4 | /** 5 | * Controls simulator lifecycle using xcrun simctl 6 | */ 7 | export class SimulatorControlAdapter implements ISimulatorControl { 8 | constructor(private executor: ICommandExecutor) {} 9 | 10 | async boot(simulatorId: string): Promise<void> { 11 | const result = await this.executor.execute(`xcrun simctl boot "${simulatorId}"`); 12 | 13 | // Already booted is not an error 14 | if (result.exitCode !== 0 && 15 | !result.stderr.includes('Unable to boot device in current state: Booted')) { 16 | // Throw raw error - presentation layer will format it 17 | throw new Error(result.stderr); 18 | } 19 | } 20 | 21 | async shutdown(simulatorId: string): Promise<void> { 22 | const result = await this.executor.execute(`xcrun simctl shutdown "${simulatorId}"`); 23 | 24 | // Already shutdown is not an error 25 | if (result.exitCode !== 0 && 26 | !result.stderr.includes('Unable to shutdown device in current state: Shutdown')) { 27 | // Throw raw error - presentation layer will format it 28 | throw new Error(result.stderr); 29 | } 30 | } 31 | } ``` -------------------------------------------------------------------------------- /src/application/ports/SimulatorPorts.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { SimulatorState } from '../../features/simulator/domain/SimulatorState.js'; 2 | 3 | /** 4 | * Port interfaces for simulator operations 5 | * 6 | * Focused, single-responsibility interfaces following ISP 7 | */ 8 | 9 | export interface ISimulatorLocator { 10 | /** 11 | * Find a simulator by ID or name 12 | * Returns null if not found 13 | */ 14 | findSimulator(idOrName: string): Promise<SimulatorInfo | null>; 15 | 16 | /** 17 | * Find first booted simulator 18 | * Returns null if none are booted 19 | * If multiple are booted, returns one (implementation-defined which) 20 | */ 21 | findBootedSimulator(): Promise<SimulatorInfo | null>; 22 | } 23 | 24 | export interface ISimulatorControl { 25 | /** 26 | * Boot a simulator 27 | */ 28 | boot(simulatorId: string): Promise<void>; 29 | 30 | /** 31 | * Shutdown a simulator 32 | */ 33 | shutdown(simulatorId: string): Promise<void>; 34 | } 35 | 36 | export interface IAppInstaller { 37 | /** 38 | * Install an app bundle on a specific simulator 39 | * Throws if installation fails 40 | */ 41 | installApp(appPath: string, simulatorId: string): Promise<void>; 42 | } 43 | 44 | /** 45 | * Simulator information snapshot 46 | * Includes current state at time of query (not cached) 47 | */ 48 | export interface SimulatorInfo { 49 | readonly id: string; 50 | readonly name: string; 51 | readonly state: SimulatorState; 52 | readonly platform: string; 53 | readonly runtime: string; 54 | } ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectXCTest/TestProjectXCTestUITests/TestProjectXCTestUITests.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // TestProjectXCTestUITests.swift 3 | // TestProjectXCTestUITests 4 | // 5 | // Created by Stefan Dragos Nitu on 17/08/2025. 6 | // 7 | 8 | import XCTest 9 | 10 | final class TestProjectXCTestUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | @MainActor 26 | func testExample() throws { 27 | // UI tests must launch the application that they test. 28 | let app = XCUIApplication() 29 | app.launch() 30 | 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | @MainActor 35 | func testLaunchPerformance() throws { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectWatchOS/TestProjectWatchOSUITests/TestProjectWatchOSUITests.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // TestProjectWatchOSUITests.swift 3 | // TestProjectWatchOSUITests 4 | // 5 | // Created by Stefan Dragos Nitu on 22/08/2025. 6 | // 7 | 8 | import XCTest 9 | 10 | final class TestProjectWatchOSUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | @MainActor 26 | func testExample() throws { 27 | // UI tests must launch the application that they test. 28 | let app = XCUIApplication() 29 | app.launch() 30 | 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | @MainActor 35 | func testLaunchPerformance() throws { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectSwiftTesting/TestProjectSwiftTestingUITests/TestProjectSwiftTestingUITests.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // TestProjectSwiftTestingUITests.swift 3 | // TestProjectSwiftTestingUITests 4 | // 5 | // Created by Stefan Dragos Nitu on 17/08/2025. 6 | // 7 | 8 | import XCTest 9 | 10 | final class TestProjectSwiftTestingUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | @MainActor 26 | func testExample() throws { 27 | // UI tests must launch the application that they test. 28 | let app = XCUIApplication() 29 | app.launch() 30 | 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | @MainActor 35 | func testLaunchPerformance() throws { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectWatchOS/TestProjectWatchOS Watch AppUITests/TestProjectWatchOS_Watch_AppUITests.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // TestProjectWatchOS_Watch_AppUITests.swift 3 | // TestProjectWatchOS Watch AppUITests 4 | // 5 | // Created by Stefan Dragos Nitu on 22/08/2025. 6 | // 7 | 8 | import XCTest 9 | 10 | final class TestProjectWatchOS_Watch_AppUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | @MainActor 26 | func testExample() throws { 27 | // UI tests must launch the application that they test. 28 | let app = XCUIApplication() 29 | app.launch() 30 | 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | @MainActor 35 | func testLaunchPerformance() throws { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | ``` -------------------------------------------------------------------------------- /src/shared/infrastructure/ShellCommandExecutorAdapter.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ExecOptions } from 'child_process'; 2 | import { createModuleLogger } from '../../logger.js'; 3 | import { ICommandExecutor, ExecutionResult, ExecutionOptions } from '../../application/ports/CommandPorts.js'; 4 | 5 | const logger = createModuleLogger('ShellCommandExecutor'); 6 | 7 | /** 8 | * Executes shell commands via child process 9 | * Single Responsibility: Execute shell commands and return results 10 | */ 11 | export class ShellCommandExecutorAdapter implements ICommandExecutor { 12 | constructor( 13 | private readonly execAsync: ( 14 | command: string, 15 | options: ExecOptions 16 | ) => Promise<{ stdout: string; stderr: string }> 17 | ) {} 18 | 19 | /** 20 | * Execute a command and return the result 21 | */ 22 | async execute(command: string, options: ExecutionOptions = {}): Promise<ExecutionResult> { 23 | const { 24 | maxBuffer = 50 * 1024 * 1024, // 50MB default 25 | timeout = 300000, // 5 minute default 26 | shell = '/bin/bash' 27 | } = options; 28 | 29 | logger.debug({ command }, 'Executing command'); 30 | 31 | try { 32 | const { stdout, stderr } = await this.execAsync(command, { 33 | maxBuffer, 34 | timeout, 35 | shell 36 | }); 37 | 38 | return { 39 | stdout, 40 | stderr, 41 | exitCode: 0 42 | }; 43 | } catch (error: any) { 44 | // Even on failure, return the output 45 | return { 46 | stdout: error.stdout || '', 47 | stderr: error.stderr || '', 48 | exitCode: error.code || 1 49 | }; 50 | } 51 | } 52 | } ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectSwiftTesting/TestProjectSwiftTesting/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | }, 30 | { 31 | "idiom" : "mac", 32 | "scale" : "1x", 33 | "size" : "16x16" 34 | }, 35 | { 36 | "idiom" : "mac", 37 | "scale" : "2x", 38 | "size" : "16x16" 39 | }, 40 | { 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "32x32" 44 | }, 45 | { 46 | "idiom" : "mac", 47 | "scale" : "2x", 48 | "size" : "32x32" 49 | }, 50 | { 51 | "idiom" : "mac", 52 | "scale" : "1x", 53 | "size" : "128x128" 54 | }, 55 | { 56 | "idiom" : "mac", 57 | "scale" : "2x", 58 | "size" : "128x128" 59 | }, 60 | { 61 | "idiom" : "mac", 62 | "scale" : "1x", 63 | "size" : "256x256" 64 | }, 65 | { 66 | "idiom" : "mac", 67 | "scale" : "2x", 68 | "size" : "256x256" 69 | }, 70 | { 71 | "idiom" : "mac", 72 | "scale" : "1x", 73 | "size" : "512x512" 74 | }, 75 | { 76 | "idiom" : "mac", 77 | "scale" : "2x", 78 | "size" : "512x512" 79 | } 80 | ], 81 | "info" : { 82 | "author" : "xcode", 83 | "version" : 1 84 | } 85 | } 86 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectXCTest/TestProjectXCTest/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | }, 30 | { 31 | "idiom" : "mac", 32 | "scale" : "1x", 33 | "size" : "16x16" 34 | }, 35 | { 36 | "idiom" : "mac", 37 | "scale" : "2x", 38 | "size" : "16x16" 39 | }, 40 | { 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "32x32" 44 | }, 45 | { 46 | "idiom" : "mac", 47 | "scale" : "2x", 48 | "size" : "32x32" 49 | }, 50 | { 51 | "idiom" : "mac", 52 | "scale" : "1x", 53 | "size" : "128x128" 54 | }, 55 | { 56 | "idiom" : "mac", 57 | "scale" : "2x", 58 | "size" : "128x128" 59 | }, 60 | { 61 | "idiom" : "mac", 62 | "scale" : "1x", 63 | "size" : "256x256" 64 | }, 65 | { 66 | "idiom" : "mac", 67 | "scale" : "2x", 68 | "size" : "256x256" 69 | }, 70 | { 71 | "idiom" : "mac", 72 | "scale" : "1x", 73 | "size" : "512x512" 74 | }, 75 | { 76 | "idiom" : "mac", 77 | "scale" : "2x", 78 | "size" : "512x512" 79 | } 80 | ], 81 | "info" : { 82 | "author" : "xcode", 83 | "version" : 1 84 | } 85 | } 86 | ``` -------------------------------------------------------------------------------- /src/presentation/formatters/strategies/BuildIssuesStrategy.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BuildIssue } from '../../../features/build/domain/BuildIssue.js'; 2 | import { ErrorFormattingStrategy } from './ErrorFormattingStrategy.js'; 3 | 4 | /** 5 | * Formats build issues (errors and warnings) 6 | */ 7 | export class BuildIssuesStrategy implements ErrorFormattingStrategy { 8 | canFormat(error: any): boolean { 9 | return !!(error && error.issues && Array.isArray(error.issues) && 10 | error.issues.some((i: any) => i instanceof BuildIssue)); 11 | } 12 | 13 | format(error: any): string { 14 | // Filter to only actual BuildIssue instances 15 | const issues = (error.issues as any[]).filter(i => i instanceof BuildIssue); 16 | const errors = issues.filter(i => i.type === 'error'); 17 | const warnings = issues.filter(i => i.type === 'warning'); 18 | 19 | let message = ''; 20 | if (errors.length > 0) { 21 | message += `❌ Errors (${errors.length}):\n`; 22 | message += errors.slice(0, 5).map(e => ` • ${e.toString()}`).join('\n'); 23 | if (errors.length > 5) { 24 | message += `\n ... and ${errors.length - 5} more errors`; 25 | } 26 | } 27 | 28 | if (warnings.length > 0) { 29 | if (message) message += '\n\n'; 30 | message += `⚠️ Warnings (${warnings.length}):\n`; 31 | message += warnings.slice(0, 3).map(w => ` • ${w.toString()}`).join('\n'); 32 | if (warnings.length > 3) { 33 | message += `\n ... and ${warnings.length - 3} more warnings`; 34 | } 35 | } 36 | 37 | return message || error.message || 'Build failed'; 38 | } 39 | } ``` -------------------------------------------------------------------------------- /src/features/simulator/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Controllers 2 | export { BootSimulatorController } from './controllers/BootSimulatorController.js'; 3 | export { ShutdownSimulatorController } from './controllers/ShutdownSimulatorController.js'; 4 | export { ListSimulatorsController } from './controllers/ListSimulatorsController.js'; 5 | 6 | // Use Cases 7 | export { BootSimulatorUseCase } from './use-cases/BootSimulatorUseCase.js'; 8 | export { ShutdownSimulatorUseCase } from './use-cases/ShutdownSimulatorUseCase.js'; 9 | export { ListSimulatorsUseCase } from './use-cases/ListSimulatorsUseCase.js'; 10 | 11 | // Factories 12 | export { BootSimulatorControllerFactory } from './factories/BootSimulatorControllerFactory.js'; 13 | export { ShutdownSimulatorControllerFactory } from './factories/ShutdownSimulatorControllerFactory.js'; 14 | export { ListSimulatorsControllerFactory } from './factories/ListSimulatorsControllerFactory.js'; 15 | 16 | // Domain 17 | export { BootRequest } from './domain/BootRequest.js'; 18 | export { ShutdownRequest } from './domain/ShutdownRequest.js'; 19 | export { ListSimulatorsRequest } from './domain/ListSimulatorsRequest.js'; 20 | export { SimulatorState } from './domain/SimulatorState.js'; 21 | export { 22 | BootResult, 23 | BootOutcome, 24 | SimulatorNotFoundError, 25 | BootCommandFailedError, 26 | SimulatorBusyError 27 | } from './domain/BootResult.js'; 28 | 29 | // Infrastructure 30 | export { SimulatorControlAdapter } from './infrastructure/SimulatorControlAdapter.js'; 31 | export { SimulatorLocatorAdapter } from './infrastructure/SimulatorLocatorAdapter.js'; ``` -------------------------------------------------------------------------------- /src/presentation/decorators/DependencyCheckingDecorator.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { MCPController } from '../interfaces/MCPController.js'; 2 | import { MCPResponse } from '../interfaces/MCPResponse.js'; 3 | import { IDependencyChecker } from '../interfaces/IDependencyChecker.js'; 4 | 5 | /** 6 | * Decorator that checks dependencies before executing the controller 7 | * 8 | * Follows the Decorator pattern to add dependency checking behavior 9 | * without modifying the original controller 10 | */ 11 | export class DependencyCheckingDecorator implements MCPController { 12 | // Delegate properties to decoratee 13 | get name(): string { return this.decoratee.name; } 14 | get description(): string { return this.decoratee.description; } 15 | get inputSchema(): object { return this.decoratee.inputSchema; } 16 | 17 | constructor( 18 | private readonly decoratee: MCPController, 19 | private readonly requiredDependencies: string[], 20 | private readonly dependencyChecker: IDependencyChecker 21 | ) {} 22 | 23 | async execute(args: unknown): Promise<MCPResponse> { 24 | // Check dependencies first 25 | const missing = await this.dependencyChecker.check(this.requiredDependencies); 26 | 27 | if (missing.length > 0) { 28 | // Dependencies missing - return error without executing 29 | let text = '❌ Missing required dependencies:\n'; 30 | for (const dep of missing) { 31 | text += `\n • ${dep.name}`; 32 | if (dep.installCommand) { 33 | text += `: ${dep.installCommand}`; 34 | } 35 | } 36 | 37 | return { 38 | content: [{ type: 'text', text }] 39 | }; 40 | } 41 | 42 | // All dependencies available - delegate to actual controller 43 | return this.decoratee.execute(args); 44 | } 45 | 46 | getToolDefinition() { 47 | // Delegate to decoratee 48 | return this.decoratee.getToolDefinition(); 49 | } 50 | } ``` -------------------------------------------------------------------------------- /src/features/simulator/domain/ShutdownResult.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Domain entity representing the result of a shutdown operation 3 | */ 4 | 5 | // Error types specific to shutdown operations 6 | export abstract class ShutdownError extends Error {} 7 | 8 | export class SimulatorNotFoundError extends ShutdownError { 9 | constructor(public readonly deviceId: string) { 10 | super(deviceId); 11 | this.name = 'SimulatorNotFoundError'; 12 | } 13 | } 14 | 15 | export class ShutdownCommandFailedError extends ShutdownError { 16 | constructor(public readonly stderr: string) { 17 | super(stderr); 18 | this.name = 'ShutdownCommandFailedError'; 19 | } 20 | } 21 | 22 | // Possible outcomes of a shutdown operation 23 | export enum ShutdownOutcome { 24 | Shutdown = 'shutdown', 25 | AlreadyShutdown = 'already_shutdown', 26 | Failed = 'failed' 27 | } 28 | 29 | // Diagnostics information about the shutdown operation 30 | interface ShutdownDiagnostics { 31 | simulatorId?: string; 32 | simulatorName?: string; 33 | error?: Error; 34 | } 35 | 36 | export class ShutdownResult { 37 | private constructor( 38 | public readonly outcome: ShutdownOutcome, 39 | public readonly diagnostics: ShutdownDiagnostics 40 | ) {} 41 | 42 | static shutdown(simulatorId: string, simulatorName: string): ShutdownResult { 43 | return new ShutdownResult( 44 | ShutdownOutcome.Shutdown, 45 | { simulatorId, simulatorName } 46 | ); 47 | } 48 | 49 | static alreadyShutdown(simulatorId: string, simulatorName: string): ShutdownResult { 50 | return new ShutdownResult( 51 | ShutdownOutcome.AlreadyShutdown, 52 | { simulatorId, simulatorName } 53 | ); 54 | } 55 | 56 | static failed(simulatorId: string | undefined, simulatorName: string | undefined, error: Error): ShutdownResult { 57 | return new ShutdownResult( 58 | ShutdownOutcome.Failed, 59 | { simulatorId, simulatorName, error } 60 | ); 61 | } 62 | } ``` -------------------------------------------------------------------------------- /src/infrastructure/services/DependencyChecker.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { IDependencyChecker, MissingDependency } from '../../presentation/interfaces/IDependencyChecker.js'; 2 | import { ICommandExecutor } from '../../application/ports/CommandPorts.js'; 3 | 4 | /** 5 | * Checks for system dependencies using shell commands 6 | */ 7 | export class DependencyChecker implements IDependencyChecker { 8 | private readonly dependencyMap: Record<string, { checkCommand: string; installCommand?: string }> = { 9 | 'xcodebuild': { 10 | checkCommand: 'which xcodebuild', 11 | installCommand: 'Install Xcode from the App Store' 12 | }, 13 | 'xcrun': { 14 | checkCommand: 'which xcrun', 15 | installCommand: 'Install Xcode Command Line Tools: xcode-select --install' 16 | }, 17 | 'xcbeautify': { 18 | checkCommand: 'which xcbeautify', 19 | installCommand: 'brew install xcbeautify' 20 | } 21 | }; 22 | 23 | constructor( 24 | private readonly executor: ICommandExecutor 25 | ) {} 26 | 27 | async check(dependencies: string[]): Promise<MissingDependency[]> { 28 | const missing: MissingDependency[] = []; 29 | 30 | for (const dep of dependencies) { 31 | const config = this.dependencyMap[dep]; 32 | if (!config) { 33 | // Unknown dependency - just check with 'which' 34 | const result = await this.executor.execute(`which ${dep}`, { 35 | shell: '/bin/bash' 36 | }); 37 | 38 | if (result.exitCode !== 0) { 39 | missing.push({ name: dep }); 40 | } 41 | continue; 42 | } 43 | 44 | // Check using configured command 45 | const result = await this.executor.execute(config.checkCommand, { 46 | shell: '/bin/bash' 47 | }); 48 | 49 | if (result.exitCode !== 0) { 50 | missing.push({ 51 | name: dep, 52 | installCommand: config.installCommand 53 | }); 54 | } 55 | } 56 | 57 | return missing; 58 | } 59 | } ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectXCTest/TestProjectXCTest/ContentView.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // ContentView.swift 3 | // TestProjectXCTest 4 | // 5 | // Created by Stefan Dragos Nitu on 17/08/2025. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftData 10 | 11 | struct ContentView: View { 12 | @Environment(\.modelContext) private var modelContext 13 | @Query private var items: [Item] 14 | 15 | var body: some View { 16 | NavigationSplitView { 17 | List { 18 | ForEach(items) { item in 19 | NavigationLink { 20 | Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))") 21 | } label: { 22 | Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard)) 23 | } 24 | } 25 | .onDelete(perform: deleteItems) 26 | } 27 | #if os(macOS) 28 | .navigationSplitViewColumnWidth(min: 180, ideal: 200) 29 | #endif 30 | .toolbar { 31 | #if os(iOS) 32 | ToolbarItem(placement: .navigationBarTrailing) { 33 | EditButton() 34 | } 35 | #endif 36 | ToolbarItem { 37 | Button(action: addItem) { 38 | Label("Add Item", systemImage: "plus") 39 | } 40 | } 41 | } 42 | } detail: { 43 | Text("Select an item") 44 | } 45 | } 46 | 47 | private func addItem() { 48 | withAnimation { 49 | let newItem = Item(timestamp: Date()) 50 | modelContext.insert(newItem) 51 | } 52 | } 53 | 54 | private func deleteItems(offsets: IndexSet) { 55 | withAnimation { 56 | for index in offsets { 57 | modelContext.delete(items[index]) 58 | } 59 | } 60 | } 61 | } 62 | 63 | #Preview { 64 | ContentView() 65 | .modelContainer(for: Item.self, inMemory: true) 66 | } 67 | ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectSwiftTesting/TestProjectSwiftTesting/ContentView.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // ContentView.swift 3 | // TestProjectSwiftTesting 4 | // 5 | // Created by Stefan Dragos Nitu on 17/08/2025. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftData 10 | 11 | struct ContentView: View { 12 | @Environment(\.modelContext) private var modelContext 13 | @Query private var items: [Item] 14 | 15 | var body: some View { 16 | NavigationSplitView { 17 | List { 18 | ForEach(items) { item in 19 | NavigationLink { 20 | Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))") 21 | } label: { 22 | Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard)) 23 | } 24 | } 25 | .onDelete(perform: deleteItems) 26 | } 27 | #if os(macOS) 28 | .navigationSplitViewColumnWidth(min: 180, ideal: 200) 29 | #endif 30 | .toolbar { 31 | #if os(iOS) 32 | ToolbarItem(placement: .navigationBarTrailing) { 33 | EditButton() 34 | } 35 | #endif 36 | ToolbarItem { 37 | Button(action: addItem) { 38 | Label("Add Item", systemImage: "plus") 39 | } 40 | } 41 | } 42 | } detail: { 43 | Text("Select an item") 44 | } 45 | } 46 | 47 | private func addItem() { 48 | withAnimation { 49 | let newItem = Item(timestamp: Date()) 50 | modelContext.insert(newItem) 51 | } 52 | } 53 | 54 | private func deleteItems(offsets: IndexSet) { 55 | withAnimation { 56 | for index in offsets { 57 | modelContext.delete(items[index]) 58 | } 59 | } 60 | } 61 | } 62 | 63 | #Preview { 64 | ContentView() 65 | .modelContainer(for: Item.self, inMemory: true) 66 | } 67 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "mcp-xcode-server", 3 | "version": "0.6.0", 4 | "description": "MCP server for Xcode - build, test, run, and manage Apple platform projects (iOS, macOS, tvOS, watchOS, visionOS)", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "bin": { 8 | "mcp-xcode-server": "dist/cli.js" 9 | }, 10 | "scripts": { 11 | "build": "tsc", 12 | "start": "node dist/index.js", 13 | "dev": "tsc && node dist/index.js", 14 | "test": "jest && jest --config jest.e2e.config.cjs --runInBand", 15 | "test:coverage": "jest --coverage", 16 | "test:unit": "jest '*.unit.test.ts'", 17 | "test:integration": "jest '*.integration.test.ts'", 18 | "test:e2e": "jest --config jest.e2e.config.cjs --runInBand", 19 | "postinstall": "cd XcodeProjectModifier && swift build -c release", 20 | "build:modifier": "cd XcodeProjectModifier && swift build -c release" 21 | }, 22 | "keywords": [ 23 | "mcp", 24 | "apple", 25 | "ios", 26 | "macos", 27 | "tvos", 28 | "watchos", 29 | "visionos", 30 | "simulator", 31 | "xcode", 32 | "spm", 33 | "swift" 34 | ], 35 | "author": "Stefan", 36 | "license": "MIT", 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/yourusername/mcp-xcode-server.git" 40 | }, 41 | "bugs": { 42 | "url": "https://github.com/yourusername/mcp-xcode-server/issues" 43 | }, 44 | "homepage": "https://github.com/yourusername/mcp-xcode-server#readme", 45 | "files": [ 46 | "dist/**/*", 47 | "XcodeProjectModifier/**/*", 48 | "README.md", 49 | "LICENSE" 50 | ], 51 | "engines": { 52 | "node": ">=18.0.0" 53 | }, 54 | "dependencies": { 55 | "@modelcontextprotocol/sdk": "^1.17.3", 56 | "commander": "^12.0.0", 57 | "fast-xml-parser": "^5.2.5", 58 | "pino": "^9.9.0", 59 | "pino-pretty": "^13.1.1" 60 | }, 61 | "devDependencies": { 62 | "@types/jest": "^30.0.0", 63 | "@types/node": "^20.11.0", 64 | "jest": "^30.1.1", 65 | "regexp-tree": "^0.1.27", 66 | "ts-jest": "^29.4.1", 67 | "typescript": "^5.3.3" 68 | } 69 | } 70 | ``` -------------------------------------------------------------------------------- /src/shared/domain/Platform.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Domain Value Object: Platform enum 3 | * Represents the supported Apple platforms 4 | */ 5 | export enum Platform { 6 | iOS = 'iOS', 7 | macOS = 'macOS', 8 | tvOS = 'tvOS', 9 | watchOS = 'watchOS', 10 | visionOS = 'visionOS' 11 | } 12 | 13 | /** 14 | * Platform validation and parsing utilities 15 | */ 16 | export namespace Platform { 17 | /** 18 | * Parse a string into a Platform enum value 19 | * @throws Error if the string is not a valid platform 20 | */ 21 | export function parse(value: unknown): Platform { 22 | // Type check 23 | if (typeof value !== 'string') { 24 | throw new InvalidTypeError(value); 25 | } 26 | 27 | // Check if valid platform - filter out namespace functions 28 | const validPlatforms = Object.values(Platform).filter(v => typeof v === 'string') as string[]; 29 | if (!validPlatforms.includes(value)) { 30 | throw new InvalidPlatformError(value, validPlatforms); 31 | } 32 | 33 | return value as Platform; 34 | } 35 | 36 | /** 37 | * Parse a string into a Platform enum value or return undefined 38 | */ 39 | export function parseOptional(value: unknown): Platform | undefined { 40 | if (value === undefined || value === null) { 41 | return undefined; 42 | } 43 | return parse(value); 44 | } 45 | 46 | // Error classes 47 | export class InvalidTypeError extends Error { 48 | constructor(public readonly providedValue: unknown) { 49 | const validValues = Object.values(Platform).filter(v => typeof v === 'string') as string[]; 50 | super(`Platform must be a string (one of: ${validValues.join(', ')}), got ${typeof providedValue}`); 51 | this.name = 'Platform.InvalidTypeError'; 52 | } 53 | } 54 | 55 | export class InvalidPlatformError extends Error { 56 | constructor( 57 | public readonly providedValue: unknown, 58 | public readonly validValues: string[] 59 | ) { 60 | super(`Invalid platform: ${providedValue}. Valid values are: ${validValues.join(', ')}`); 61 | this.name = 'Platform.InvalidPlatformError'; 62 | } 63 | } 64 | } ``` -------------------------------------------------------------------------------- /src/shared/tests/mocks/selectiveExecMock.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { ExecOptions, ChildProcess } from 'child_process'; 2 | import type { NodeExecError } from '../types/execTypes.js'; 3 | 4 | type ExecCallback = (error: NodeExecError | null, stdout: string, stderr: string) => void; 5 | type ExecFunction = { 6 | (command: string, callback: ExecCallback): ChildProcess; 7 | (command: string, options: ExecOptions, callback: ExecCallback): ChildProcess; 8 | }; 9 | 10 | export interface MockResponse { 11 | error?: NodeExecError; 12 | stdout?: string; 13 | stderr?: string; 14 | } 15 | 16 | /** 17 | * Creates a selective exec mock that only mocks specific commands 18 | * and delegates others to the real exec implementation 19 | */ 20 | export function createSelectiveExecMock( 21 | commandFilter: (cmd: string) => boolean, 22 | getMockResponse: () => MockResponse | undefined, 23 | actualExec: ExecFunction 24 | ) { 25 | return (cmd: string, ...args: unknown[]) => { 26 | // Handle both (cmd, callback) and (cmd, options, callback) signatures 27 | const callback = typeof args[0] === 'function' ? args[0] as ExecCallback : args[1] as ExecCallback; 28 | const options = typeof args[0] === 'function' ? {} : args[0] as ExecOptions; 29 | 30 | if (commandFilter(cmd)) { 31 | const response = getMockResponse(); 32 | if (response) { 33 | process.nextTick(() => { 34 | if (response.error) { 35 | callback(response.error, response.stdout || '', response.stderr || ''); 36 | } else { 37 | callback(null, response.stdout || '', response.stderr || ''); 38 | } 39 | }); 40 | } else { 41 | process.nextTick(() => { 42 | const error = new Error(`No mock response configured for: ${cmd}`) as NodeExecError; 43 | error.code = 1; 44 | error.stdout = ''; 45 | error.stderr = ''; 46 | callback(error, '', ''); 47 | }); 48 | } 49 | return; 50 | } 51 | 52 | // Delegate to real exec for other commands 53 | return actualExec(cmd, options, callback); 54 | }; 55 | } ``` -------------------------------------------------------------------------------- /src/features/simulator/domain/SimulatorState.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Simulator state enum 3 | * Values match xcrun simctl output exactly for direct comparison 4 | */ 5 | export enum SimulatorState { 6 | Booted = 'Booted', 7 | Booting = 'Booting', 8 | Shutdown = 'Shutdown', 9 | ShuttingDown = 'Shutting Down' 10 | } 11 | 12 | /** 13 | * SimulatorState validation and parsing utilities 14 | */ 15 | export namespace SimulatorState { 16 | /** 17 | * Parse a string into a SimulatorState enum value 18 | * @throws Error if the string is not a valid state 19 | */ 20 | export function parse(value: unknown): SimulatorState { 21 | // Type check 22 | if (typeof value !== 'string') { 23 | throw new InvalidTypeError(value); 24 | } 25 | 26 | // Check if valid state - filter out namespace functions 27 | const validStates = Object.values(SimulatorState).filter(v => typeof v === 'string') as string[]; 28 | if (!validStates.includes(value)) { 29 | throw new InvalidStateError(value, validStates); 30 | } 31 | 32 | return value as SimulatorState; 33 | } 34 | 35 | /** 36 | * Parse a string into a SimulatorState enum value or return undefined 37 | */ 38 | export function parseOptional(value: unknown): SimulatorState | undefined { 39 | if (value === undefined || value === null) { 40 | return undefined; 41 | } 42 | return parse(value); 43 | } 44 | 45 | // Error classes 46 | export class InvalidTypeError extends Error { 47 | constructor(public readonly providedValue: unknown) { 48 | const validValues = Object.values(SimulatorState).filter(v => typeof v === 'string') as string[]; 49 | super(`Simulator state must be a string (one of: ${validValues.join(', ')}), got ${typeof providedValue}`); 50 | this.name = 'SimulatorState.InvalidTypeError'; 51 | } 52 | } 53 | 54 | export class InvalidStateError extends Error { 55 | constructor( 56 | public readonly providedValue: unknown, 57 | public readonly validValues: string[] 58 | ) { 59 | super(`Invalid simulator state: ${providedValue}. Valid values are: ${validValues.join(', ')}`); 60 | this.name = 'SimulatorState.InvalidStateError'; 61 | } 62 | } 63 | } ``` -------------------------------------------------------------------------------- /src/features/simulator/factories/BootSimulatorControllerFactory.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { exec } from 'child_process'; 2 | import { promisify } from 'util'; 3 | import { BootSimulatorUseCase } from '../use-cases/BootSimulatorUseCase.js'; 4 | import { BootSimulatorController } from '../controllers/BootSimulatorController.js'; 5 | import { MCPController } from '../../../presentation/interfaces/MCPController.js'; 6 | import { SimulatorLocatorAdapter } from '../infrastructure/SimulatorLocatorAdapter.js'; 7 | import { SimulatorControlAdapter } from '../infrastructure/SimulatorControlAdapter.js'; 8 | import { ShellCommandExecutorAdapter } from '../../../shared/infrastructure/ShellCommandExecutorAdapter.js'; 9 | import { DependencyCheckingDecorator } from '../../../presentation/decorators/DependencyCheckingDecorator.js'; 10 | import { DependencyChecker } from '../../../infrastructure/services/DependencyChecker.js'; 11 | 12 | /** 13 | * Factory class for creating BootSimulatorController with all dependencies 14 | * This is the composition root for the boot simulator functionality 15 | */ 16 | export class BootSimulatorControllerFactory { 17 | static create(): MCPController { 18 | // Create the shell executor that all adapters will use 19 | const execAsync = promisify(exec); 20 | const executor = new ShellCommandExecutorAdapter(execAsync); 21 | 22 | // Create infrastructure adapters 23 | const simulatorLocator = new SimulatorLocatorAdapter(executor); 24 | const simulatorControl = new SimulatorControlAdapter(executor); 25 | 26 | // Create the use case with all dependencies 27 | const useCase = new BootSimulatorUseCase( 28 | simulatorLocator, 29 | simulatorControl 30 | ); 31 | 32 | // Create the controller 33 | const controller = new BootSimulatorController(useCase); 34 | 35 | // Create dependency checker 36 | const dependencyChecker = new DependencyChecker(executor); 37 | 38 | // Wrap with dependency checking decorator 39 | const decoratedController = new DependencyCheckingDecorator( 40 | controller, 41 | ['xcrun'], // simctl is part of xcrun 42 | dependencyChecker 43 | ); 44 | 45 | return decoratedController; 46 | } 47 | } ``` -------------------------------------------------------------------------------- /src/features/simulator/tests/unit/BootRequest.unit.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect } from '@jest/globals'; 2 | import { BootRequest } from '../../domain/BootRequest.js'; 3 | import { DeviceId } from '../../../../shared/domain/DeviceId.js'; 4 | 5 | describe('BootRequest', () => { 6 | describe('create', () => { 7 | it('should create a valid boot request with simulator UUID', () => { 8 | // Arrange 9 | const deviceIdString = 'ABC123-DEF456-789'; 10 | const deviceId = DeviceId.create(deviceIdString); 11 | 12 | // Act 13 | const request = BootRequest.create(deviceId); 14 | 15 | // Assert 16 | expect(request.deviceId).toBe(deviceIdString); 17 | }); 18 | 19 | it('should create a valid boot request with simulator name', () => { 20 | // Arrange 21 | const deviceName = 'iPhone 15 Pro'; 22 | const deviceId = DeviceId.create(deviceName); 23 | 24 | // Act 25 | const request = BootRequest.create(deviceId); 26 | 27 | // Assert 28 | expect(request.deviceId).toBe(deviceName); 29 | }); 30 | 31 | it('should throw error for empty device ID', () => { 32 | // Arrange 33 | const emptyId = ''; 34 | 35 | // Act & Assert 36 | expect(() => DeviceId.create(emptyId)).toThrow('Device ID cannot be empty'); 37 | }); 38 | 39 | it('should throw error for whitespace-only device ID', () => { 40 | // Arrange 41 | const whitespaceId = ' '; 42 | 43 | // Act & Assert 44 | expect(() => DeviceId.create(whitespaceId)).toThrow('Device ID cannot be whitespace only'); 45 | }); 46 | 47 | it('should be immutable', () => { 48 | // Arrange 49 | const deviceId = DeviceId.create('ABC123'); 50 | const request = BootRequest.create(deviceId); 51 | 52 | // Act & Assert 53 | expect(() => { 54 | (request as any).deviceId = 'changed'; 55 | }).toThrow(); 56 | }); 57 | 58 | it('should trim whitespace from device ID', () => { 59 | // Arrange 60 | const idWithSpaces = ' iPhone 15 '; 61 | const deviceId = DeviceId.create(idWithSpaces); 62 | 63 | // Act 64 | const request = BootRequest.create(deviceId); 65 | 66 | // Assert 67 | expect(request.deviceId).toBe('iPhone 15'); 68 | }); 69 | }); 70 | }); ``` -------------------------------------------------------------------------------- /src/shared/tests/utils/gitResetTestArtifacts.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Utility to reset test artifacts using git 3 | * This ensures consistent cleanup across all test utilities 4 | */ 5 | 6 | import { execSync } from 'child_process'; 7 | import { resolve } from 'path'; 8 | import { createModuleLogger } from '../../../logger.js'; 9 | 10 | const logger = createModuleLogger('GitResetTestArtifacts'); 11 | 12 | /** 13 | * Reset test_artifacts directory to pristine git state 14 | * @param path - Optional specific path within test_artifacts to reset 15 | */ 16 | export function gitResetTestArtifacts(path?: string): void { 17 | const targetPath = path || 'test_artifacts/'; 18 | 19 | try { 20 | // Remove untracked files and directories (build artifacts) 21 | execSync(`git clean -fdx ${targetPath}`, { 22 | cwd: resolve(process.cwd()), 23 | stdio: 'pipe' 24 | }); 25 | 26 | // First unstage any staged changes 27 | execSync(`git reset HEAD ${targetPath}`, { 28 | cwd: resolve(process.cwd()), 29 | stdio: 'pipe' 30 | }); 31 | 32 | // Then reset any modified tracked files 33 | execSync(`git checkout -- ${targetPath}`, { 34 | cwd: resolve(process.cwd()), 35 | stdio: 'pipe' 36 | }); 37 | 38 | logger.debug({ path: targetPath }, 'Reset test artifacts using git'); 39 | } catch (error) { 40 | logger.error({ error, path: targetPath }, 'Failed to reset test artifacts'); 41 | // Don't throw - cleanup should be best effort 42 | } 43 | } 44 | 45 | /** 46 | * Reset a specific file within test_artifacts 47 | * @param filePath - Path to the file relative to project root 48 | */ 49 | export function gitResetFile(filePath: string): void { 50 | try { 51 | // Only reset if the file is within test_artifacts 52 | if (!filePath.includes('test_artifacts')) { 53 | logger.warn({ filePath }, 'Attempting to reset file outside test_artifacts - skipping'); 54 | return; 55 | } 56 | 57 | execSync(`git checkout -- ${filePath}`, { 58 | cwd: resolve(process.cwd()), 59 | stdio: 'pipe' 60 | }); 61 | 62 | logger.debug({ filePath }, 'Reset file using git'); 63 | } catch (error) { 64 | logger.warn({ error, filePath }, 'Failed to reset file - may be untracked'); 65 | } 66 | } ``` -------------------------------------------------------------------------------- /src/features/simulator/factories/ShutdownSimulatorControllerFactory.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { exec } from 'child_process'; 2 | import { promisify } from 'util'; 3 | import { ShutdownSimulatorUseCase } from '../use-cases/ShutdownSimulatorUseCase.js'; 4 | import { ShutdownSimulatorController } from '../controllers/ShutdownSimulatorController.js'; 5 | import { MCPController } from '../../../presentation/interfaces/MCPController.js'; 6 | import { SimulatorLocatorAdapter } from '../infrastructure/SimulatorLocatorAdapter.js'; 7 | import { SimulatorControlAdapter } from '../infrastructure/SimulatorControlAdapter.js'; 8 | import { ShellCommandExecutorAdapter } from '../../../shared/infrastructure/ShellCommandExecutorAdapter.js'; 9 | import { DependencyCheckingDecorator } from '../../../presentation/decorators/DependencyCheckingDecorator.js'; 10 | import { DependencyChecker } from '../../../infrastructure/services/DependencyChecker.js'; 11 | 12 | /** 13 | * Factory class for creating ShutdownSimulatorController with all dependencies 14 | * This is the composition root for the shutdown simulator functionality 15 | */ 16 | export class ShutdownSimulatorControllerFactory { 17 | static create(): MCPController { 18 | // Create the shell executor that all adapters will use 19 | const execAsync = promisify(exec); 20 | const executor = new ShellCommandExecutorAdapter(execAsync); 21 | 22 | // Create infrastructure adapters 23 | const simulatorLocator = new SimulatorLocatorAdapter(executor); 24 | const simulatorControl = new SimulatorControlAdapter(executor); 25 | 26 | // Create the use case with all dependencies 27 | const useCase = new ShutdownSimulatorUseCase( 28 | simulatorLocator, 29 | simulatorControl 30 | ); 31 | 32 | // Create the controller 33 | const controller = new ShutdownSimulatorController(useCase); 34 | 35 | // Create dependency checker 36 | const dependencyChecker = new DependencyChecker(executor); 37 | 38 | // Wrap with dependency checking decorator 39 | const decoratedController = new DependencyCheckingDecorator( 40 | controller, 41 | ['xcrun'], // simctl is part of xcrun 42 | dependencyChecker 43 | ); 44 | 45 | return decoratedController; 46 | } 47 | } ``` -------------------------------------------------------------------------------- /src/shared/domain/DeviceId.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { DomainEmptyError, DomainInvalidTypeError } from '../../domain/errors/DomainError.js'; 2 | 3 | /** 4 | * Value object for a device identifier 5 | * Can be either a device UDID or a device name 6 | * Works for both simulators and physical devices 7 | */ 8 | export class DeviceId { 9 | private constructor(private readonly value: string) {} 10 | 11 | static create(id: unknown): DeviceId { 12 | // Required check 13 | if (id === undefined || id === null) { 14 | throw new DeviceId.RequiredError(); 15 | } 16 | 17 | // Type checking 18 | if (typeof id !== 'string') { 19 | throw new DeviceId.InvalidTypeError(id); 20 | } 21 | 22 | // Empty check 23 | if (id === '') { 24 | throw new DeviceId.EmptyError(id); 25 | } 26 | 27 | // Whitespace-only check 28 | if (id.trim() === '') { 29 | throw new DeviceId.WhitespaceOnlyError(id); 30 | } 31 | 32 | return new DeviceId(id.trim()); 33 | } 34 | 35 | static createOptional(id: unknown): DeviceId | undefined { 36 | if (id === undefined || id === null) { 37 | return undefined; 38 | } 39 | return DeviceId.create(id); 40 | } 41 | 42 | toString(): string { 43 | return this.value; 44 | } 45 | 46 | equals(other: DeviceId): boolean { 47 | return this.value === other.value; 48 | } 49 | } 50 | 51 | // Nested error classes under DeviceId namespace 52 | export namespace DeviceId { 53 | export class RequiredError extends Error { 54 | constructor() { 55 | super('Device ID is required'); 56 | this.name = 'DeviceId.RequiredError'; 57 | } 58 | } 59 | 60 | export class InvalidTypeError extends DomainInvalidTypeError { 61 | constructor(public readonly providedValue: unknown) { 62 | super('Device ID', 'string'); 63 | this.name = 'DeviceId.InvalidTypeError'; 64 | } 65 | } 66 | 67 | export class EmptyError extends DomainEmptyError { 68 | constructor(public readonly providedValue: unknown) { 69 | super('Device ID'); 70 | this.name = 'DeviceId.EmptyError'; 71 | } 72 | } 73 | 74 | export class WhitespaceOnlyError extends Error { 75 | constructor(public readonly providedValue: unknown) { 76 | super('Device ID cannot be whitespace only'); 77 | this.name = 'DeviceId.WhitespaceOnlyError'; 78 | } 79 | } 80 | } ``` -------------------------------------------------------------------------------- /src/shared/infrastructure/ConfigProviderAdapter.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { IConfigProvider } from '../../application/ports/ConfigPorts.js'; 2 | import { homedir } from 'os'; 3 | import path from 'path'; 4 | 5 | /** 6 | * Infrastructure adapter for configuration access 7 | * Implements the IConfigProvider port 8 | * 9 | * Self-contained with no external dependencies. 10 | * Configuration values come from: 11 | * 1. Environment variables (when available) 12 | * 2. System defaults (e.g., user home directory) 13 | * 3. Hardcoded defaults as fallback 14 | */ 15 | export class ConfigProviderAdapter implements IConfigProvider { 16 | private readonly derivedDataBasePath: string; 17 | 18 | constructor() { 19 | // Read from environment or use default 20 | // This is an infrastructure concern - reading from the environment 21 | this.derivedDataBasePath = process.env.MCP_XCODE_DERIVED_DATA_PATH || 22 | path.join(homedir(), 'Library', 'Developer', 'Xcode', 'DerivedData', 'MCP-Xcode'); 23 | } 24 | 25 | getDerivedDataPath(projectPath?: string): string { 26 | // If we have a project path, use it for the derived data path 27 | if (projectPath) { 28 | const projectName = path.basename(projectPath, path.extname(projectPath)); 29 | return path.join(this.derivedDataBasePath, projectName); 30 | } 31 | // Otherwise return the base path 32 | return this.derivedDataBasePath; 33 | } 34 | 35 | getBuildTimeout(): number { 36 | // Read from environment or use default (10 minutes) 37 | const timeout = process.env.MCP_XCODE_BUILD_TIMEOUT; 38 | return timeout ? parseInt(timeout, 10) : 600000; 39 | } 40 | 41 | isXcbeautifyEnabled(): boolean { 42 | // Read from environment or default to true 43 | const enabled = process.env.MCP_XCODE_XCBEAUTIFY_ENABLED; 44 | return enabled ? enabled.toLowerCase() === 'true' : true; 45 | } 46 | 47 | getCustomBuildSettings(): Record<string, string> { 48 | // Could read from environment as JSON or return empty 49 | const settings = process.env.MCP_XCODE_CUSTOM_BUILD_SETTINGS; 50 | if (settings) { 51 | try { 52 | return JSON.parse(settings); 53 | } catch { 54 | // Invalid JSON, return empty 55 | return {}; 56 | } 57 | } 58 | return {}; 59 | } 60 | } ``` -------------------------------------------------------------------------------- /src/features/simulator/use-cases/ShutdownSimulatorUseCase.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ShutdownRequest } from '../domain/ShutdownRequest.js'; 2 | import { ShutdownResult, SimulatorNotFoundError, ShutdownCommandFailedError } from '../domain/ShutdownResult.js'; 3 | import { SimulatorState } from '../domain/SimulatorState.js'; 4 | import { ISimulatorLocator, ISimulatorControl } from '../../../application/ports/SimulatorPorts.js'; 5 | 6 | export interface IShutdownSimulatorUseCase { 7 | execute(request: ShutdownRequest): Promise<ShutdownResult>; 8 | } 9 | 10 | /** 11 | * Use Case: Shutdown a simulator 12 | * 13 | * Orchestrates finding the target simulator and shutting it down if needed 14 | */ 15 | export class ShutdownSimulatorUseCase implements IShutdownSimulatorUseCase { 16 | constructor( 17 | private simulatorLocator: ISimulatorLocator, 18 | private simulatorControl: ISimulatorControl 19 | ) {} 20 | 21 | async execute(request: ShutdownRequest): Promise<ShutdownResult> { 22 | // Find the simulator 23 | const simulator = await this.simulatorLocator.findSimulator(request.deviceId); 24 | 25 | if (!simulator) { 26 | return ShutdownResult.failed( 27 | request.deviceId, 28 | '', // No name available since simulator wasn't found 29 | new SimulatorNotFoundError(request.deviceId) 30 | ); 31 | } 32 | 33 | // Check simulator state 34 | if (simulator.state === SimulatorState.Shutdown) { 35 | return ShutdownResult.alreadyShutdown( 36 | simulator.id, 37 | simulator.name 38 | ); 39 | } 40 | 41 | // Handle ShuttingDown state - simulator is already shutting down 42 | if (simulator.state === SimulatorState.ShuttingDown) { 43 | return ShutdownResult.alreadyShutdown( 44 | simulator.id, 45 | simulator.name 46 | ); 47 | } 48 | 49 | // Shutdown the simulator (handles Booted and Booting states) 50 | try { 51 | await this.simulatorControl.shutdown(simulator.id); 52 | 53 | return ShutdownResult.shutdown( 54 | simulator.id, 55 | simulator.name 56 | ); 57 | } catch (error: any) { 58 | return ShutdownResult.failed( 59 | simulator.id, 60 | simulator.name, 61 | new ShutdownCommandFailedError(error.stderr || error.message || '') 62 | ); 63 | } 64 | } 65 | } ``` -------------------------------------------------------------------------------- /src/presentation/tests/unit/ErrorFormatter.unit.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ErrorFormatter } from '../../formatters/ErrorFormatter.js'; 2 | import { BuildIssue } from '../../../features/build/domain/BuildIssue.js'; 3 | 4 | describe('ErrorFormatter', () => { 5 | describe('strategy delegation', () => { 6 | it('should delegate error with BuildIssues to BuildIssuesStrategy', () => { 7 | const error = { 8 | issues: [ 9 | BuildIssue.error('Test error') 10 | ] 11 | }; 12 | 13 | const result = ErrorFormatter.format(error); 14 | 15 | // Should return formatted result 16 | expect(result).toBeDefined(); 17 | expect(typeof result).toBe('string'); 18 | expect(result.length).toBeGreaterThan(0); 19 | }); 20 | 21 | it('should delegate plain Error to DefaultErrorStrategy', () => { 22 | const error = new Error('Plain error message'); 23 | 24 | const result = ErrorFormatter.format(error); 25 | 26 | // Should return formatted result 27 | expect(result).toBeDefined(); 28 | expect(typeof result).toBe('string'); 29 | expect(result.length).toBeGreaterThan(0); 30 | }); 31 | 32 | it('should delegate unknown objects to DefaultErrorStrategy', () => { 33 | const error = { someField: 'value' }; 34 | 35 | const result = ErrorFormatter.format(error); 36 | 37 | // Should return formatted result (DefaultErrorStrategy handles everything) 38 | expect(result).toBeDefined(); 39 | expect(typeof result).toBe('string'); 40 | expect(result.length).toBeGreaterThan(0); 41 | }); 42 | 43 | it('should handle null by delegating to DefaultErrorStrategy', () => { 44 | const result = ErrorFormatter.format(null); 45 | 46 | // DefaultErrorStrategy should handle null 47 | expect(result).toBeDefined(); 48 | expect(typeof result).toBe('string'); 49 | expect(result.length).toBeGreaterThan(0); 50 | }); 51 | 52 | it('should handle undefined by delegating to DefaultErrorStrategy', () => { 53 | const result = ErrorFormatter.format(undefined); 54 | 55 | // DefaultErrorStrategy should handle undefined 56 | expect(result).toBeDefined(); 57 | expect(typeof result).toBe('string'); 58 | expect(result.length).toBeGreaterThan(0); 59 | }); 60 | }); 61 | }); ``` -------------------------------------------------------------------------------- /test_artifacts/TestProjectXCTest/TestProjectXCTestTests/TestProjectXCTestTests.swift: -------------------------------------------------------------------------------- ```swift 1 | // 2 | // TestProjectXCTestTests.swift 3 | // TestProjectXCTestTests 4 | // 5 | // Created by Stefan Dragos Nitu on 17/08/2025. 6 | // 7 | 8 | import XCTest 9 | 10 | final class TestProjectXCTestTests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | } 15 | 16 | override func tearDownWithError() throws { 17 | // Put teardown code here. This method is called after the invocation of each test method in the class. 18 | } 19 | 20 | func testExample() throws { 21 | // This is an example of a functional test case. 22 | // Use XCTAssert and related functions to verify your tests produce the correct results. 23 | // Any test you write for XCTest can be annotated as throws and async. 24 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 25 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 26 | } 27 | 28 | func testPerformanceExample() throws { 29 | // This is an example of a performance test case. 30 | measure { 31 | // Put the code you want to measure the time of here. 32 | } 33 | } 34 | 35 | func testTargetForFilter() throws { 36 | // This is an example of a functional test case. 37 | // Use XCTAssert and related functions to verify your tests produce the correct results. 38 | // Any test you write for XCTest can be annotated as throws and async. 39 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 40 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 41 | } 42 | 43 | func testFailingTest() throws { 44 | XCTFail("Test MCP failing test reporting") 45 | } 46 | 47 | func testAnotherFailure() throws { 48 | // Another failing test to verify multiple failures are handled 49 | let result = 42 50 | XCTAssertEqual(result, 100, "Expected result to be 100 but got \(result)") 51 | } 52 | 53 | } 54 | ``` -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: macos-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18.x, 20.x, 22.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | 25 | - name: Install dependencies 26 | run: npm ci 27 | 28 | - name: Build TypeScript 29 | run: npm run build 30 | 31 | - name: Run unit tests with coverage 32 | run: npm run test:unit -- --coverage 33 | 34 | - name: Run integration tests with coverage 35 | run: npm run test:integration -- --coverage 36 | 37 | - name: Upload coverage reports 38 | uses: codecov/codecov-action@v4 39 | if: matrix.node-version == '20.x' 40 | with: 41 | files: ./coverage/lcov.info 42 | flags: unittests 43 | name: codecov-umbrella 44 | fail_ci_if_error: false 45 | 46 | e2e-test: 47 | runs-on: macos-15 48 | needs: test 49 | 50 | steps: 51 | - uses: actions/checkout@v4 52 | 53 | - name: Use Node.js 20.x 54 | uses: actions/setup-node@v4 55 | with: 56 | node-version: '20.x' 57 | 58 | - name: Install dependencies 59 | run: | 60 | echo "Installing system dependencies..." 61 | which xcbeautify || brew install xcbeautify 62 | which xcbeautify 63 | echo "Installing Node.js dependencies..." 64 | npm ci 65 | 66 | - name: Build TypeScript 67 | run: npm run build 68 | 69 | - name: Run E2E tests 70 | run: npm run test:e2e 71 | timeout-minutes: 90 72 | 73 | - name: Upload test logs 74 | if: always() 75 | uses: actions/upload-artifact@v4 76 | with: 77 | name: e2e-test-logs 78 | path: | 79 | ~/.mcp-xcode-server/logs/ 80 | retention-days: 7 81 | if-no-files-found: warn 82 | 83 | lint: 84 | runs-on: macos-latest 85 | 86 | steps: 87 | - uses: actions/checkout@v4 88 | 89 | - name: Use Node.js 90 | uses: actions/setup-node@v4 91 | with: 92 | node-version: '20.x' 93 | 94 | - name: Install dependencies 95 | run: npm ci 96 | 97 | - name: Check TypeScript 98 | run: npx tsc --noEmit 99 | 100 | - name: Build 101 | run: npm run build ``` -------------------------------------------------------------------------------- /src/features/app-management/factories/InstallAppControllerFactory.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { exec } from 'child_process'; 2 | import { promisify } from 'util'; 3 | import { InstallAppUseCase } from '../use-cases/InstallAppUseCase.js'; 4 | import { InstallAppController } from '../controllers/InstallAppController.js'; 5 | import { MCPController } from '../../../presentation/interfaces/MCPController.js'; 6 | import { SimulatorLocatorAdapter } from '../../simulator/infrastructure/SimulatorLocatorAdapter.js'; 7 | import { SimulatorControlAdapter } from '../../simulator/infrastructure/SimulatorControlAdapter.js'; 8 | import { AppInstallerAdapter } from '../infrastructure/AppInstallerAdapter.js'; 9 | import { ShellCommandExecutorAdapter } from '../../../shared/infrastructure/ShellCommandExecutorAdapter.js'; 10 | import { LogManagerInstance } from '../../../utils/LogManagerInstance.js'; 11 | import { DependencyCheckingDecorator } from '../../../presentation/decorators/DependencyCheckingDecorator.js'; 12 | import { DependencyChecker } from '../../../infrastructure/services/DependencyChecker.js'; 13 | 14 | /** 15 | * Factory class for creating InstallAppController with all dependencies 16 | * This is the composition root for the install app functionality 17 | */ 18 | export class InstallAppControllerFactory { 19 | static create(): MCPController { 20 | // Create the shell executor that all adapters will use 21 | const execAsync = promisify(exec); 22 | const executor = new ShellCommandExecutorAdapter(execAsync); 23 | 24 | // Create infrastructure adapters 25 | const simulatorLocator = new SimulatorLocatorAdapter(executor); 26 | const simulatorControl = new SimulatorControlAdapter(executor); 27 | const appInstaller = new AppInstallerAdapter(executor); 28 | const logManager = new LogManagerInstance(); 29 | 30 | // Create the use case with all dependencies 31 | const useCase = new InstallAppUseCase( 32 | simulatorLocator, 33 | simulatorControl, 34 | appInstaller, 35 | logManager 36 | ); 37 | 38 | // Create the controller 39 | const controller = new InstallAppController(useCase); 40 | 41 | // Create dependency checker 42 | const dependencyChecker = new DependencyChecker(executor); 43 | 44 | // Wrap with dependency checking decorator 45 | const decoratedController = new DependencyCheckingDecorator( 46 | controller, 47 | ['xcrun'], // simctl is part of xcrun 48 | dependencyChecker 49 | ); 50 | 51 | return decoratedController; 52 | } 53 | } ``` -------------------------------------------------------------------------------- /src/shared/domain/ProjectPath.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { existsSync } from 'fs'; 2 | import path from 'path'; 3 | import { DomainEmptyError, DomainInvalidTypeError, DomainInvalidFormatError, DomainRequiredError } from '../../domain/errors/DomainError.js'; 4 | 5 | /** 6 | * Value Object: Represents a validated project path 7 | * Ensures the path exists and is a valid Xcode project or workspace 8 | */ 9 | export class ProjectPath { 10 | private constructor(private readonly value: string) {} 11 | 12 | static create(pathString: unknown): ProjectPath { 13 | // Required check (for undefined/null) 14 | if (pathString === undefined || pathString === null) { 15 | throw new ProjectPath.RequiredError(); 16 | } 17 | 18 | // Type checking 19 | if (typeof pathString !== 'string') { 20 | throw new ProjectPath.InvalidTypeError(pathString); 21 | } 22 | 23 | // Empty check 24 | if (pathString.trim() === '') { 25 | throw new ProjectPath.EmptyError(pathString); 26 | } 27 | 28 | const trimmed = pathString.trim(); 29 | 30 | // Format validation 31 | const ext = path.extname(trimmed); 32 | if (ext !== '.xcodeproj' && ext !== '.xcworkspace') { 33 | throw new ProjectPath.InvalidFormatError(trimmed); 34 | } 35 | 36 | // Runtime check - this stays as a regular Error since it's not validation 37 | if (!existsSync(trimmed)) { 38 | throw new Error(`Project path does not exist: ${trimmed}`); 39 | } 40 | 41 | return new ProjectPath(trimmed); 42 | } 43 | 44 | toString(): string { 45 | return this.value; 46 | } 47 | 48 | get name(): string { 49 | return path.basename(this.value, path.extname(this.value)); 50 | } 51 | 52 | get isWorkspace(): boolean { 53 | return path.extname(this.value) === '.xcworkspace'; 54 | } 55 | } 56 | 57 | // Nested error classes under ProjectPath namespace 58 | export namespace ProjectPath { 59 | export class RequiredError extends DomainRequiredError { 60 | constructor() { 61 | super('Project path'); 62 | this.name = 'ProjectPath.RequiredError'; 63 | } 64 | } 65 | 66 | export class InvalidTypeError extends DomainInvalidTypeError { 67 | constructor(public readonly providedValue: unknown) { 68 | super('Project path', 'string'); 69 | this.name = 'ProjectPath.InvalidTypeError'; 70 | } 71 | } 72 | 73 | export class EmptyError extends DomainEmptyError { 74 | constructor(public readonly providedValue: unknown) { 75 | super('Project path'); 76 | this.name = 'ProjectPath.EmptyError'; 77 | } 78 | } 79 | 80 | export class InvalidFormatError extends DomainInvalidFormatError { 81 | constructor(public readonly path: string) { 82 | super('Project path must be an .xcodeproj or .xcworkspace file'); 83 | this.name = 'ProjectPath.InvalidFormatError'; 84 | } 85 | } 86 | } ``` -------------------------------------------------------------------------------- /src/features/simulator/use-cases/BootSimulatorUseCase.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BootRequest } from '../domain/BootRequest.js'; 2 | import { BootResult, SimulatorNotFoundError, BootCommandFailedError, SimulatorBusyError } from '../domain/BootResult.js'; 3 | import { SimulatorState } from '../domain/SimulatorState.js'; 4 | import { ISimulatorLocator, ISimulatorControl } from '../../../application/ports/SimulatorPorts.js'; 5 | 6 | export interface IBootSimulatorUseCase { 7 | execute(request: BootRequest): Promise<BootResult>; 8 | } 9 | 10 | /** 11 | * Use Case: Boot a simulator 12 | * 13 | * Orchestrates finding the target simulator and booting it if needed 14 | */ 15 | export class BootSimulatorUseCase implements IBootSimulatorUseCase { 16 | constructor( 17 | private simulatorLocator: ISimulatorLocator, 18 | private simulatorControl: ISimulatorControl 19 | ) {} 20 | 21 | async execute(request: BootRequest): Promise<BootResult> { 22 | // Find the simulator 23 | const simulator = await this.simulatorLocator.findSimulator(request.deviceId); 24 | 25 | if (!simulator) { 26 | return BootResult.failed( 27 | request.deviceId, 28 | '', // No name available since simulator wasn't found 29 | new SimulatorNotFoundError(request.deviceId) 30 | ); 31 | } 32 | 33 | // Check simulator state 34 | if (simulator.state === SimulatorState.Booted) { 35 | return BootResult.alreadyBooted( 36 | simulator.id, 37 | simulator.name, 38 | { 39 | platform: simulator.platform, 40 | runtime: simulator.runtime 41 | } 42 | ); 43 | } 44 | 45 | // Handle Booting state - simulator is already in the process of booting 46 | if (simulator.state === SimulatorState.Booting) { 47 | return BootResult.alreadyBooted( 48 | simulator.id, 49 | simulator.name, 50 | { 51 | platform: simulator.platform, 52 | runtime: simulator.runtime 53 | } 54 | ); 55 | } 56 | 57 | // Handle ShuttingDown state - can't boot while shutting down 58 | if (simulator.state === SimulatorState.ShuttingDown) { 59 | return BootResult.failed( 60 | simulator.id, 61 | simulator.name, 62 | new SimulatorBusyError(SimulatorState.ShuttingDown) 63 | ); 64 | } 65 | 66 | // Boot the simulator (handles Shutdown state) 67 | try { 68 | await this.simulatorControl.boot(simulator.id); 69 | 70 | return BootResult.booted( 71 | simulator.id, 72 | simulator.name, 73 | { 74 | platform: simulator.platform, 75 | runtime: simulator.runtime 76 | } 77 | ); 78 | } catch (error: any) { 79 | return BootResult.failed( 80 | simulator.id, 81 | simulator.name, 82 | new BootCommandFailedError(error.stderr || error.message || '') 83 | ); 84 | } 85 | } 86 | } ``` -------------------------------------------------------------------------------- /src/utils/errors/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Export types and functions from xcbeautify parser 3 | */ 4 | 5 | export { 6 | Issue, 7 | Test, 8 | XcbeautifyOutput, 9 | parseXcbeautifyOutput, 10 | formatParsedOutput 11 | } from './xcbeautify-parser.js'; 12 | 13 | import { Issue, parseXcbeautifyOutput as parseOutput } from './xcbeautify-parser.js'; 14 | 15 | // Error handlers for tools 16 | // These return MCP format for tools 17 | export function handleSwiftPackageError(error: unknown, context?: any): { content: { type: string; text: string }[] } { 18 | const message = error instanceof Error ? error.message : String(error); 19 | 20 | // Add ❌ prefix if message doesn't already have xcbeautify formatting 21 | const formattedMessage = message.includes('❌') || message.includes('⚠️') || message.includes('✅') 22 | ? message 23 | : `❌ ${message}`; 24 | 25 | const contextInfo = context 26 | ? Object.entries(context) 27 | .filter(([_, v]) => v !== undefined) 28 | .map(([k, v]) => `${k}: ${v}`) 29 | .join(', ') 30 | : ''; 31 | 32 | // Check if error has a logPath property 33 | const logPath = (error as any)?.logPath; 34 | const logInfo = logPath ? `\n\n📁 Full logs saved to: ${logPath}` : ''; 35 | 36 | return { 37 | content: [{ 38 | type: 'text', 39 | text: contextInfo ? `${formattedMessage}\n\nContext: ${contextInfo}${logInfo}` : `${formattedMessage}${logInfo}` 40 | }] 41 | }; 42 | } 43 | 44 | export function handleXcodeError(error: unknown, context?: any): { content: { type: string; text: string }[] } { 45 | const message = error instanceof Error ? error.message : String(error); 46 | 47 | // Add ❌ prefix if message doesn't already have xcbeautify formatting 48 | const formattedMessage = message.includes('❌') || message.includes('⚠️') || message.includes('✅') 49 | ? message 50 | : `❌ ${message}`; 51 | 52 | const contextInfo = context 53 | ? Object.entries(context) 54 | .filter(([_, v]) => v !== undefined) 55 | .map(([k, v]) => `${k}: ${v}`) 56 | .join(', ') 57 | : ''; 58 | 59 | // Check if error has a logPath property 60 | const logPath = (error as any)?.logPath; 61 | const logInfo = logPath ? `\n\n📁 Full logs saved to: ${logPath}` : ''; 62 | 63 | return { 64 | content: [{ 65 | type: 'text', 66 | text: contextInfo ? `${formattedMessage}\n\nContext: ${contextInfo}${logInfo}` : `${formattedMessage}${logInfo}` 67 | }] 68 | }; 69 | } 70 | 71 | // Backward compatibility for tests 72 | export function parseBuildErrors(output: string): Issue[] { 73 | const { errors, warnings } = parseOutput(output); 74 | return [...errors, ...warnings]; 75 | } 76 | 77 | export function formatBuildErrors(errors: Issue[]): string { 78 | return errors.map(e => `${e.file ? `${e.file}:${e.line}:${e.column} - ` : ''}${e.message}`).join('\n'); 79 | } ``` -------------------------------------------------------------------------------- /examples/screenshot-demo.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Demo script showing how to use the view_simulator_screen tool 5 | * to capture and view the current simulator screen 6 | */ 7 | 8 | import { Client } from '@modelcontextprotocol/sdk/client/index.js'; 9 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; 10 | import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; 11 | import { writeFileSync } from 'fs'; 12 | 13 | async function main() { 14 | // Create MCP client 15 | const transport = new StdioClientTransport({ 16 | command: 'node', 17 | args: ['../dist/index.js'], 18 | cwd: process.cwd(), 19 | }); 20 | 21 | const client = new Client({ 22 | name: 'screenshot-demo', 23 | version: '1.0.0', 24 | }, { 25 | capabilities: {} 26 | }); 27 | 28 | console.log('Connecting to MCP server...'); 29 | await client.connect(transport); 30 | 31 | try { 32 | // List available simulators 33 | console.log('Listing simulators...'); 34 | const listResponse = await client.request({ 35 | method: 'tools/call', 36 | params: { 37 | name: 'list_simulators', 38 | arguments: { 39 | platform: 'iOS' 40 | } 41 | } 42 | }, CallToolResultSchema); 43 | 44 | const devices = JSON.parse(listResponse.content[0].text); 45 | console.log(`Found ${devices.length} iOS simulators`); 46 | 47 | const bootedDevice = devices.find(d => d.state === 'Booted'); 48 | if (!bootedDevice) { 49 | console.log('No booted simulator found. Please boot a simulator first.'); 50 | return; 51 | } 52 | 53 | console.log(`Using booted simulator: ${bootedDevice.name}`); 54 | 55 | // Capture the screen 56 | console.log('Capturing simulator screen...'); 57 | const screenshotResponse = await client.request({ 58 | method: 'tools/call', 59 | params: { 60 | name: 'view_simulator_screen', 61 | arguments: { 62 | deviceId: bootedDevice.udid 63 | } 64 | } 65 | }, CallToolResultSchema); 66 | 67 | // The response contains the image data 68 | const imageContent = screenshotResponse.content[0]; 69 | if (imageContent.type === 'image') { 70 | console.log(`Screenshot captured! Image size: ${imageContent.data.length} bytes (base64)`); 71 | 72 | // Save to file for demonstration 73 | const buffer = Buffer.from(imageContent.data, 'base64'); 74 | const filename = `simulator-screen-${Date.now()}.png`; 75 | writeFileSync(filename, buffer); 76 | console.log(`Screenshot saved to: ${filename}`); 77 | 78 | // In a real MCP client like Claude Code, the image would be displayed directly 79 | console.log('In an MCP client, this image would be displayed for viewing and analysis.'); 80 | } 81 | 82 | } finally { 83 | await client.close(); 84 | console.log('Disconnected from MCP server'); 85 | } 86 | } 87 | 88 | main().catch(console.error); ``` -------------------------------------------------------------------------------- /src/features/app-management/domain/InstallResult.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { AppPath } from '../../../shared/domain/AppPath.js'; 2 | import { DeviceId } from '../../../shared/domain/DeviceId.js'; 3 | 4 | /** 5 | * Domain Entity: Represents the result of an app installation 6 | * 7 | * Separates user-facing outcome from internal diagnostics 8 | */ 9 | 10 | // User-facing outcome (what happened) 11 | export enum InstallOutcome { 12 | Succeeded = 'succeeded', 13 | Failed = 'failed' 14 | } 15 | 16 | // Base class for all install-related errors 17 | export abstract class InstallError extends Error {} 18 | 19 | // Specific error types 20 | export class AppNotFoundError extends InstallError { 21 | constructor(public readonly appPath: AppPath) { 22 | super(appPath.toString()); 23 | this.name = 'AppNotFoundError'; 24 | } 25 | } 26 | 27 | export class SimulatorNotFoundError extends InstallError { 28 | constructor(public readonly simulatorId: DeviceId) { 29 | super(simulatorId.toString()); 30 | this.name = 'SimulatorNotFoundError'; 31 | } 32 | } 33 | 34 | export class NoBootedSimulatorError extends InstallError { 35 | constructor() { 36 | super('No booted simulator found'); 37 | this.name = 'NoBootedSimulatorError'; 38 | } 39 | } 40 | 41 | export class InstallCommandFailedError extends InstallError { 42 | constructor(public readonly stderr: string) { 43 | super(stderr); 44 | this.name = 'InstallCommandFailedError'; 45 | } 46 | } 47 | 48 | // Internal diagnostics (why/how it happened) 49 | export interface InstallDiagnostics { 50 | readonly appPath: AppPath; 51 | readonly simulatorId?: DeviceId; 52 | readonly simulatorName?: string; 53 | readonly bundleId?: string; 54 | readonly error?: InstallError; 55 | readonly installedAt: Date; 56 | } 57 | 58 | // Complete result combining outcome and diagnostics 59 | export interface InstallResult { 60 | readonly outcome: InstallOutcome; 61 | readonly diagnostics: InstallDiagnostics; 62 | } 63 | 64 | export const InstallResult = { 65 | /** 66 | * Installation succeeded 67 | */ 68 | succeeded( 69 | bundleId: string, 70 | simulatorId: DeviceId, 71 | simulatorName: string, 72 | appPath: AppPath, 73 | diagnostics?: Partial<InstallDiagnostics> 74 | ): InstallResult { 75 | return Object.freeze({ 76 | outcome: InstallOutcome.Succeeded, 77 | diagnostics: Object.freeze({ 78 | bundleId, 79 | simulatorId, 80 | simulatorName, 81 | appPath, 82 | installedAt: new Date(), 83 | ...diagnostics 84 | }) 85 | }); 86 | }, 87 | 88 | /** 89 | * Installation failed 90 | */ 91 | failed( 92 | error: InstallError, 93 | appPath: AppPath, 94 | simulatorId?: DeviceId, 95 | simulatorName?: string, 96 | diagnostics?: Partial<InstallDiagnostics> 97 | ): InstallResult { 98 | return Object.freeze({ 99 | outcome: InstallOutcome.Failed, 100 | diagnostics: Object.freeze({ 101 | error, 102 | appPath, 103 | simulatorId, 104 | simulatorName, 105 | installedAt: new Date(), 106 | ...diagnostics 107 | }) 108 | }); 109 | } 110 | }; ``` -------------------------------------------------------------------------------- /src/features/simulator/domain/BootResult.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Domain entity representing the result of a boot simulator operation 3 | * 4 | * Separates user-facing outcome from internal diagnostics 5 | */ 6 | 7 | // User-facing outcome (what happened) 8 | export enum BootOutcome { 9 | Booted = 'booted', // Successfully booted the simulator 10 | AlreadyBooted = 'alreadyBooted', // Simulator was already running 11 | Failed = 'failed' // Boot failed 12 | } 13 | 14 | // Base class for all boot-related errors 15 | export abstract class BootError extends Error {} 16 | 17 | // Specific error types 18 | export class SimulatorNotFoundError extends BootError { 19 | constructor(public readonly deviceId: string) { 20 | super(deviceId); // Just store the data 21 | this.name = 'SimulatorNotFoundError'; 22 | } 23 | } 24 | 25 | export class BootCommandFailedError extends BootError { 26 | constructor(public readonly stderr: string) { 27 | super(stderr); // Just store the stderr output 28 | this.name = 'BootCommandFailedError'; 29 | } 30 | } 31 | 32 | export class SimulatorBusyError extends BootError { 33 | constructor(public readonly currentState: string) { 34 | super(currentState); // Just store the state 35 | this.name = 'SimulatorBusyError'; 36 | } 37 | } 38 | 39 | // Internal diagnostics (why/how it happened) 40 | export interface BootDiagnostics { 41 | readonly simulatorId: string; 42 | readonly simulatorName: string; 43 | readonly error?: BootError; // Any boot-specific error 44 | readonly runtime?: string; // Which iOS version 45 | readonly platform?: string; // iOS, tvOS, etc 46 | } 47 | 48 | // Complete result combining outcome and diagnostics 49 | export interface BootResult { 50 | readonly outcome: BootOutcome; 51 | readonly diagnostics: BootDiagnostics; 52 | } 53 | 54 | export const BootResult = { 55 | /** 56 | * Simulator was successfully booted 57 | */ 58 | booted(simulatorId: string, simulatorName: string, diagnostics?: Partial<BootDiagnostics>): BootResult { 59 | return Object.freeze({ 60 | outcome: BootOutcome.Booted, 61 | diagnostics: Object.freeze({ 62 | simulatorId, 63 | simulatorName, 64 | ...diagnostics 65 | }) 66 | }); 67 | }, 68 | 69 | /** 70 | * Simulator was already running 71 | */ 72 | alreadyBooted(simulatorId: string, simulatorName: string, diagnostics?: Partial<BootDiagnostics>): BootResult { 73 | return Object.freeze({ 74 | outcome: BootOutcome.AlreadyBooted, 75 | diagnostics: Object.freeze({ 76 | simulatorId, 77 | simulatorName, 78 | ...diagnostics 79 | }) 80 | }); 81 | }, 82 | 83 | /** 84 | * Boot operation failed 85 | */ 86 | failed(simulatorId: string, simulatorName: string, error: BootError, diagnostics?: Partial<BootDiagnostics>): BootResult { 87 | return Object.freeze({ 88 | outcome: BootOutcome.Failed, 89 | diagnostics: Object.freeze({ 90 | simulatorId, 91 | simulatorName, 92 | error, 93 | ...diagnostics 94 | }) 95 | }); 96 | } 97 | }; ``` -------------------------------------------------------------------------------- /src/domain/tests/unit/PlatformDetector.unit.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect } from '@jest/globals'; 2 | import { PlatformDetector } from '../../services/PlatformDetector.js'; 3 | import { BuildDestination } from '../../../features/build/domain/BuildDestination.js'; 4 | import { Platform } from '../../../shared/domain/Platform.js'; 5 | 6 | /** 7 | * Unit tests for PlatformDetector domain service 8 | * 9 | * Testing pure domain logic for platform detection from build destinations 10 | */ 11 | describe('PlatformDetector', () => { 12 | describe('fromDestination', () => { 13 | it('should detect iOS platform from iOS destinations', () => { 14 | // Arrange & Act & Assert 15 | expect(PlatformDetector.fromDestination(BuildDestination.iOSSimulator)).toBe(Platform.iOS); 16 | expect(PlatformDetector.fromDestination(BuildDestination.iOSDevice)).toBe(Platform.iOS); 17 | expect(PlatformDetector.fromDestination(BuildDestination.iOSSimulatorUniversal)).toBe(Platform.iOS); 18 | }); 19 | 20 | it('should detect macOS platform from macOS destinations', () => { 21 | // Arrange & Act & Assert 22 | expect(PlatformDetector.fromDestination(BuildDestination.macOS)).toBe(Platform.macOS); 23 | expect(PlatformDetector.fromDestination(BuildDestination.macOSUniversal)).toBe(Platform.macOS); 24 | }); 25 | 26 | it('should detect tvOS platform from tvOS destinations', () => { 27 | // Arrange & Act & Assert 28 | expect(PlatformDetector.fromDestination(BuildDestination.tvOSSimulator)).toBe(Platform.tvOS); 29 | expect(PlatformDetector.fromDestination(BuildDestination.tvOSDevice)).toBe(Platform.tvOS); 30 | expect(PlatformDetector.fromDestination(BuildDestination.tvOSSimulatorUniversal)).toBe(Platform.tvOS); 31 | }); 32 | 33 | it('should detect watchOS platform from watchOS destinations', () => { 34 | // Arrange & Act & Assert 35 | expect(PlatformDetector.fromDestination(BuildDestination.watchOSSimulator)).toBe(Platform.watchOS); 36 | expect(PlatformDetector.fromDestination(BuildDestination.watchOSDevice)).toBe(Platform.watchOS); 37 | expect(PlatformDetector.fromDestination(BuildDestination.watchOSSimulatorUniversal)).toBe(Platform.watchOS); 38 | }); 39 | 40 | it('should detect visionOS platform from visionOS destinations', () => { 41 | // Arrange & Act & Assert 42 | expect(PlatformDetector.fromDestination(BuildDestination.visionOSSimulator)).toBe(Platform.visionOS); 43 | expect(PlatformDetector.fromDestination(BuildDestination.visionOSDevice)).toBe(Platform.visionOS); 44 | expect(PlatformDetector.fromDestination(BuildDestination.visionOSSimulatorUniversal)).toBe(Platform.visionOS); 45 | }); 46 | 47 | it('should default to iOS for unknown destination patterns', () => { 48 | // Arrange 49 | const unknownDestination = 'unknownPlatform' as BuildDestination; 50 | 51 | // Act 52 | const result = PlatformDetector.fromDestination(unknownDestination); 53 | 54 | // Assert 55 | expect(result).toBe(Platform.iOS); 56 | }); 57 | }); 58 | }); ``` -------------------------------------------------------------------------------- /src/features/app-management/tests/unit/InstallRequest.unit.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect } from '@jest/globals'; 2 | import { InstallRequest } from '../../domain/InstallRequest.js'; 3 | 4 | describe('InstallRequest', () => { 5 | describe('create', () => { 6 | it('should create valid install request with simulator ID', () => { 7 | // Arrange & Act 8 | const request = InstallRequest.create( 9 | '/path/to/app.app', 10 | 'iPhone-15-Simulator' 11 | ); 12 | 13 | // Assert 14 | expect(request.appPath.toString()).toBe('/path/to/app.app'); 15 | expect(request.simulatorId?.toString()).toBe('iPhone-15-Simulator'); 16 | }); 17 | 18 | it('should create valid install request without simulator ID', () => { 19 | // Arrange & Act 20 | const request = InstallRequest.create( 21 | '/path/to/app.app' 22 | ); 23 | 24 | // Assert 25 | expect(request.appPath.toString()).toBe('/path/to/app.app'); 26 | expect(request.simulatorId).toBeUndefined(); 27 | }); 28 | 29 | it('should reject empty app path', () => { 30 | // Arrange & Act & Assert 31 | expect(() => InstallRequest.create('', 'test-sim')) 32 | .toThrow('App path cannot be empty'); 33 | }); 34 | 35 | it('should reject whitespace-only app path', () => { 36 | // Arrange & Act & Assert 37 | expect(() => InstallRequest.create(' ', 'test-sim')) 38 | .toThrow('App path cannot be empty'); 39 | }); 40 | 41 | it('should reject invalid app extension', () => { 42 | // Arrange & Act & Assert 43 | expect(() => InstallRequest.create('/path/to/file.txt', 'test-sim')) 44 | .toThrow('App path must end with .app'); 45 | }); 46 | 47 | it('should accept .app bundle path', () => { 48 | // Arrange & Act 49 | const request = InstallRequest.create( 50 | '/path/to/MyApp.app', 51 | 'test-sim' 52 | ); 53 | 54 | // Assert 55 | expect(request.appPath.toString()).toBe('/path/to/MyApp.app'); 56 | }); 57 | 58 | it('should trim whitespace from simulator ID', () => { 59 | // Arrange & Act 60 | const request = InstallRequest.create( 61 | '/path/to/app.app', 62 | ' test-sim ' 63 | ); 64 | 65 | // Assert 66 | expect(request.simulatorId?.toString()).toBe('test-sim'); 67 | }); 68 | }); 69 | 70 | describe('validation', () => { 71 | it('should reject path traversal attempts', () => { 72 | // Arrange & Act & Assert 73 | expect(() => InstallRequest.create('../../../etc/passwd.app')) 74 | .toThrow(); 75 | }); 76 | 77 | it('should accept absolute paths', () => { 78 | // Arrange & Act 79 | const request = InstallRequest.create( 80 | '/Users/developer/MyApp.app' 81 | ); 82 | 83 | // Assert 84 | expect(request.appPath.toString()).toBe('/Users/developer/MyApp.app'); 85 | }); 86 | 87 | it('should accept relative paths within project', () => { 88 | // Arrange & Act 89 | const request = InstallRequest.create( 90 | './build/Debug/MyApp.app' 91 | ); 92 | 93 | // Assert 94 | expect(request.appPath.toString()).toBe('./build/Debug/MyApp.app'); 95 | }); 96 | }); 97 | }); ``` -------------------------------------------------------------------------------- /src/features/simulator/tests/unit/ShutdownRequest.unit.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect } from '@jest/globals'; 2 | import { ShutdownRequest } from '../../domain/ShutdownRequest.js'; 3 | import { DeviceId } from '../../../../shared/domain/DeviceId.js'; 4 | 5 | describe('ShutdownRequest', () => { 6 | describe('create', () => { 7 | it('should create a valid shutdown request with device ID', () => { 8 | // Arrange 9 | const deviceIdString = 'iPhone-15'; 10 | const deviceId = DeviceId.create(deviceIdString); 11 | 12 | // Act 13 | const request = ShutdownRequest.create(deviceId); 14 | 15 | // Assert 16 | expect(request.deviceId).toBe('iPhone-15'); 17 | }); 18 | 19 | it('should trim whitespace from device ID', () => { 20 | // Arrange 21 | const deviceIdString = ' iPhone-15 '; 22 | const deviceId = DeviceId.create(deviceIdString); 23 | 24 | // Act 25 | const request = ShutdownRequest.create(deviceId); 26 | 27 | // Assert 28 | expect(request.deviceId).toBe('iPhone-15'); 29 | }); 30 | 31 | it('should accept UUID format device ID', () => { 32 | // Arrange 33 | const uuid = '550e8400-e29b-41d4-a716-446655440000'; 34 | const deviceId = DeviceId.create(uuid); 35 | 36 | // Act 37 | const request = ShutdownRequest.create(deviceId); 38 | 39 | // Assert 40 | expect(request.deviceId).toBe(uuid); 41 | }); 42 | 43 | it('should throw error for empty device ID', () => { 44 | // Arrange & Act & Assert 45 | expect(() => DeviceId.create('')).toThrow('Device ID cannot be empty'); 46 | }); 47 | 48 | it('should throw error for null device ID', () => { 49 | // Arrange & Act & Assert 50 | expect(() => DeviceId.create(null as any)).toThrow('Device ID is required'); 51 | }); 52 | 53 | it('should throw error for undefined device ID', () => { 54 | // Arrange & Act & Assert 55 | expect(() => DeviceId.create(undefined as any)).toThrow('Device ID is required'); 56 | }); 57 | 58 | it('should throw error for whitespace-only device ID', () => { 59 | // Arrange & Act & Assert 60 | expect(() => DeviceId.create(' ')).toThrow('Device ID cannot be whitespace only'); 61 | }); 62 | 63 | it('should be immutable', () => { 64 | // Arrange 65 | const deviceId = DeviceId.create('iPhone-15'); 66 | const request = ShutdownRequest.create(deviceId); 67 | 68 | // Act & Assert 69 | expect(() => { 70 | (request as any).deviceId = 'Changed'; 71 | }).toThrow(); 72 | }); 73 | 74 | it('should handle device names with spaces', () => { 75 | // Arrange 76 | const deviceName = 'iPhone 15 Pro Max'; 77 | const deviceId = DeviceId.create(deviceName); 78 | 79 | // Act 80 | const request = ShutdownRequest.create(deviceId); 81 | 82 | // Assert 83 | expect(request.deviceId).toBe('iPhone 15 Pro Max'); 84 | }); 85 | 86 | it('should handle device names with special characters', () => { 87 | // Arrange 88 | const deviceName = "John's iPhone (Work)"; 89 | const deviceId = DeviceId.create(deviceName); 90 | 91 | // Act 92 | const request = ShutdownRequest.create(deviceId); 93 | 94 | // Assert 95 | expect(request.deviceId).toBe("John's iPhone (Work)"); 96 | }); 97 | }); 98 | }); ``` -------------------------------------------------------------------------------- /src/features/simulator/controllers/ListSimulatorsController.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ListSimulatorsUseCase } from '../use-cases/ListSimulatorsUseCase.js'; 2 | import { ListSimulatorsRequest } from '../domain/ListSimulatorsRequest.js'; 3 | import { SimulatorState } from '../domain/SimulatorState.js'; 4 | import { Platform } from '../../../shared/domain/Platform.js'; 5 | import { ErrorFormatter } from '../../../presentation/formatters/ErrorFormatter.js'; 6 | import { MCPController } from '../../../presentation/interfaces/MCPController.js'; 7 | 8 | /** 9 | * Controller for the list_simulators MCP tool 10 | * 11 | * Lists available simulators with optional filtering 12 | */ 13 | export class ListSimulatorsController implements MCPController { 14 | readonly name = 'list_simulators'; 15 | readonly description = 'List available iOS simulators'; 16 | 17 | constructor( 18 | private useCase: ListSimulatorsUseCase 19 | ) {} 20 | 21 | get inputSchema() { 22 | return { 23 | type: 'object' as const, 24 | properties: { 25 | platform: { 26 | type: 'string' as const, 27 | description: 'Filter by platform', 28 | enum: ['iOS', 'tvOS', 'watchOS', 'visionOS'] as const 29 | }, 30 | state: { 31 | type: 'string' as const, 32 | description: 'Filter by simulator state', 33 | enum: ['Booted', 'Shutdown'] as const 34 | }, 35 | name: { 36 | type: 'string' as const, 37 | description: 'Filter by device name (partial match, case-insensitive)' 38 | } 39 | }, 40 | required: [] as const 41 | }; 42 | } 43 | 44 | getToolDefinition() { 45 | return { 46 | name: this.name, 47 | description: this.description, 48 | inputSchema: this.inputSchema 49 | }; 50 | } 51 | 52 | async execute(args: unknown): Promise<{ content: Array<{ type: string; text: string }> }> { 53 | try { 54 | // Cast to expected shape 55 | const input = args as { platform?: string; state?: string; name?: string }; 56 | 57 | // Use the new validation functions 58 | const platform = Platform.parseOptional(input.platform); 59 | const state = SimulatorState.parseOptional(input.state); 60 | 61 | const request = ListSimulatorsRequest.create(platform, state, input.name); 62 | const result = await this.useCase.execute(request); 63 | 64 | if (!result.isSuccess) { 65 | return { 66 | content: [{ 67 | type: 'text', 68 | text: `❌ ${ErrorFormatter.format(result.error!)}` 69 | }] 70 | }; 71 | } 72 | 73 | if (result.count === 0) { 74 | return { 75 | content: [{ 76 | type: 'text', 77 | text: '🔍 No simulators found' 78 | }] 79 | }; 80 | } 81 | 82 | const lines: string[] = [ 83 | `✅ Found ${result.count} simulator${result.count === 1 ? '' : 's'}`, 84 | '' 85 | ]; 86 | 87 | for (const simulator of result.simulators) { 88 | lines.push(`• ${simulator.name} (${simulator.udid}) - ${simulator.state} - ${simulator.runtime}`); 89 | } 90 | 91 | return { 92 | content: [{ 93 | type: 'text', 94 | text: lines.join('\n') 95 | }] 96 | }; 97 | } catch (error) { 98 | return { 99 | content: [{ 100 | type: 'text', 101 | text: `❌ ${ErrorFormatter.format(error as Error)}` 102 | }] 103 | }; 104 | } 105 | } 106 | 107 | } ```