xcode-mcp-server 1.3.13__tar.gz → 1.3.15__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/CLAUDE.md +65 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/PKG-INFO +1 -1
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/TODO.md +86 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/tests/test_build_log_parser.py +78 -1
- xcode_mcp_server-1.3.15/tests/test_run_guard.py +159 -0
- xcode_mcp_server-1.3.15/tests/test_xcresult_wait.py +104 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/__init__.py +1 -1
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/security.py +26 -4
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/clean_project.py +26 -7
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/create_project.py +33 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/get_active_run_destination.py +8 -113
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/get_directory_tree.py +32 -5
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/get_xcode_projects.py +12 -3
- xcode_mcp_server-1.3.15/xcode_mcp_server/tools/list_project_tests.py +240 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/list_run_destinations.py +23 -67
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/run_project_tests.py +41 -26
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/run_project_unmonitored.py +10 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/run_project_until_terminated.py +82 -62
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/run_project_with_user_interaction.py +64 -18
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/take_window_screenshot.py +16 -1
- xcode_mcp_server-1.3.15/xcode_mcp_server/tools/version.py +64 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/utils/applescript.py +80 -6
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/utils/build_log_parser.py +96 -2
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/utils/project_templates.py +9 -1
- xcode_mcp_server-1.3.15/xcode_mcp_server/utils/run_guard.py +87 -0
- xcode_mcp_server-1.3.15/xcode_mcp_server/utils/xcodebuild_query.py +341 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/utils/xcresult.py +158 -46
- xcode_mcp_server-1.3.13/xcode_mcp_server/tools/list_project_tests.py +0 -91
- xcode_mcp_server-1.3.13/xcode_mcp_server/tools/version.py +0 -20
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/.github/workflows/claude-code.yml +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/.github/workflows/claude-self-review.yml +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/.gitignore +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/LICENSE +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/README.md +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/__PUBLIC_REPO +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/_venv-setup.sh +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/deploy-beta.sh +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/deploy.sh +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/dev.sh +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/info.sh +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/pyproject.toml +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/requirements.txt +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/run_local_for_claude.sh +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/scripts/cleanup_tests.sh +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/scripts/create_projects.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/scripts/create_real_projects.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/scripts/create_spm_projects.sh +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/scripts/create_xcode_projects.sh +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/scripts/run_all_tests.sh +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/scripts/setup_test_projects.sh +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/scripts/setup_tests.sh +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/iosEmptySwiftUIApp/iosEmptySwiftUIApp/Assets.xcassets/AccentColor.colorset/Contents.json +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/iosEmptySwiftUIApp/iosEmptySwiftUIApp/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/iosEmptySwiftUIApp/iosEmptySwiftUIApp/Assets.xcassets/Contents.json +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/iosEmptySwiftUIApp/iosEmptySwiftUIApp/ContentView.swift +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/iosEmptySwiftUIApp/iosEmptySwiftUIApp/iosEmptySwiftUIAppApp.swift +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/iosEmptySwiftUIApp/iosEmptySwiftUIApp.xcodeproj/project.pbxproj +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/iosEmptySwiftUIApp/iosEmptySwiftUIApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/iosEmptySwiftUIApp/iosEmptySwiftUIApp.xcodeproj/project.xcworkspace/xcuserdata/andrew.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/iosEmptySwiftUIApp/iosEmptySwiftUIApp.xcodeproj/xcuserdata/andrew.xcuserdatad/xcschemes/xcschememanagement.plist +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptyCommandLineApp/macosEmptyCommandLineApp/main.swift +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptyCommandLineApp/macosEmptyCommandLineApp.xcodeproj/project.pbxproj +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptyCommandLineApp/macosEmptyCommandLineApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptyCommandLineApp/macosEmptyCommandLineApp.xcodeproj/project.xcworkspace/xcuserdata/andrew.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptyCommandLineApp/macosEmptyCommandLineApp.xcodeproj/xcuserdata/andrew.xcuserdatad/xcschemes/xcschememanagement.plist +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptySwiftUIApp/macosEmptySwiftUIApp/Assets.xcassets/AccentColor.colorset/Contents.json +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptySwiftUIApp/macosEmptySwiftUIApp/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptySwiftUIApp/macosEmptySwiftUIApp/Assets.xcassets/Contents.json +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptySwiftUIApp/macosEmptySwiftUIApp/ContentView.swift +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptySwiftUIApp/macosEmptySwiftUIApp/macosEmptySwiftUIAppApp.swift +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptySwiftUIApp/macosEmptySwiftUIApp.xcodeproj/project.pbxproj +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptySwiftUIApp/macosEmptySwiftUIApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptySwiftUIApp/macosEmptySwiftUIApp.xcodeproj/project.xcworkspace/xcuserdata/andrew.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptySwiftUIApp/macosEmptySwiftUIApp.xcodeproj/xcuserdata/andrew.xcuserdatad/xcschemes/xcschememanagement.plist +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/BrokenApp/BrokenApp/main.swift +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/BrokenApp/BrokenApp.xcodeproj/project.pbxproj +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/BrokenApp/BrokenApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/BrokenApp/BrokenApp.xcodeproj/project.xcworkspace/xcuserdata/andrew.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/BrokenApp/BrokenApp.xcodeproj/xcuserdata/andrew.xcuserdatad/xcschemes/xcschememanagement.plist +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/ConsoleApp/ConsoleApp/main.swift +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/ConsoleApp/ConsoleApp.xcodeproj/project.pbxproj +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/ConsoleApp/ConsoleApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/ConsoleApp/ConsoleApp.xcodeproj/project.xcworkspace/xcuserdata/andrew.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/ConsoleApp/ConsoleApp.xcodeproj/xcuserdata/andrew.xcuserdatad/xcschemes/xcschememanagement.plist +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SimpleApp/SimpleApp/main.swift +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SimpleApp/SimpleApp.xcodeproj/project.pbxproj +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SimpleApp/SimpleApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SimpleApp/SimpleApp.xcodeproj/project.xcworkspace/xcuserdata/andrew.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SimpleApp/SimpleApp.xcodeproj/xcuserdata/andrew.xcuserdatad/xcschemes/xcschememanagement.plist +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SwiftUITestApp/SwiftUITestApp/Assets.xcassets/AccentColor.colorset/Contents.json +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SwiftUITestApp/SwiftUITestApp/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SwiftUITestApp/SwiftUITestApp/Assets.xcassets/Contents.json +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SwiftUITestApp/SwiftUITestApp/ContentView.swift +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SwiftUITestApp/SwiftUITestApp/macosEmptySwiftUIAppApp.swift +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SwiftUITestApp/SwiftUITestApp.xcodeproj/project.pbxproj +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SwiftUITestApp/SwiftUITestApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SwiftUITestApp/SwiftUITestApp.xcodeproj/project.xcworkspace/xcuserdata/andrew.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SwiftUITestApp/SwiftUITestApp.xcodeproj/xcuserdata/andrew.xcuserdatad/xcschemes/xcschememanagement.plist +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/iOSTestApp/iOSTestApp/Assets.xcassets/AccentColor.colorset/Contents.json +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/iOSTestApp/iOSTestApp/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/iOSTestApp/iOSTestApp/Assets.xcassets/Contents.json +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/iOSTestApp/iOSTestApp/ContentView.swift +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/iOSTestApp/iOSTestApp/iosEmptySwiftUIAppApp.swift +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/iOSTestApp/iOSTestApp.xcodeproj/project.pbxproj +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/iOSTestApp/iOSTestApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/iOSTestApp/iOSTestApp.xcodeproj/project.xcworkspace/xcuserdata/andrew.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/iOSTestApp/iOSTestApp.xcodeproj/xcuserdata/andrew.xcuserdatad/xcschemes/xcschememanagement.plist +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/tests/debug_build.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/tests/debug_test.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/tests/test_basic.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/tests/test_build.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/tests/test_fixed.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/tests/test_manifest_wait.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/tests/test_minimal.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/tests/test_runner.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/__main__.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/cli.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/config_manager.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/config_ui.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/exceptions.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/server.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/__init__.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/build_project.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/debug_list_notification_history.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/get_build_errors.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/get_build_results.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/get_directory_listing.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/get_latest_test_results.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/get_project_schemes.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/get_runtime_output.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/list_booted_simulators.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/list_mac_app_windows.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/list_running_mac_apps.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/set_run_destination.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/stop_project.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/take_app_screenshot.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/take_simulator_screenshot.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/take_xcode_screenshot.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/utils/__init__.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/utils/decode_active_destination.swift +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/utils/decode_xcode_recents.swift +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/utils/paths.py +0 -0
- {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/utils/screenshot.py +0 -0
|
@@ -21,6 +21,71 @@ mcp dev xcode_mcp_server/__main__.py
|
|
|
21
21
|
python -m xcode_mcp_server
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
### Testing in-progress changes through the real MCP path
|
|
25
|
+
|
|
26
|
+
Two MCP servers are registered against this repo:
|
|
27
|
+
|
|
28
|
+
- **`xcode-mcp-server`** — the *deployed* build (`uvx xcode-mcp-server==<version>`).
|
|
29
|
+
It runs the published PyPI/beta release, NOT your working tree. Editing source
|
|
30
|
+
here does nothing to it until you `./deploy.sh` and switch versions.
|
|
31
|
+
- **`xcode-mcp-local-dev-server`** — runs your **live local source** via
|
|
32
|
+
`run_local_for_claude.sh` (`cd` into the repo, then `python -m xcode_mcp_server`).
|
|
33
|
+
Use this to test uncommitted changes without deploying. (Note: the canonical
|
|
34
|
+
repo path `~/cursor/xcode-mcp-server` and `~/Documents/ncc_source/cursor/xcode-mcp-server`
|
|
35
|
+
are the same directory via symlink.)
|
|
36
|
+
|
|
37
|
+
**Critical freshness rule:** a Claude instance spawns the dev-server process once,
|
|
38
|
+
when it first connects, and Python imports each module once per process. So the
|
|
39
|
+
dev-server bound to a *running* Claude session is **frozen at that session's
|
|
40
|
+
start** — editing source afterward does NOT update it. Per-tool-call does not
|
|
41
|
+
re-import; only a brand-new server process picks up edits.
|
|
42
|
+
|
|
43
|
+
To test the latest source after editing, pick one:
|
|
44
|
+
|
|
45
|
+
1. **Spawn a fresh Claude instance from bash** (best for end-to-end MCP-path
|
|
46
|
+
testing — each invocation starts its own dev-server process = current source):
|
|
47
|
+
```bash
|
|
48
|
+
claude -p "Call mcp__xcode-mcp-local-dev-server__list_project_tests with
|
|
49
|
+
project_path=... and report the result verbatim." \
|
|
50
|
+
--allowedTools "mcp__xcode-mcp-local-dev-server__list_project_tests"
|
|
51
|
+
```
|
|
52
|
+
2. **Call the tool function directly via `python3 -c`** (fastest for iterating on
|
|
53
|
+
tool logic; imports fresh source each run, but bypasses FastMCP/serialization):
|
|
54
|
+
```bash
|
|
55
|
+
python3 -c "
|
|
56
|
+
import xcode_mcp_server.security as sec
|
|
57
|
+
sec.ALLOWED_FOLDERS = {'/path/allowed'}
|
|
58
|
+
from xcode_mcp_server.tools.<tool_module> import <tool_fn>
|
|
59
|
+
fn = getattr(<tool_fn>, 'fn', <tool_fn>) # unwrap the FastMCP tool
|
|
60
|
+
print(fn('/path/to/Project.xcodeproj'))
|
|
61
|
+
"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Do NOT trust the dev-server tools exposed in your *current* session to reflect
|
|
65
|
+
edits you just made — they reflect the source as of when this session connected.
|
|
66
|
+
|
|
67
|
+
**Confirming the running server matches your edits.** When running from a source
|
|
68
|
+
checkout, the `version` tool appends a source fingerprint, e.g.
|
|
69
|
+
`Xcode MCP Server version 1.3.14b1 (dev source e5f07eab)`. The hash is a SHA-256
|
|
70
|
+
of all `.py` files in the package, computed at import — so it identifies the
|
|
71
|
+
source the process actually loaded. Deployed (uvx) builds have no `.git` and omit
|
|
72
|
+
the suffix. To verify a server is running your current code, compare its reported
|
|
73
|
+
hash to the on-disk hash:
|
|
74
|
+
```bash
|
|
75
|
+
python3 -c "
|
|
76
|
+
import hashlib, os
|
|
77
|
+
d = hashlib.sha256()
|
|
78
|
+
for root, dirs, files in os.walk('xcode_mcp_server'):
|
|
79
|
+
dirs[:] = sorted(x for x in dirs if x != '__pycache__')
|
|
80
|
+
for n in sorted(files):
|
|
81
|
+
if n.endswith('.py'):
|
|
82
|
+
with open(os.path.join(root, n), 'rb') as h: d.update(h.read())
|
|
83
|
+
print(d.hexdigest()[:8])
|
|
84
|
+
"
|
|
85
|
+
```
|
|
86
|
+
If the `version` tool's hash differs from this, the server is running stale code
|
|
87
|
+
(start a fresh Claude instance). If they match, it is your current source.
|
|
88
|
+
|
|
24
89
|
### Testing
|
|
25
90
|
```bash
|
|
26
91
|
# Run tests using the test runner framework
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xcode-mcp-server
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.15
|
|
4
4
|
Summary: Drew's MCP server for Xcode integration
|
|
5
5
|
Project-URL: Homepage, https://github.com/drewster99/xcode-mcp-server
|
|
6
6
|
Project-URL: Repository, https://github.com/drewster99/xcode-mcp-server
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
- [ ] Add timeout configuration for build and test operations
|
|
16
16
|
- [ ] Implement parallel test execution support
|
|
17
17
|
|
|
18
|
+
### Project Creation
|
|
19
|
+
- [ ] Make `create_project` toolchain-aware instead of emitting a format frozen in source (see "Project Template Generation — Toolchain Awareness" below)
|
|
20
|
+
|
|
18
21
|
### Documentation & Quality
|
|
19
22
|
- [ ] Create CHANGELOG.md with version history
|
|
20
23
|
- [ ] Add ROADMAP.md for future feature planning
|
|
@@ -32,6 +35,89 @@
|
|
|
32
35
|
|
|
33
36
|
## Implementation Details
|
|
34
37
|
|
|
38
|
+
### Project Template Generation — Toolchain Awareness
|
|
39
|
+
|
|
40
|
+
**Current behavior (as of this writing)**: `create_project` does NOT use Xcode at
|
|
41
|
+
all to scaffold a project, and does NOT consult the `xcode-select`-selected
|
|
42
|
+
toolchain. The entire project is synthesized in-process from hardcoded Python
|
|
43
|
+
string constants in `xcode_mcp_server/utils/project_templates.py`. There is no
|
|
44
|
+
`xcrun`, no `DEVELOPER_DIR`, no read of Xcode's `.xctemplate` bundles, and no
|
|
45
|
+
filesystem copy — the only filesystem call in that module is `open(path, 'x')`
|
|
46
|
+
(a write). `generate_project()` fills the templates with fresh UUIDs and writes
|
|
47
|
+
each file directly.
|
|
48
|
+
|
|
49
|
+
The eight baked-in template constants and what they produce:
|
|
50
|
+
| Constant | Produces |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `PBXPROJ_TEMPLATE` | `project.pbxproj` |
|
|
53
|
+
| `WORKSPACE_DATA_TEMPLATE` | `project.xcworkspace/contents.xcworkspacedata` |
|
|
54
|
+
| `APP_SWIFT_TEMPLATE` | `{Identifier}App.swift` |
|
|
55
|
+
| `CONTENT_VIEW_TEMPLATE` | `ContentView.swift` |
|
|
56
|
+
| `ASSETS_CONTENTS_JSON` | `Assets.xcassets/Contents.json` |
|
|
57
|
+
| `ACCENT_COLOR_CONTENTS_JSON` | `AccentColor.colorset/Contents.json` |
|
|
58
|
+
| `APP_ICON_IOS_CONTENTS_JSON` / `APP_ICON_MACOS_CONTENTS_JSON` | `AppIcon.appiconset/Contents.json` |
|
|
59
|
+
|
|
60
|
+
Real Xcode templates (for reference) live at:
|
|
61
|
+
`<selected Xcode>.app/Contents/Developer/Library/Xcode/Templates/…`
|
|
62
|
+
(resolve the active Xcode with `xcode-select -p` / `xcrun --find`).
|
|
63
|
+
|
|
64
|
+
**Problem**: The generated format is FROZEN in source, independent of whatever
|
|
65
|
+
Xcode is selected:
|
|
66
|
+
- `objectVersion = 77` + `PBXFileSystemSynchronizedRootGroup` → requires Xcode 16+.
|
|
67
|
+
On an older selected Xcode the project may fail to open or prompt to upgrade.
|
|
68
|
+
- Default deployment target hardcoded to `26.0`, default bundle id
|
|
69
|
+
`com.example.{identifier}`.
|
|
70
|
+
- Only a SwiftUI App template (iOS/macOS). No other product types, no tests
|
|
71
|
+
target, no Storyboard/UIKit, no Swift package, etc.
|
|
72
|
+
- As Xcode evolves the pbxproj format (object version bumps, new isa types),
|
|
73
|
+
these strings silently drift out of date and must be hand-edited.
|
|
74
|
+
|
|
75
|
+
This is intentional decoupling (project creation works with zero dependency on
|
|
76
|
+
where/whether Xcode's templates exist), but it trades freshness and fidelity for
|
|
77
|
+
that independence.
|
|
78
|
+
|
|
79
|
+
**Goal**: Let `create_project` respect the selected toolchain (and/or broaden the
|
|
80
|
+
template set) without giving up the "works offline / no Xcode UI" property where
|
|
81
|
+
possible.
|
|
82
|
+
|
|
83
|
+
**Options (roughly increasing fidelity / effort)**:
|
|
84
|
+
1. **Version-adaptive hardcoded templates (smallest change)**. Detect the active
|
|
85
|
+
Xcode version (`xcodebuild -version` / parse `version.plist` under
|
|
86
|
+
`xcode-select -p`) and pick an `objectVersion` + group style that matches
|
|
87
|
+
(e.g. emit a pre-77 file-group layout for Xcode < 16). Keep generation
|
|
88
|
+
in-process. Pros: still no Xcode UI, no template parsing. Cons: we maintain N
|
|
89
|
+
format variants by hand.
|
|
90
|
+
2. **Derive defaults from the toolchain**. Even if we keep our own pbxproj
|
|
91
|
+
strings, pull the default deployment target and available SDKs from the
|
|
92
|
+
selected Xcode (`xcrun --sdk iphoneos --show-sdk-version`, etc.) instead of
|
|
93
|
+
hardcoding `26.0`. Low effort, removes the most surprising hardcode.
|
|
94
|
+
3. **Drive Xcode's real templates** via the toolchain. Investigate scaffolding
|
|
95
|
+
from `…/Library/Xcode/Templates/…` (the same `.xctemplate` bundles Xcode's
|
|
96
|
+
"New Project" uses) so output matches exactly what a user gets in the IDE.
|
|
97
|
+
This is non-trivial: the template format (`TemplateInfo.plist`, ancestors,
|
|
98
|
+
option substitution) is undocumented and Xcode applies it through internal
|
|
99
|
+
machinery, not a public CLI. Highest fidelity, highest risk/maintenance.
|
|
100
|
+
4. **Expand product types** regardless of source: add a tests target, a macOS
|
|
101
|
+
AppKit/SwiftUI choice, a Swift Package option, etc. — orthogonal to where the
|
|
102
|
+
format comes from, but worth tracking here.
|
|
103
|
+
|
|
104
|
+
**Recommendation**: Start with options 1 + 2 (version-adaptive `objectVersion`
|
|
105
|
+
and toolchain-derived deployment target/SDK). They remove the real correctness
|
|
106
|
+
risk (a frozen format that breaks on older/newer Xcode) while preserving the
|
|
107
|
+
no-Xcode-needed generation path. Treat option 3 as research-only until there's a
|
|
108
|
+
demonstrated need for exact IDE parity.
|
|
109
|
+
|
|
110
|
+
**Validation when implemented**:
|
|
111
|
+
- Generate on machines with different `xcode-select` targets and confirm the
|
|
112
|
+
project opens cleanly in each (no upgrade prompt, no "damaged project").
|
|
113
|
+
- Round-trip: open the generated project, let Xcode re-save, and diff the
|
|
114
|
+
pbxproj to see what Xcode would have changed.
|
|
115
|
+
- Keep a regression test that opens each generated variant headlessly.
|
|
116
|
+
|
|
117
|
+
**Key files**: `xcode_mcp_server/utils/project_templates.py` (the templates +
|
|
118
|
+
`generate_project`), `xcode_mcp_server/tools/create_project.py` (validation +
|
|
119
|
+
entry point), `xcode_mcp_server/security.py` (`validate_parent_for_new_project`).
|
|
120
|
+
|
|
35
121
|
### Selective Test Execution (GET_RUN_DESTINATIONS research)
|
|
36
122
|
|
|
37
123
|
**Problem**: AppleScript's `test workspaceDoc` command doesn't support filtering specific tests. Need to use `xcodebuild test -only-testing:TestBundle/Class/testMethod`.
|
|
@@ -23,7 +23,11 @@ from pathlib import Path
|
|
|
23
23
|
|
|
24
24
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
import xcode_mcp_server.utils.build_log_parser as build_log_parser
|
|
27
|
+
from xcode_mcp_server.utils.build_log_parser import (
|
|
28
|
+
parse_xcactivitylog,
|
|
29
|
+
select_derived_data_dirs_for_project,
|
|
30
|
+
)
|
|
27
31
|
|
|
28
32
|
|
|
29
33
|
def _write_gzipped_log(path: str, text: str) -> None:
|
|
@@ -32,6 +36,62 @@ def _write_gzipped_log(path: str, text: str) -> None:
|
|
|
32
36
|
f.write(text.encode('utf-8'))
|
|
33
37
|
|
|
34
38
|
|
|
39
|
+
class SelectDerivedDataDirsTests(unittest.TestCase):
|
|
40
|
+
"""The name-prefix candidate list must collapse to confirmed dirs, and when
|
|
41
|
+
none are confirmed fall back ONLY to dirs of unknown ownership — never to
|
|
42
|
+
ones info.plist proved belong to a different same-named project."""
|
|
43
|
+
|
|
44
|
+
def setUp(self):
|
|
45
|
+
self._orig = build_log_parser.derived_data_matches_project
|
|
46
|
+
|
|
47
|
+
def tearDown(self):
|
|
48
|
+
build_log_parser.derived_data_matches_project = self._orig
|
|
49
|
+
|
|
50
|
+
def _patch(self, verdicts):
|
|
51
|
+
build_log_parser.derived_data_matches_project = (
|
|
52
|
+
lambda path, project_realpath: verdicts[path]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def test_confirmed_only_when_any_confirmed(self):
|
|
56
|
+
self._patch({'/c1': True, '/c2': True, '/mm': False, '/uk': None})
|
|
57
|
+
dirs = [(0.0, '/c1'), (0.0, '/c2'), (0.0, '/mm'), (0.0, '/uk')]
|
|
58
|
+
self.assertEqual(
|
|
59
|
+
select_derived_data_dirs_for_project(dirs, '/p'),
|
|
60
|
+
[(0.0, '/c1'), (0.0, '/c2')],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def test_falls_back_to_unknown_excluding_mismatch(self):
|
|
64
|
+
self._patch({'/mm': False, '/uk': None})
|
|
65
|
+
dirs = [(0.0, '/mm'), (0.0, '/uk')]
|
|
66
|
+
self.assertEqual(
|
|
67
|
+
select_derived_data_dirs_for_project(dirs, '/p'),
|
|
68
|
+
[(0.0, '/uk')],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def test_all_mismatch_returns_empty(self):
|
|
72
|
+
self._patch({'/mm1': False, '/mm2': False})
|
|
73
|
+
dirs = [(0.0, '/mm1'), (0.0, '/mm2')]
|
|
74
|
+
self.assertEqual(select_derived_data_dirs_for_project(dirs, '/p'), [])
|
|
75
|
+
|
|
76
|
+
def test_find_derived_data_returns_none_when_all_mismatch(self):
|
|
77
|
+
"""find_derived_data_for_project must return None (not IndexError) when
|
|
78
|
+
the selector filters every candidate out as a proven mismatch."""
|
|
79
|
+
from unittest import mock
|
|
80
|
+
with tempfile.TemporaryDirectory() as base:
|
|
81
|
+
os.makedirs(os.path.join(base, "MyApp-abc123"))
|
|
82
|
+
# Every name-prefix dir is a proven mismatch -> selector returns [].
|
|
83
|
+
build_log_parser.derived_data_matches_project = (
|
|
84
|
+
lambda path, project_realpath: False
|
|
85
|
+
)
|
|
86
|
+
with mock.patch.object(
|
|
87
|
+
build_log_parser.os.path, "expanduser", return_value=base
|
|
88
|
+
):
|
|
89
|
+
result = build_log_parser.find_derived_data_for_project(
|
|
90
|
+
"/somewhere/MyApp.xcodeproj"
|
|
91
|
+
)
|
|
92
|
+
self.assertIsNone(result)
|
|
93
|
+
|
|
94
|
+
|
|
35
95
|
class PathsWithSpacesTests(unittest.TestCase):
|
|
36
96
|
def test_swift_file_with_space_in_path_is_tracked_as_compiled(self):
|
|
37
97
|
"""A path with a space must be recognized in SwiftCompile lines."""
|
|
@@ -85,6 +145,23 @@ class MixedLanguageTests(unittest.TestCase):
|
|
|
85
145
|
finally:
|
|
86
146
|
os.unlink(tmp_path)
|
|
87
147
|
|
|
148
|
+
def test_objc_compilec_with_space_in_object_path_tracks_source(self):
|
|
149
|
+
"""The object-file path (under DerivedData) may contain a space; the
|
|
150
|
+
source file must still be tracked rather than dropped."""
|
|
151
|
+
log_text = (
|
|
152
|
+
"CompileC /Users/test/My Project/Build/Foo.o "
|
|
153
|
+
"/Users/test/My Project/Foo.m normal arm64 objective-c\r"
|
|
154
|
+
)
|
|
155
|
+
with tempfile.NamedTemporaryFile(suffix='.xcactivitylog', delete=False) as tmp:
|
|
156
|
+
tmp_path = tmp.name
|
|
157
|
+
try:
|
|
158
|
+
_write_gzipped_log(tmp_path, log_text)
|
|
159
|
+
_, compiled = parse_xcactivitylog(tmp_path)
|
|
160
|
+
self.assertIn('/Users/test/My Project/Foo.m', compiled,
|
|
161
|
+
f"compiled files were: {compiled}")
|
|
162
|
+
finally:
|
|
163
|
+
os.unlink(tmp_path)
|
|
164
|
+
|
|
88
165
|
def test_objc_warning_is_extracted(self):
|
|
89
166
|
log_text = (
|
|
90
167
|
"/Users/test/MyProj/Foo.m:10:3: warning: "
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tests for the per-project run/test exclusivity guard.
|
|
3
|
+
|
|
4
|
+
The guard rejects a second concurrent run/test of the SAME project (result
|
|
5
|
+
isolation assumes a single in-flight action per project), while letting
|
|
6
|
+
different projects proceed and always releasing the key — including on
|
|
7
|
+
exception. It must also preserve the wrapped function's signature so the
|
|
8
|
+
@apply_config / FastMCP layers above it still bind parameters.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import inspect
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import threading
|
|
15
|
+
import unittest
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
19
|
+
|
|
20
|
+
from xcode_mcp_server.utils.run_guard import exclusive_per_project
|
|
21
|
+
import xcode_mcp_server.utils.run_guard as run_guard
|
|
22
|
+
from xcode_mcp_server.exceptions import XCodeMCPError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RunGuardTests(unittest.TestCase):
|
|
26
|
+
def setUp(self):
|
|
27
|
+
# Ensure a clean global state between tests.
|
|
28
|
+
with run_guard._active_lock:
|
|
29
|
+
run_guard._active_projects.clear()
|
|
30
|
+
|
|
31
|
+
def test_signature_is_preserved(self):
|
|
32
|
+
@exclusive_per_project
|
|
33
|
+
def tool(project_path, scheme=None, timeout=None):
|
|
34
|
+
return "ok"
|
|
35
|
+
self.assertEqual(
|
|
36
|
+
list(inspect.signature(tool).parameters),
|
|
37
|
+
["project_path", "scheme", "timeout"],
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def test_rejects_concurrent_same_project(self):
|
|
41
|
+
entered = threading.Event()
|
|
42
|
+
release = threading.Event()
|
|
43
|
+
|
|
44
|
+
@exclusive_per_project
|
|
45
|
+
def tool(project_path):
|
|
46
|
+
entered.set()
|
|
47
|
+
release.wait(timeout=5)
|
|
48
|
+
return "ran"
|
|
49
|
+
|
|
50
|
+
path = os.path.realpath(".")
|
|
51
|
+
t = threading.Thread(target=lambda: tool(path))
|
|
52
|
+
t.start()
|
|
53
|
+
self.assertTrue(entered.wait(timeout=5))
|
|
54
|
+
try:
|
|
55
|
+
with self.assertRaises(XCodeMCPError):
|
|
56
|
+
tool(path)
|
|
57
|
+
finally:
|
|
58
|
+
release.set()
|
|
59
|
+
t.join(timeout=5)
|
|
60
|
+
|
|
61
|
+
def test_allows_different_projects_concurrently(self):
|
|
62
|
+
a_entered = threading.Event()
|
|
63
|
+
a_release = threading.Event()
|
|
64
|
+
|
|
65
|
+
@exclusive_per_project
|
|
66
|
+
def tool_a(project_path):
|
|
67
|
+
a_entered.set()
|
|
68
|
+
a_release.wait(timeout=5)
|
|
69
|
+
return "ran-a"
|
|
70
|
+
|
|
71
|
+
@exclusive_per_project
|
|
72
|
+
def tool_b(project_path):
|
|
73
|
+
return "ran-b"
|
|
74
|
+
|
|
75
|
+
a = os.path.realpath(os.sep) # "/"
|
|
76
|
+
b = os.path.realpath(os.path.expanduser("~"))
|
|
77
|
+
self.assertNotEqual(a, b)
|
|
78
|
+
|
|
79
|
+
t = threading.Thread(target=lambda: tool_a(a))
|
|
80
|
+
t.start()
|
|
81
|
+
self.assertTrue(a_entered.wait(timeout=5))
|
|
82
|
+
try:
|
|
83
|
+
# A's key is held right now (its thread is parked in a_release.wait).
|
|
84
|
+
# A different project must run to completion, not be rejected —
|
|
85
|
+
# actually invoke it rather than only inspecting the active set.
|
|
86
|
+
self.assertEqual(tool_b(b), "ran-b")
|
|
87
|
+
finally:
|
|
88
|
+
a_release.set()
|
|
89
|
+
t.join(timeout=5)
|
|
90
|
+
|
|
91
|
+
def test_key_released_after_normal_return(self):
|
|
92
|
+
@exclusive_per_project
|
|
93
|
+
def tool(project_path):
|
|
94
|
+
return "ran"
|
|
95
|
+
path = os.path.realpath(".")
|
|
96
|
+
tool(path)
|
|
97
|
+
tool(path) # would raise if the key leaked
|
|
98
|
+
with run_guard._active_lock:
|
|
99
|
+
self.assertNotIn(path, run_guard._active_projects)
|
|
100
|
+
|
|
101
|
+
def test_key_released_after_exception(self):
|
|
102
|
+
@exclusive_per_project
|
|
103
|
+
def tool(project_path):
|
|
104
|
+
raise ValueError("boom")
|
|
105
|
+
path = os.path.realpath(".")
|
|
106
|
+
with self.assertRaises(ValueError):
|
|
107
|
+
tool(path)
|
|
108
|
+
with run_guard._active_lock:
|
|
109
|
+
self.assertNotIn(path, run_guard._active_projects)
|
|
110
|
+
|
|
111
|
+
def test_empty_path_is_not_guarded(self):
|
|
112
|
+
# An empty project_path is left to the body's own validation; the guard
|
|
113
|
+
# must not key on it (and must not raise here).
|
|
114
|
+
@exclusive_per_project
|
|
115
|
+
def tool(project_path):
|
|
116
|
+
return "ran"
|
|
117
|
+
self.assertEqual(tool(""), "ran")
|
|
118
|
+
with run_guard._active_lock:
|
|
119
|
+
self.assertEqual(len(run_guard._active_projects), 0)
|
|
120
|
+
|
|
121
|
+
def test_project_key_collapses_implicit_workspace(self):
|
|
122
|
+
import tempfile
|
|
123
|
+
with tempfile.TemporaryDirectory() as d:
|
|
124
|
+
proj = os.path.join(d, "Foo.xcodeproj")
|
|
125
|
+
ws = os.path.join(proj, "project.xcworkspace")
|
|
126
|
+
os.makedirs(ws)
|
|
127
|
+
# The implicit workspace must canonicalize to its .xcodeproj.
|
|
128
|
+
self.assertEqual(run_guard._project_key(ws), os.path.realpath(proj))
|
|
129
|
+
self.assertEqual(run_guard._project_key(proj), os.path.realpath(proj))
|
|
130
|
+
|
|
131
|
+
def test_rejects_implicit_workspace_while_xcodeproj_active(self):
|
|
132
|
+
import tempfile
|
|
133
|
+
with tempfile.TemporaryDirectory() as d:
|
|
134
|
+
proj = os.path.join(d, "Foo.xcodeproj")
|
|
135
|
+
ws = os.path.join(proj, "project.xcworkspace")
|
|
136
|
+
os.makedirs(ws)
|
|
137
|
+
entered = threading.Event()
|
|
138
|
+
release = threading.Event()
|
|
139
|
+
|
|
140
|
+
@exclusive_per_project
|
|
141
|
+
def tool(project_path):
|
|
142
|
+
entered.set()
|
|
143
|
+
release.wait(timeout=5)
|
|
144
|
+
return "ran"
|
|
145
|
+
|
|
146
|
+
t = threading.Thread(target=lambda: tool(proj))
|
|
147
|
+
t.start()
|
|
148
|
+
self.assertTrue(entered.wait(timeout=5))
|
|
149
|
+
try:
|
|
150
|
+
# Same project named via its implicit workspace must collide.
|
|
151
|
+
with self.assertRaises(XCodeMCPError):
|
|
152
|
+
tool(ws)
|
|
153
|
+
finally:
|
|
154
|
+
release.set()
|
|
155
|
+
t.join(timeout=5)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
unittest.main(verbosity=2)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tests for wait_for_xcresult_after_timestamp's new-bundle selection.
|
|
3
|
+
|
|
4
|
+
The wait helper must accept a bundle that is genuinely new — either a path
|
|
5
|
+
absent from the pre-action snapshot, OR a pre-existing path whose mtime has
|
|
6
|
+
advanced since the snapshot (Xcode rewriting a bundle in place) — and must NOT
|
|
7
|
+
re-accept an unchanged prior-run bundle. The snapshot records {path: mtime} so
|
|
8
|
+
the in-place-rewrite case is distinguishable from a stale result.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
import tempfile
|
|
15
|
+
import unittest
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
19
|
+
|
|
20
|
+
import xcode_mcp_server.utils.xcresult as xcresult
|
|
21
|
+
from xcode_mcp_server.utils.xcresult import (
|
|
22
|
+
snapshot_xcresult_mtimes,
|
|
23
|
+
wait_for_xcresult_after_timestamp,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _make_xcresult(logs_dir: str, name: str, mtime: float) -> str:
|
|
28
|
+
path = os.path.join(logs_dir, name)
|
|
29
|
+
os.makedirs(path, exist_ok=True)
|
|
30
|
+
os.utime(path, (mtime, mtime))
|
|
31
|
+
return path
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class WaitForXcresultTests(unittest.TestCase):
|
|
35
|
+
def setUp(self):
|
|
36
|
+
self._tmp = tempfile.TemporaryDirectory()
|
|
37
|
+
root = self._tmp.name
|
|
38
|
+
# Emulate a single matching DerivedData root with a Logs/Launch folder.
|
|
39
|
+
self.derived_root = os.path.join(root, "MyApp-abc123")
|
|
40
|
+
self.logs_dir = os.path.join(self.derived_root, "Logs", "Launch")
|
|
41
|
+
os.makedirs(self.logs_dir)
|
|
42
|
+
|
|
43
|
+
# Pin directory matching to our fake root so the helper never touches
|
|
44
|
+
# the real ~/Library/Developer/Xcode/DerivedData.
|
|
45
|
+
self._orig_matching = xcresult._matching_derived_data_dirs
|
|
46
|
+
xcresult._matching_derived_data_dirs = lambda project_path: [(0.0, self.derived_root)]
|
|
47
|
+
|
|
48
|
+
def tearDown(self):
|
|
49
|
+
xcresult._matching_derived_data_dirs = self._orig_matching
|
|
50
|
+
self._tmp.cleanup()
|
|
51
|
+
|
|
52
|
+
def test_accepts_brand_new_bundle_not_in_snapshot(self):
|
|
53
|
+
now = time.time()
|
|
54
|
+
_make_xcresult(self.logs_dir, "old.xcresult", now - 100)
|
|
55
|
+
prior = snapshot_xcresult_mtimes("MyApp.xcodeproj", logs_subdir="Launch")
|
|
56
|
+
|
|
57
|
+
new_path = _make_xcresult(self.logs_dir, "new.xcresult", now)
|
|
58
|
+
result = wait_for_xcresult_after_timestamp(
|
|
59
|
+
"MyApp.xcodeproj", now, timeout_seconds=2,
|
|
60
|
+
logs_subdir="Launch", prior_mtimes=prior,
|
|
61
|
+
)
|
|
62
|
+
self.assertEqual(result, new_path)
|
|
63
|
+
|
|
64
|
+
def test_accepts_pre_existing_bundle_rewritten_in_place(self):
|
|
65
|
+
now = time.time()
|
|
66
|
+
reused = _make_xcresult(self.logs_dir, "run.xcresult", now - 100)
|
|
67
|
+
prior = snapshot_xcresult_mtimes("MyApp.xcodeproj", logs_subdir="Launch")
|
|
68
|
+
self.assertIn(reused, prior)
|
|
69
|
+
|
|
70
|
+
# Xcode rewrites the same bundle path in place: mtime advances.
|
|
71
|
+
os.utime(reused, (now, now))
|
|
72
|
+
result = wait_for_xcresult_after_timestamp(
|
|
73
|
+
"MyApp.xcodeproj", now, timeout_seconds=2,
|
|
74
|
+
logs_subdir="Launch", prior_mtimes=prior,
|
|
75
|
+
)
|
|
76
|
+
self.assertEqual(result, reused)
|
|
77
|
+
|
|
78
|
+
def test_rejects_unchanged_prior_bundle(self):
|
|
79
|
+
now = time.time()
|
|
80
|
+
_make_xcresult(self.logs_dir, "stale.xcresult", now - 100)
|
|
81
|
+
prior = snapshot_xcresult_mtimes("MyApp.xcodeproj", logs_subdir="Launch")
|
|
82
|
+
|
|
83
|
+
# Nothing new and nothing rewritten: must time out to None, never
|
|
84
|
+
# re-accept the stale prior-run bundle.
|
|
85
|
+
result = wait_for_xcresult_after_timestamp(
|
|
86
|
+
"MyApp.xcodeproj", now, timeout_seconds=1,
|
|
87
|
+
logs_subdir="Launch", prior_mtimes=prior,
|
|
88
|
+
)
|
|
89
|
+
self.assertIsNone(result)
|
|
90
|
+
|
|
91
|
+
def test_newest_new_bundle_is_preferred(self):
|
|
92
|
+
now = time.time()
|
|
93
|
+
prior = snapshot_xcresult_mtimes("MyApp.xcodeproj", logs_subdir="Launch")
|
|
94
|
+
_make_xcresult(self.logs_dir, "a.xcresult", now)
|
|
95
|
+
newest = _make_xcresult(self.logs_dir, "b.xcresult", now + 5)
|
|
96
|
+
result = wait_for_xcresult_after_timestamp(
|
|
97
|
+
"MyApp.xcodeproj", now, timeout_seconds=2,
|
|
98
|
+
logs_subdir="Launch", prior_mtimes=prior,
|
|
99
|
+
)
|
|
100
|
+
self.assertEqual(result, newest)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
unittest.main(verbosity=2)
|
|
@@ -21,7 +21,21 @@ def set_allowed_folders(folders: Set[str]):
|
|
|
21
21
|
correct comparisons.
|
|
22
22
|
"""
|
|
23
23
|
global ALLOWED_FOLDERS
|
|
24
|
-
|
|
24
|
+
resolved = set()
|
|
25
|
+
for f in folders:
|
|
26
|
+
if not f:
|
|
27
|
+
continue
|
|
28
|
+
real = os.path.realpath(f)
|
|
29
|
+
# `realpath("/").rstrip("/")` is "", and an empty allowed entry makes the
|
|
30
|
+
# `startswith(entry + "/")` test in is_path_allowed match every absolute
|
|
31
|
+
# path — i.e. a silent allow-everything. Root isn't a permitted allowed
|
|
32
|
+
# folder (get_allowed_folders drops it too), so skip it here rather than
|
|
33
|
+
# store a value that defeats the allow-list.
|
|
34
|
+
if real == "/":
|
|
35
|
+
print("Warning: refusing to use '/' as an allowed folder", file=sys.stderr)
|
|
36
|
+
continue
|
|
37
|
+
resolved.add(real.rstrip("/"))
|
|
38
|
+
ALLOWED_FOLDERS = resolved
|
|
25
39
|
|
|
26
40
|
|
|
27
41
|
def get_allowed_folders(command_line_folders: Optional[List[str]] = None) -> Set[str]:
|
|
@@ -120,6 +134,10 @@ def is_path_allowed(project_path: str) -> bool:
|
|
|
120
134
|
resolved = os.path.realpath(project_path).rstrip("/")
|
|
121
135
|
|
|
122
136
|
for allowed_folder in ALLOWED_FOLDERS:
|
|
137
|
+
# Defensively ignore an empty entry; matching against "" + "/" would
|
|
138
|
+
# accept every absolute path. set_allowed_folders should never store one.
|
|
139
|
+
if not allowed_folder:
|
|
140
|
+
continue
|
|
123
141
|
if resolved == allowed_folder:
|
|
124
142
|
return True
|
|
125
143
|
if resolved.startswith(allowed_folder + "/"):
|
|
@@ -259,11 +277,15 @@ def validate_parent_for_new_project(parent_path: str, project_name: str) -> str:
|
|
|
259
277
|
"Set XCODEMCP_ALLOWED_FOLDERS environment variable."
|
|
260
278
|
)
|
|
261
279
|
|
|
262
|
-
#
|
|
263
|
-
|
|
280
|
+
# Disallow spaces (and other punctuation): the name is interpolated unquoted
|
|
281
|
+
# into project.pbxproj fields (path/name/productName), where a space
|
|
282
|
+
# terminates the token and yields a syntactically invalid OpenStep plist that
|
|
283
|
+
# Xcode refuses to open. Quoting in the template would be an alternative, but
|
|
284
|
+
# bare-identifier names keep the generated pbxproj simplest and safest.
|
|
285
|
+
if not re.match(r'^[A-Za-z0-9][A-Za-z0-9_-]*$', project_name):
|
|
264
286
|
raise InvalidParameterError(
|
|
265
287
|
"project_name must start with a letter or digit and contain only "
|
|
266
|
-
"letters, digits,
|
|
288
|
+
"letters, digits, hyphens, or underscores (no spaces)"
|
|
267
289
|
)
|
|
268
290
|
|
|
269
291
|
# Prevent overwriting existing projects
|
|
@@ -10,10 +10,14 @@ from xcode_mcp_server.security import validate_and_normalize_project_path
|
|
|
10
10
|
from xcode_mcp_server.exceptions import XCodeMCPError
|
|
11
11
|
from xcode_mcp_server.utils.applescript import (
|
|
12
12
|
build_open_and_wait_applescript,
|
|
13
|
+
build_wait_for_completion_applescript,
|
|
14
|
+
is_action_timeout,
|
|
13
15
|
resolve_build_timeout,
|
|
16
|
+
format_timeout_duration,
|
|
14
17
|
escape_applescript_string,
|
|
15
18
|
run_applescript,
|
|
16
19
|
show_result_notification,
|
|
20
|
+
show_warning_notification,
|
|
17
21
|
show_error_notification,
|
|
18
22
|
)
|
|
19
23
|
|
|
@@ -37,14 +41,20 @@ def clean_project(project_path: str, timeout: Optional[int] = None) -> str:
|
|
|
37
41
|
escaped_path = escape_applescript_string(normalized_path)
|
|
38
42
|
effective_timeout = resolve_build_timeout(timeout)
|
|
39
43
|
|
|
44
|
+
# `clean workspaceDoc` returns a scheme-action-result that completes
|
|
45
|
+
# asynchronously (same as build/test), so capture it and poll `completed`
|
|
46
|
+
# rather than assuming the command blocks. This bounds the wait by
|
|
47
|
+
# `effective_timeout` and avoids reporting success before the clean has
|
|
48
|
+
# actually finished.
|
|
40
49
|
script = build_open_and_wait_applescript(escaped_path) + (
|
|
41
|
-
' clean workspaceDoc\n'
|
|
42
|
-
|
|
50
|
+
' set actionResult to clean workspaceDoc\n'
|
|
51
|
+
+ build_wait_for_completion_applescript("actionResult", effective_timeout, action_name="Clean")
|
|
52
|
+
+ ' return "Clean completed successfully"\n'
|
|
43
53
|
'end tell\n'
|
|
44
54
|
)
|
|
45
55
|
|
|
46
|
-
#
|
|
47
|
-
#
|
|
56
|
+
# The script polls inside AppleScript for up to effective_timeout; the
|
|
57
|
+
# subprocess timeout must exceed that, plus a buffer for workspace load.
|
|
48
58
|
success, output = run_applescript(script, timeout=effective_timeout + 60)
|
|
49
59
|
|
|
50
60
|
project_name = os.path.basename(normalized_path)
|
|
@@ -52,6 +62,15 @@ def clean_project(project_path: str, timeout: Optional[int] = None) -> str:
|
|
|
52
62
|
if success:
|
|
53
63
|
show_result_notification("Clean completed", project_name)
|
|
54
64
|
return output
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
|
|
66
|
+
# The AppleScript poll loop raises (rather than returning) on timeout, so a
|
|
67
|
+
# timeout surfaces here as a failed subprocess. Detect it by the helper's
|
|
68
|
+
# error number and report it as a clean-specific timeout instead of leaking
|
|
69
|
+
# the wait-helper's wording or treating it as a hard failure.
|
|
70
|
+
if is_action_timeout(output):
|
|
71
|
+
duration = format_timeout_duration(effective_timeout)
|
|
72
|
+
show_warning_notification(f"Clean timeout ({duration})")
|
|
73
|
+
return f"⏳ Clean did not complete within {duration}"
|
|
74
|
+
|
|
75
|
+
show_error_notification("Clean failed", project_name)
|
|
76
|
+
raise XCodeMCPError(f"Clean failed: {output}")
|