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.
Files changed (143) hide show
  1. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/CLAUDE.md +65 -0
  2. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/PKG-INFO +1 -1
  3. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/TODO.md +86 -0
  4. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/tests/test_build_log_parser.py +78 -1
  5. xcode_mcp_server-1.3.15/tests/test_run_guard.py +159 -0
  6. xcode_mcp_server-1.3.15/tests/test_xcresult_wait.py +104 -0
  7. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/__init__.py +1 -1
  8. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/security.py +26 -4
  9. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/clean_project.py +26 -7
  10. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/create_project.py +33 -0
  11. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/get_active_run_destination.py +8 -113
  12. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/get_directory_tree.py +32 -5
  13. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/get_xcode_projects.py +12 -3
  14. xcode_mcp_server-1.3.15/xcode_mcp_server/tools/list_project_tests.py +240 -0
  15. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/list_run_destinations.py +23 -67
  16. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/run_project_tests.py +41 -26
  17. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/run_project_unmonitored.py +10 -0
  18. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/run_project_until_terminated.py +82 -62
  19. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/run_project_with_user_interaction.py +64 -18
  20. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/take_window_screenshot.py +16 -1
  21. xcode_mcp_server-1.3.15/xcode_mcp_server/tools/version.py +64 -0
  22. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/utils/applescript.py +80 -6
  23. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/utils/build_log_parser.py +96 -2
  24. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/utils/project_templates.py +9 -1
  25. xcode_mcp_server-1.3.15/xcode_mcp_server/utils/run_guard.py +87 -0
  26. xcode_mcp_server-1.3.15/xcode_mcp_server/utils/xcodebuild_query.py +341 -0
  27. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/utils/xcresult.py +158 -46
  28. xcode_mcp_server-1.3.13/xcode_mcp_server/tools/list_project_tests.py +0 -91
  29. xcode_mcp_server-1.3.13/xcode_mcp_server/tools/version.py +0 -20
  30. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/.github/workflows/claude-code.yml +0 -0
  31. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/.github/workflows/claude-self-review.yml +0 -0
  32. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/.gitignore +0 -0
  33. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/LICENSE +0 -0
  34. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/README.md +0 -0
  35. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/__PUBLIC_REPO +0 -0
  36. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/_venv-setup.sh +0 -0
  37. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/deploy-beta.sh +0 -0
  38. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/deploy.sh +0 -0
  39. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/dev.sh +0 -0
  40. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/info.sh +0 -0
  41. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/pyproject.toml +0 -0
  42. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/requirements.txt +0 -0
  43. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/run_local_for_claude.sh +0 -0
  44. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/scripts/cleanup_tests.sh +0 -0
  45. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/scripts/create_projects.py +0 -0
  46. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/scripts/create_real_projects.py +0 -0
  47. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/scripts/create_spm_projects.sh +0 -0
  48. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/scripts/create_xcode_projects.sh +0 -0
  49. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/scripts/run_all_tests.sh +0 -0
  50. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/scripts/setup_test_projects.sh +0 -0
  51. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/scripts/setup_tests.sh +0 -0
  52. {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
  53. {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
  54. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/iosEmptySwiftUIApp/iosEmptySwiftUIApp/Assets.xcassets/Contents.json +0 -0
  55. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/iosEmptySwiftUIApp/iosEmptySwiftUIApp/ContentView.swift +0 -0
  56. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/iosEmptySwiftUIApp/iosEmptySwiftUIApp/iosEmptySwiftUIAppApp.swift +0 -0
  57. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/iosEmptySwiftUIApp/iosEmptySwiftUIApp.xcodeproj/project.pbxproj +0 -0
  58. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/iosEmptySwiftUIApp/iosEmptySwiftUIApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
  59. {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
  60. {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
  61. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptyCommandLineApp/macosEmptyCommandLineApp/main.swift +0 -0
  62. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptyCommandLineApp/macosEmptyCommandLineApp.xcodeproj/project.pbxproj +0 -0
  63. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptyCommandLineApp/macosEmptyCommandLineApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
  64. {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
  65. {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
  66. {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
  67. {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
  68. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptySwiftUIApp/macosEmptySwiftUIApp/Assets.xcassets/Contents.json +0 -0
  69. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptySwiftUIApp/macosEmptySwiftUIApp/ContentView.swift +0 -0
  70. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptySwiftUIApp/macosEmptySwiftUIApp/macosEmptySwiftUIAppApp.swift +0 -0
  71. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptySwiftUIApp/macosEmptySwiftUIApp.xcodeproj/project.pbxproj +0 -0
  72. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/fromXcode/macosEmptySwiftUIApp/macosEmptySwiftUIApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
  73. {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
  74. {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
  75. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/BrokenApp/BrokenApp/main.swift +0 -0
  76. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/BrokenApp/BrokenApp.xcodeproj/project.pbxproj +0 -0
  77. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/BrokenApp/BrokenApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
  78. {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
  79. {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
  80. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/ConsoleApp/ConsoleApp/main.swift +0 -0
  81. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/ConsoleApp/ConsoleApp.xcodeproj/project.pbxproj +0 -0
  82. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/ConsoleApp/ConsoleApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
  83. {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
  84. {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
  85. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SimpleApp/SimpleApp/main.swift +0 -0
  86. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SimpleApp/SimpleApp.xcodeproj/project.pbxproj +0 -0
  87. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SimpleApp/SimpleApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
  88. {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
  89. {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
  90. {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
  91. {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
  92. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SwiftUITestApp/SwiftUITestApp/Assets.xcassets/Contents.json +0 -0
  93. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SwiftUITestApp/SwiftUITestApp/ContentView.swift +0 -0
  94. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SwiftUITestApp/SwiftUITestApp/macosEmptySwiftUIAppApp.swift +0 -0
  95. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SwiftUITestApp/SwiftUITestApp.xcodeproj/project.pbxproj +0 -0
  96. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/SwiftUITestApp/SwiftUITestApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
  97. {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
  98. {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
  99. {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
  100. {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
  101. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/iOSTestApp/iOSTestApp/Assets.xcassets/Contents.json +0 -0
  102. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/iOSTestApp/iOSTestApp/ContentView.swift +0 -0
  103. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/iOSTestApp/iOSTestApp/iosEmptySwiftUIAppApp.swift +0 -0
  104. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/iOSTestApp/iOSTestApp.xcodeproj/project.pbxproj +0 -0
  105. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/test_projects/templates/iOSTestApp/iOSTestApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
  106. {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
  107. {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
  108. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/tests/debug_build.py +0 -0
  109. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/tests/debug_test.py +0 -0
  110. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/tests/test_basic.py +0 -0
  111. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/tests/test_build.py +0 -0
  112. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/tests/test_fixed.py +0 -0
  113. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/tests/test_manifest_wait.py +0 -0
  114. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/tests/test_minimal.py +0 -0
  115. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/tests/test_runner.py +0 -0
  116. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/__main__.py +0 -0
  117. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/cli.py +0 -0
  118. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/config_manager.py +0 -0
  119. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/config_ui.py +0 -0
  120. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/exceptions.py +0 -0
  121. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/server.py +0 -0
  122. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/__init__.py +0 -0
  123. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/build_project.py +0 -0
  124. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/debug_list_notification_history.py +0 -0
  125. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/get_build_errors.py +0 -0
  126. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/get_build_results.py +0 -0
  127. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/get_directory_listing.py +0 -0
  128. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/get_latest_test_results.py +0 -0
  129. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/get_project_schemes.py +0 -0
  130. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/get_runtime_output.py +0 -0
  131. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/list_booted_simulators.py +0 -0
  132. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/list_mac_app_windows.py +0 -0
  133. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/list_running_mac_apps.py +0 -0
  134. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/set_run_destination.py +0 -0
  135. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/stop_project.py +0 -0
  136. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/take_app_screenshot.py +0 -0
  137. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/take_simulator_screenshot.py +0 -0
  138. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/tools/take_xcode_screenshot.py +0 -0
  139. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/utils/__init__.py +0 -0
  140. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/utils/decode_active_destination.swift +0 -0
  141. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/utils/decode_xcode_recents.swift +0 -0
  142. {xcode_mcp_server-1.3.13 → xcode_mcp_server-1.3.15}/xcode_mcp_server/utils/paths.py +0 -0
  143. {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.13
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
- from xcode_mcp_server.utils.build_log_parser import parse_xcactivitylog
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)
@@ -1,6 +1,6 @@
1
1
  """Xcode MCP Server - Model Context Protocol server for Xcode integration"""
2
2
 
3
- __version__ = "1.3.13"
3
+ __version__ = "1.3.15"
4
4
 
5
5
 
6
6
  def main():
@@ -21,7 +21,21 @@ def set_allowed_folders(folders: Set[str]):
21
21
  correct comparisons.
22
22
  """
23
23
  global ALLOWED_FOLDERS
24
- ALLOWED_FOLDERS = {os.path.realpath(f).rstrip("/") for f in folders if f}
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
- # Validate project name contains only safe characters
263
- if not re.match(r'^[A-Za-z0-9][A-Za-z0-9 _-]*$', project_name):
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, spaces, hyphens, or underscores"
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
- ' return "Clean completed successfully"\n'
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
- # Clean is synchronous in AppleScript and can take minutes on large projects;
47
- # use the same budget as build/test rather than the short default.
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
- else:
56
- show_error_notification("Clean failed", project_name)
57
- raise XCodeMCPError(f"Clean failed: {output}")
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}")