This is page 9 of 19. Use http://codebase.md/justinpbarnett/unity-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .claude │ ├── prompts │ │ ├── nl-unity-suite-nl.md │ │ └── nl-unity-suite-t.md │ └── settings.json ├── .github │ ├── scripts │ │ └── mark_skipped.py │ └── workflows │ ├── bump-version.yml │ ├── claude-nl-suite.yml │ ├── github-repo-stats.yml │ └── unity-tests.yml ├── .gitignore ├── deploy-dev.bat ├── docs │ ├── CURSOR_HELP.md │ ├── CUSTOM_TOOLS.md │ ├── README-DEV-zh.md │ ├── README-DEV.md │ ├── screenshots │ │ ├── v5_01_uninstall.png │ │ ├── v5_02_install.png │ │ ├── v5_03_open_mcp_window.png │ │ ├── v5_04_rebuild_mcp_server.png │ │ ├── v5_05_rebuild_success.png │ │ ├── v6_2_create_python_tools_asset.png │ │ ├── v6_2_python_tools_asset.png │ │ ├── v6_new_ui_asset_store_version.png │ │ ├── v6_new_ui_dark.png │ │ └── v6_new_ui_light.png │ ├── TELEMETRY.md │ ├── v5_MIGRATION.md │ └── v6_NEW_UI_CHANGES.md ├── LICENSE ├── logo.png ├── mcp_source.py ├── MCPForUnity │ ├── Editor │ │ ├── AssemblyInfo.cs │ │ ├── AssemblyInfo.cs.meta │ │ ├── Data │ │ │ ├── DefaultServerConfig.cs │ │ │ ├── DefaultServerConfig.cs.meta │ │ │ ├── McpClients.cs │ │ │ ├── McpClients.cs.meta │ │ │ ├── PythonToolsAsset.cs │ │ │ └── PythonToolsAsset.cs.meta │ │ ├── Data.meta │ │ ├── Dependencies │ │ │ ├── DependencyManager.cs │ │ │ ├── DependencyManager.cs.meta │ │ │ ├── Models │ │ │ │ ├── DependencyCheckResult.cs │ │ │ │ ├── DependencyCheckResult.cs.meta │ │ │ │ ├── DependencyStatus.cs │ │ │ │ └── DependencyStatus.cs.meta │ │ │ ├── Models.meta │ │ │ ├── PlatformDetectors │ │ │ │ ├── IPlatformDetector.cs │ │ │ │ ├── IPlatformDetector.cs.meta │ │ │ │ ├── LinuxPlatformDetector.cs │ │ │ │ ├── LinuxPlatformDetector.cs.meta │ │ │ │ ├── MacOSPlatformDetector.cs │ │ │ │ ├── MacOSPlatformDetector.cs.meta │ │ │ │ ├── PlatformDetectorBase.cs │ │ │ │ ├── PlatformDetectorBase.cs.meta │ │ │ │ ├── WindowsPlatformDetector.cs │ │ │ │ └── WindowsPlatformDetector.cs.meta │ │ │ └── PlatformDetectors.meta │ │ ├── Dependencies.meta │ │ ├── External │ │ │ ├── Tommy.cs │ │ │ └── Tommy.cs.meta │ │ ├── External.meta │ │ ├── Helpers │ │ │ ├── AssetPathUtility.cs │ │ │ ├── AssetPathUtility.cs.meta │ │ │ ├── CodexConfigHelper.cs │ │ │ ├── CodexConfigHelper.cs.meta │ │ │ ├── ConfigJsonBuilder.cs │ │ │ ├── ConfigJsonBuilder.cs.meta │ │ │ ├── ExecPath.cs │ │ │ ├── ExecPath.cs.meta │ │ │ ├── GameObjectSerializer.cs │ │ │ ├── GameObjectSerializer.cs.meta │ │ │ ├── McpConfigFileHelper.cs │ │ │ ├── McpConfigFileHelper.cs.meta │ │ │ ├── McpConfigurationHelper.cs │ │ │ ├── McpConfigurationHelper.cs.meta │ │ │ ├── McpLog.cs │ │ │ ├── McpLog.cs.meta │ │ │ ├── McpPathResolver.cs │ │ │ ├── McpPathResolver.cs.meta │ │ │ ├── PackageDetector.cs │ │ │ ├── PackageDetector.cs.meta │ │ │ ├── PackageInstaller.cs │ │ │ ├── PackageInstaller.cs.meta │ │ │ ├── PortManager.cs │ │ │ ├── PortManager.cs.meta │ │ │ ├── PythonToolSyncProcessor.cs │ │ │ ├── PythonToolSyncProcessor.cs.meta │ │ │ ├── Response.cs │ │ │ ├── Response.cs.meta │ │ │ ├── ServerInstaller.cs │ │ │ ├── ServerInstaller.cs.meta │ │ │ ├── ServerPathResolver.cs │ │ │ ├── ServerPathResolver.cs.meta │ │ │ ├── TelemetryHelper.cs │ │ │ ├── TelemetryHelper.cs.meta │ │ │ ├── Vector3Helper.cs │ │ │ └── Vector3Helper.cs.meta │ │ ├── Helpers.meta │ │ ├── Importers │ │ │ ├── PythonFileImporter.cs │ │ │ └── PythonFileImporter.cs.meta │ │ ├── Importers.meta │ │ ├── MCPForUnity.Editor.asmdef │ │ ├── MCPForUnity.Editor.asmdef.meta │ │ ├── MCPForUnityBridge.cs │ │ ├── MCPForUnityBridge.cs.meta │ │ ├── Models │ │ │ ├── Command.cs │ │ │ ├── Command.cs.meta │ │ │ ├── McpClient.cs │ │ │ ├── McpClient.cs.meta │ │ │ ├── McpConfig.cs │ │ │ ├── McpConfig.cs.meta │ │ │ ├── MCPConfigServer.cs │ │ │ ├── MCPConfigServer.cs.meta │ │ │ ├── MCPConfigServers.cs │ │ │ ├── MCPConfigServers.cs.meta │ │ │ ├── McpStatus.cs │ │ │ ├── McpStatus.cs.meta │ │ │ ├── McpTypes.cs │ │ │ ├── McpTypes.cs.meta │ │ │ ├── ServerConfig.cs │ │ │ └── ServerConfig.cs.meta │ │ ├── Models.meta │ │ ├── Resources │ │ │ ├── McpForUnityResourceAttribute.cs │ │ │ ├── McpForUnityResourceAttribute.cs.meta │ │ │ ├── MenuItems │ │ │ │ ├── GetMenuItems.cs │ │ │ │ └── GetMenuItems.cs.meta │ │ │ ├── MenuItems.meta │ │ │ ├── Tests │ │ │ │ ├── GetTests.cs │ │ │ │ └── GetTests.cs.meta │ │ │ └── Tests.meta │ │ ├── Resources.meta │ │ ├── Services │ │ │ ├── BridgeControlService.cs │ │ │ ├── BridgeControlService.cs.meta │ │ │ ├── ClientConfigurationService.cs │ │ │ ├── ClientConfigurationService.cs.meta │ │ │ ├── IBridgeControlService.cs │ │ │ ├── IBridgeControlService.cs.meta │ │ │ ├── IClientConfigurationService.cs │ │ │ ├── IClientConfigurationService.cs.meta │ │ │ ├── IPackageUpdateService.cs │ │ │ ├── IPackageUpdateService.cs.meta │ │ │ ├── IPathResolverService.cs │ │ │ ├── IPathResolverService.cs.meta │ │ │ ├── IPythonToolRegistryService.cs │ │ │ ├── IPythonToolRegistryService.cs.meta │ │ │ ├── ITestRunnerService.cs │ │ │ ├── ITestRunnerService.cs.meta │ │ │ ├── IToolSyncService.cs │ │ │ ├── IToolSyncService.cs.meta │ │ │ ├── MCPServiceLocator.cs │ │ │ ├── MCPServiceLocator.cs.meta │ │ │ ├── PackageUpdateService.cs │ │ │ ├── PackageUpdateService.cs.meta │ │ │ ├── PathResolverService.cs │ │ │ ├── PathResolverService.cs.meta │ │ │ ├── PythonToolRegistryService.cs │ │ │ ├── PythonToolRegistryService.cs.meta │ │ │ ├── TestRunnerService.cs │ │ │ ├── TestRunnerService.cs.meta │ │ │ ├── ToolSyncService.cs │ │ │ └── ToolSyncService.cs.meta │ │ ├── Services.meta │ │ ├── Setup │ │ │ ├── SetupWizard.cs │ │ │ ├── SetupWizard.cs.meta │ │ │ ├── SetupWizardWindow.cs │ │ │ └── SetupWizardWindow.cs.meta │ │ ├── Setup.meta │ │ ├── Tools │ │ │ ├── CommandRegistry.cs │ │ │ ├── CommandRegistry.cs.meta │ │ │ ├── ExecuteMenuItem.cs │ │ │ ├── ExecuteMenuItem.cs.meta │ │ │ ├── ManageAsset.cs │ │ │ ├── ManageAsset.cs.meta │ │ │ ├── ManageEditor.cs │ │ │ ├── ManageEditor.cs.meta │ │ │ ├── ManageGameObject.cs │ │ │ ├── ManageGameObject.cs.meta │ │ │ ├── ManageScene.cs │ │ │ ├── ManageScene.cs.meta │ │ │ ├── ManageScript.cs │ │ │ ├── ManageScript.cs.meta │ │ │ ├── ManageShader.cs │ │ │ ├── ManageShader.cs.meta │ │ │ ├── McpForUnityToolAttribute.cs │ │ │ ├── McpForUnityToolAttribute.cs.meta │ │ │ ├── Prefabs │ │ │ │ ├── ManagePrefabs.cs │ │ │ │ └── ManagePrefabs.cs.meta │ │ │ ├── Prefabs.meta │ │ │ ├── ReadConsole.cs │ │ │ ├── ReadConsole.cs.meta │ │ │ ├── RunTests.cs │ │ │ └── RunTests.cs.meta │ │ ├── Tools.meta │ │ ├── Windows │ │ │ ├── ManualConfigEditorWindow.cs │ │ │ ├── ManualConfigEditorWindow.cs.meta │ │ │ ├── MCPForUnityEditorWindow.cs │ │ │ ├── MCPForUnityEditorWindow.cs.meta │ │ │ ├── MCPForUnityEditorWindowNew.cs │ │ │ ├── MCPForUnityEditorWindowNew.cs.meta │ │ │ ├── MCPForUnityEditorWindowNew.uss │ │ │ ├── MCPForUnityEditorWindowNew.uss.meta │ │ │ ├── MCPForUnityEditorWindowNew.uxml │ │ │ ├── MCPForUnityEditorWindowNew.uxml.meta │ │ │ ├── VSCodeManualSetupWindow.cs │ │ │ └── VSCodeManualSetupWindow.cs.meta │ │ └── Windows.meta │ ├── Editor.meta │ ├── package.json │ ├── package.json.meta │ ├── README.md │ ├── README.md.meta │ ├── Runtime │ │ ├── MCPForUnity.Runtime.asmdef │ │ ├── MCPForUnity.Runtime.asmdef.meta │ │ ├── Serialization │ │ │ ├── UnityTypeConverters.cs │ │ │ └── UnityTypeConverters.cs.meta │ │ └── Serialization.meta │ ├── Runtime.meta │ └── UnityMcpServer~ │ └── src │ ├── __init__.py │ ├── config.py │ ├── Dockerfile │ ├── models.py │ ├── module_discovery.py │ ├── port_discovery.py │ ├── pyproject.toml │ ├── pyrightconfig.json │ ├── registry │ │ ├── __init__.py │ │ ├── resource_registry.py │ │ └── tool_registry.py │ ├── reload_sentinel.py │ ├── resources │ │ ├── __init__.py │ │ ├── menu_items.py │ │ └── tests.py │ ├── server_version.txt │ ├── server.py │ ├── telemetry_decorator.py │ ├── telemetry.py │ ├── test_telemetry.py │ ├── tools │ │ ├── __init__.py │ │ ├── execute_menu_item.py │ │ ├── manage_asset.py │ │ ├── manage_editor.py │ │ ├── manage_gameobject.py │ │ ├── manage_prefabs.py │ │ ├── manage_scene.py │ │ ├── manage_script.py │ │ ├── manage_shader.py │ │ ├── read_console.py │ │ ├── resource_tools.py │ │ ├── run_tests.py │ │ └── script_apply_edits.py │ ├── unity_connection.py │ └── uv.lock ├── prune_tool_results.py ├── README-zh.md ├── README.md ├── restore-dev.bat ├── scripts │ └── validate-nlt-coverage.sh ├── test_unity_socket_framing.py ├── TestProjects │ └── UnityMCPTests │ ├── .gitignore │ ├── Assets │ │ ├── Editor.meta │ │ ├── Scenes │ │ │ ├── SampleScene.unity │ │ │ └── SampleScene.unity.meta │ │ ├── Scenes.meta │ │ ├── Scripts │ │ │ ├── Hello.cs │ │ │ ├── Hello.cs.meta │ │ │ ├── LongUnityScriptClaudeTest.cs │ │ │ ├── LongUnityScriptClaudeTest.cs.meta │ │ │ ├── TestAsmdef │ │ │ │ ├── CustomComponent.cs │ │ │ │ ├── CustomComponent.cs.meta │ │ │ │ ├── TestAsmdef.asmdef │ │ │ │ └── TestAsmdef.asmdef.meta │ │ │ └── TestAsmdef.meta │ │ ├── Scripts.meta │ │ ├── Tests │ │ │ ├── EditMode │ │ │ │ ├── Data │ │ │ │ │ ├── PythonToolsAssetTests.cs │ │ │ │ │ └── PythonToolsAssetTests.cs.meta │ │ │ │ ├── Data.meta │ │ │ │ ├── Helpers │ │ │ │ │ ├── CodexConfigHelperTests.cs │ │ │ │ │ ├── CodexConfigHelperTests.cs.meta │ │ │ │ │ ├── WriteToConfigTests.cs │ │ │ │ │ └── WriteToConfigTests.cs.meta │ │ │ │ ├── Helpers.meta │ │ │ │ ├── MCPForUnityTests.Editor.asmdef │ │ │ │ ├── MCPForUnityTests.Editor.asmdef.meta │ │ │ │ ├── Resources │ │ │ │ │ ├── GetMenuItemsTests.cs │ │ │ │ │ └── GetMenuItemsTests.cs.meta │ │ │ │ ├── Resources.meta │ │ │ │ ├── Services │ │ │ │ │ ├── PackageUpdateServiceTests.cs │ │ │ │ │ ├── PackageUpdateServiceTests.cs.meta │ │ │ │ │ ├── PythonToolRegistryServiceTests.cs │ │ │ │ │ ├── PythonToolRegistryServiceTests.cs.meta │ │ │ │ │ ├── ToolSyncServiceTests.cs │ │ │ │ │ └── ToolSyncServiceTests.cs.meta │ │ │ │ ├── Services.meta │ │ │ │ ├── Tools │ │ │ │ │ ├── AIPropertyMatchingTests.cs │ │ │ │ │ ├── AIPropertyMatchingTests.cs.meta │ │ │ │ │ ├── CommandRegistryTests.cs │ │ │ │ │ ├── CommandRegistryTests.cs.meta │ │ │ │ │ ├── ComponentResolverTests.cs │ │ │ │ │ ├── ComponentResolverTests.cs.meta │ │ │ │ │ ├── ExecuteMenuItemTests.cs │ │ │ │ │ ├── ExecuteMenuItemTests.cs.meta │ │ │ │ │ ├── ManageGameObjectTests.cs │ │ │ │ │ ├── ManageGameObjectTests.cs.meta │ │ │ │ │ ├── ManagePrefabsTests.cs │ │ │ │ │ ├── ManagePrefabsTests.cs.meta │ │ │ │ │ ├── ManageScriptValidationTests.cs │ │ │ │ │ ├── ManageScriptValidationTests.cs.meta │ │ │ │ │ └── MaterialMeshInstantiationTests.cs │ │ │ │ ├── Tools.meta │ │ │ │ ├── Windows │ │ │ │ │ ├── ManualConfigJsonBuilderTests.cs │ │ │ │ │ └── ManualConfigJsonBuilderTests.cs.meta │ │ │ │ └── Windows.meta │ │ │ └── EditMode.meta │ │ └── Tests.meta │ ├── Packages │ │ └── manifest.json │ └── ProjectSettings │ ├── Packages │ │ └── com.unity.testtools.codecoverage │ │ └── Settings.json │ └── ProjectVersion.txt ├── tests │ ├── conftest.py │ ├── test_edit_normalization_and_noop.py │ ├── test_edit_strict_and_warnings.py │ ├── test_find_in_file_minimal.py │ ├── test_get_sha.py │ ├── test_improved_anchor_matching.py │ ├── test_logging_stdout.py │ ├── test_manage_script_uri.py │ ├── test_read_console_truncate.py │ ├── test_read_resource_minimal.py │ ├── test_resources_api.py │ ├── test_script_editing.py │ ├── test_script_tools.py │ ├── test_telemetry_endpoint_validation.py │ ├── test_telemetry_queue_worker.py │ ├── test_telemetry_subaction.py │ ├── test_transport_framing.py │ └── test_validate_script_summary.py ├── tools │ └── stress_mcp.py └── UnityMcpBridge ├── Editor │ ├── AssemblyInfo.cs │ ├── AssemblyInfo.cs.meta │ ├── Data │ │ ├── DefaultServerConfig.cs │ │ ├── DefaultServerConfig.cs.meta │ │ ├── McpClients.cs │ │ └── McpClients.cs.meta │ ├── Data.meta │ ├── Dependencies │ │ ├── DependencyManager.cs │ │ ├── DependencyManager.cs.meta │ │ ├── Models │ │ │ ├── DependencyCheckResult.cs │ │ │ ├── DependencyCheckResult.cs.meta │ │ │ ├── DependencyStatus.cs │ │ │ └── DependencyStatus.cs.meta │ │ ├── Models.meta │ │ ├── PlatformDetectors │ │ │ ├── IPlatformDetector.cs │ │ │ ├── IPlatformDetector.cs.meta │ │ │ ├── LinuxPlatformDetector.cs │ │ │ ├── LinuxPlatformDetector.cs.meta │ │ │ ├── MacOSPlatformDetector.cs │ │ │ ├── MacOSPlatformDetector.cs.meta │ │ │ ├── PlatformDetectorBase.cs │ │ │ ├── PlatformDetectorBase.cs.meta │ │ │ ├── WindowsPlatformDetector.cs │ │ │ └── WindowsPlatformDetector.cs.meta │ │ └── PlatformDetectors.meta │ ├── Dependencies.meta │ ├── External │ │ ├── Tommy.cs │ │ └── Tommy.cs.meta │ ├── External.meta │ ├── Helpers │ │ ├── AssetPathUtility.cs │ │ ├── AssetPathUtility.cs.meta │ │ ├── CodexConfigHelper.cs │ │ ├── CodexConfigHelper.cs.meta │ │ ├── ConfigJsonBuilder.cs │ │ ├── ConfigJsonBuilder.cs.meta │ │ ├── ExecPath.cs │ │ ├── ExecPath.cs.meta │ │ ├── GameObjectSerializer.cs │ │ ├── GameObjectSerializer.cs.meta │ │ ├── McpConfigFileHelper.cs │ │ ├── McpConfigFileHelper.cs.meta │ │ ├── McpConfigurationHelper.cs │ │ ├── McpConfigurationHelper.cs.meta │ │ ├── McpLog.cs │ │ ├── McpLog.cs.meta │ │ ├── McpPathResolver.cs │ │ ├── McpPathResolver.cs.meta │ │ ├── PackageDetector.cs │ │ ├── PackageDetector.cs.meta │ │ ├── PackageInstaller.cs │ │ ├── PackageInstaller.cs.meta │ │ ├── PortManager.cs │ │ ├── PortManager.cs.meta │ │ ├── Response.cs │ │ ├── Response.cs.meta │ │ ├── ServerInstaller.cs │ │ ├── ServerInstaller.cs.meta │ │ ├── ServerPathResolver.cs │ │ ├── ServerPathResolver.cs.meta │ │ ├── TelemetryHelper.cs │ │ ├── TelemetryHelper.cs.meta │ │ ├── Vector3Helper.cs │ │ └── Vector3Helper.cs.meta │ ├── Helpers.meta │ ├── MCPForUnity.Editor.asmdef │ ├── MCPForUnity.Editor.asmdef.meta │ ├── MCPForUnityBridge.cs │ ├── MCPForUnityBridge.cs.meta │ ├── Models │ │ ├── Command.cs │ │ ├── Command.cs.meta │ │ ├── McpClient.cs │ │ ├── McpClient.cs.meta │ │ ├── McpConfig.cs │ │ ├── McpConfig.cs.meta │ │ ├── MCPConfigServer.cs │ │ ├── MCPConfigServer.cs.meta │ │ ├── MCPConfigServers.cs │ │ ├── MCPConfigServers.cs.meta │ │ ├── McpStatus.cs │ │ ├── McpStatus.cs.meta │ │ ├── McpTypes.cs │ │ ├── McpTypes.cs.meta │ │ ├── ServerConfig.cs │ │ └── ServerConfig.cs.meta │ ├── Models.meta │ ├── Setup │ │ ├── SetupWizard.cs │ │ ├── SetupWizard.cs.meta │ │ ├── SetupWizardWindow.cs │ │ └── SetupWizardWindow.cs.meta │ ├── Setup.meta │ ├── Tools │ │ ├── CommandRegistry.cs │ │ ├── CommandRegistry.cs.meta │ │ ├── ManageAsset.cs │ │ ├── ManageAsset.cs.meta │ │ ├── ManageEditor.cs │ │ ├── ManageEditor.cs.meta │ │ ├── ManageGameObject.cs │ │ ├── ManageGameObject.cs.meta │ │ ├── ManageScene.cs │ │ ├── ManageScene.cs.meta │ │ ├── ManageScript.cs │ │ ├── ManageScript.cs.meta │ │ ├── ManageShader.cs │ │ ├── ManageShader.cs.meta │ │ ├── McpForUnityToolAttribute.cs │ │ ├── McpForUnityToolAttribute.cs.meta │ │ ├── MenuItems │ │ │ ├── ManageMenuItem.cs │ │ │ ├── ManageMenuItem.cs.meta │ │ │ ├── MenuItemExecutor.cs │ │ │ ├── MenuItemExecutor.cs.meta │ │ │ ├── MenuItemsReader.cs │ │ │ └── MenuItemsReader.cs.meta │ │ ├── MenuItems.meta │ │ ├── Prefabs │ │ │ ├── ManagePrefabs.cs │ │ │ └── ManagePrefabs.cs.meta │ │ ├── Prefabs.meta │ │ ├── ReadConsole.cs │ │ └── ReadConsole.cs.meta │ ├── Tools.meta │ ├── Windows │ │ ├── ManualConfigEditorWindow.cs │ │ ├── ManualConfigEditorWindow.cs.meta │ │ ├── MCPForUnityEditorWindow.cs │ │ ├── MCPForUnityEditorWindow.cs.meta │ │ ├── VSCodeManualSetupWindow.cs │ │ └── VSCodeManualSetupWindow.cs.meta │ └── Windows.meta ├── Editor.meta ├── package.json ├── package.json.meta ├── README.md ├── README.md.meta ├── Runtime │ ├── MCPForUnity.Runtime.asmdef │ ├── MCPForUnity.Runtime.asmdef.meta │ ├── Serialization │ │ ├── UnityTypeConverters.cs │ │ └── UnityTypeConverters.cs.meta │ └── Serialization.meta ├── Runtime.meta └── UnityMcpServer~ └── src ├── __init__.py ├── config.py ├── Dockerfile ├── port_discovery.py ├── pyproject.toml ├── pyrightconfig.json ├── registry │ ├── __init__.py │ └── tool_registry.py ├── reload_sentinel.py ├── server_version.txt ├── server.py ├── telemetry_decorator.py ├── telemetry.py ├── test_telemetry.py ├── tools │ ├── __init__.py │ ├── manage_asset.py │ ├── manage_editor.py │ ├── manage_gameobject.py │ ├── manage_menu_item.py │ ├── manage_prefabs.py │ ├── manage_scene.py │ ├── manage_script.py │ ├── manage_shader.py │ ├── read_console.py │ ├── resource_tools.py │ └── script_apply_edits.py ├── unity_connection.py └── uv.lock ``` # Files -------------------------------------------------------------------------------- /MCPForUnity/Editor/Setup/SetupWizardWindow.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System; 2 | using System.Linq; 3 | using MCPForUnity.Editor.Data; 4 | using MCPForUnity.Editor.Dependencies; 5 | using MCPForUnity.Editor.Dependencies.Models; 6 | using MCPForUnity.Editor.Helpers; 7 | using MCPForUnity.Editor.Models; 8 | using UnityEditor; 9 | using UnityEngine; 10 | 11 | namespace MCPForUnity.Editor.Setup 12 | { 13 | /// <summary> 14 | /// Setup wizard window for guiding users through dependency installation 15 | /// </summary> 16 | public class SetupWizardWindow : EditorWindow 17 | { 18 | private DependencyCheckResult _dependencyResult; 19 | private Vector2 _scrollPosition; 20 | private int _currentStep = 0; 21 | private McpClients _mcpClients; 22 | private int _selectedClientIndex = 0; 23 | 24 | private readonly string[] _stepTitles = { 25 | "Setup", 26 | "Configure", 27 | "Complete" 28 | }; 29 | 30 | public static void ShowWindow(DependencyCheckResult dependencyResult = null) 31 | { 32 | var window = GetWindow<SetupWizardWindow>("MCP for Unity Setup"); 33 | window.minSize = new Vector2(500, 400); 34 | window.maxSize = new Vector2(800, 600); 35 | window._dependencyResult = dependencyResult ?? DependencyManager.CheckAllDependencies(); 36 | window.Show(); 37 | } 38 | 39 | private void OnEnable() 40 | { 41 | if (_dependencyResult == null) 42 | { 43 | _dependencyResult = DependencyManager.CheckAllDependencies(); 44 | } 45 | 46 | _mcpClients = new McpClients(); 47 | 48 | // Check client configurations on startup 49 | foreach (var client in _mcpClients.clients) 50 | { 51 | CheckClientConfiguration(client); 52 | } 53 | } 54 | 55 | private void OnGUI() 56 | { 57 | DrawHeader(); 58 | DrawProgressBar(); 59 | 60 | _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition); 61 | 62 | switch (_currentStep) 63 | { 64 | case 0: DrawSetupStep(); break; 65 | case 1: DrawConfigureStep(); break; 66 | case 2: DrawCompleteStep(); break; 67 | } 68 | 69 | EditorGUILayout.EndScrollView(); 70 | 71 | DrawFooter(); 72 | } 73 | 74 | private void DrawHeader() 75 | { 76 | EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); 77 | GUILayout.Label("MCP for Unity Setup Wizard", EditorStyles.boldLabel); 78 | GUILayout.FlexibleSpace(); 79 | GUILayout.Label($"Step {_currentStep + 1} of {_stepTitles.Length}"); 80 | EditorGUILayout.EndHorizontal(); 81 | 82 | EditorGUILayout.Space(); 83 | 84 | // Step title 85 | var titleStyle = new GUIStyle(EditorStyles.largeLabel) 86 | { 87 | fontSize = 16, 88 | fontStyle = FontStyle.Bold 89 | }; 90 | EditorGUILayout.LabelField(_stepTitles[_currentStep], titleStyle); 91 | EditorGUILayout.Space(); 92 | } 93 | 94 | private void DrawProgressBar() 95 | { 96 | var rect = EditorGUILayout.GetControlRect(false, 4); 97 | var progress = (_currentStep + 1) / (float)_stepTitles.Length; 98 | EditorGUI.ProgressBar(rect, progress, ""); 99 | EditorGUILayout.Space(); 100 | } 101 | 102 | private void DrawSetupStep() 103 | { 104 | // Welcome section 105 | DrawSectionTitle("MCP for Unity Setup"); 106 | 107 | EditorGUILayout.LabelField( 108 | "This wizard will help you set up MCP for Unity to connect AI assistants with your Unity Editor.", 109 | EditorStyles.wordWrappedLabel 110 | ); 111 | EditorGUILayout.Space(); 112 | 113 | // Dependency check section 114 | EditorGUILayout.BeginHorizontal(); 115 | DrawSectionTitle("System Check", 14); 116 | GUILayout.FlexibleSpace(); 117 | if (GUILayout.Button("Refresh", GUILayout.Width(60), GUILayout.Height(20))) 118 | { 119 | _dependencyResult = DependencyManager.CheckAllDependencies(); 120 | } 121 | EditorGUILayout.EndHorizontal(); 122 | 123 | // Show simplified dependency status 124 | foreach (var dep in _dependencyResult.Dependencies) 125 | { 126 | DrawSimpleDependencyStatus(dep); 127 | } 128 | 129 | // Overall status and installation guidance 130 | EditorGUILayout.Space(); 131 | if (!_dependencyResult.IsSystemReady) 132 | { 133 | // Only show critical warnings when dependencies are actually missing 134 | EditorGUILayout.HelpBox( 135 | "⚠️ Missing Dependencies: MCP for Unity requires Python 3.10+ and UV package manager to function properly.", 136 | MessageType.Warning 137 | ); 138 | 139 | EditorGUILayout.Space(); 140 | EditorGUILayout.BeginVertical(EditorStyles.helpBox); 141 | DrawErrorStatus("Installation Required"); 142 | 143 | var recommendations = DependencyManager.GetInstallationRecommendations(); 144 | EditorGUILayout.LabelField(recommendations, EditorStyles.wordWrappedLabel); 145 | 146 | EditorGUILayout.Space(); 147 | if (GUILayout.Button("Open Installation Links", GUILayout.Height(25))) 148 | { 149 | OpenInstallationUrls(); 150 | } 151 | EditorGUILayout.EndVertical(); 152 | } 153 | else 154 | { 155 | DrawSuccessStatus("System Ready"); 156 | EditorGUILayout.LabelField("All requirements are met. You can proceed to configure your AI clients.", EditorStyles.wordWrappedLabel); 157 | } 158 | } 159 | 160 | 161 | 162 | private void DrawCompleteStep() 163 | { 164 | DrawSectionTitle("Setup Complete"); 165 | 166 | // Refresh dependency check with caching to avoid heavy operations on every repaint 167 | if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2) 168 | { 169 | _dependencyResult = DependencyManager.CheckAllDependencies(); 170 | } 171 | 172 | if (_dependencyResult.IsSystemReady) 173 | { 174 | DrawSuccessStatus("MCP for Unity Ready!"); 175 | 176 | EditorGUILayout.HelpBox( 177 | "🎉 MCP for Unity is now set up and ready to use!\n\n" + 178 | "• Dependencies verified\n" + 179 | "• MCP server ready\n" + 180 | "• Client configuration accessible", 181 | MessageType.Info 182 | ); 183 | 184 | EditorGUILayout.Space(); 185 | EditorGUILayout.BeginHorizontal(); 186 | if (GUILayout.Button("Documentation", GUILayout.Height(30))) 187 | { 188 | Application.OpenURL("https://github.com/CoplayDev/unity-mcp"); 189 | } 190 | if (GUILayout.Button("Client Settings", GUILayout.Height(30))) 191 | { 192 | Windows.MCPForUnityEditorWindow.ShowWindow(); 193 | } 194 | EditorGUILayout.EndHorizontal(); 195 | } 196 | else 197 | { 198 | DrawErrorStatus("Setup Incomplete - Package Non-Functional"); 199 | 200 | EditorGUILayout.HelpBox( 201 | "🚨 MCP for Unity CANNOT work - dependencies still missing!\n\n" + 202 | "Install ALL required dependencies before the package will function.", 203 | MessageType.Error 204 | ); 205 | 206 | var missingDeps = _dependencyResult.GetMissingRequired(); 207 | if (missingDeps.Count > 0) 208 | { 209 | EditorGUILayout.Space(); 210 | EditorGUILayout.LabelField("Still Missing:", EditorStyles.boldLabel); 211 | foreach (var dep in missingDeps) 212 | { 213 | EditorGUILayout.LabelField($"✗ {dep.Name}", EditorStyles.label); 214 | } 215 | } 216 | 217 | EditorGUILayout.Space(); 218 | if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30))) 219 | { 220 | _currentStep = 0; 221 | } 222 | } 223 | } 224 | 225 | // Helper methods for consistent UI components 226 | private void DrawSectionTitle(string title, int fontSize = 16) 227 | { 228 | var titleStyle = new GUIStyle(EditorStyles.boldLabel) 229 | { 230 | fontSize = fontSize, 231 | fontStyle = FontStyle.Bold 232 | }; 233 | EditorGUILayout.LabelField(title, titleStyle); 234 | EditorGUILayout.Space(); 235 | } 236 | 237 | private void DrawSuccessStatus(string message) 238 | { 239 | var originalColor = GUI.color; 240 | GUI.color = Color.green; 241 | EditorGUILayout.LabelField($"✓ {message}", EditorStyles.boldLabel); 242 | GUI.color = originalColor; 243 | EditorGUILayout.Space(); 244 | } 245 | 246 | private void DrawErrorStatus(string message) 247 | { 248 | var originalColor = GUI.color; 249 | GUI.color = Color.red; 250 | EditorGUILayout.LabelField($"✗ {message}", EditorStyles.boldLabel); 251 | GUI.color = originalColor; 252 | EditorGUILayout.Space(); 253 | } 254 | 255 | private void DrawSimpleDependencyStatus(DependencyStatus dep) 256 | { 257 | EditorGUILayout.BeginHorizontal(); 258 | 259 | var statusIcon = dep.IsAvailable ? "✓" : "✗"; 260 | var statusColor = dep.IsAvailable ? Color.green : Color.red; 261 | 262 | var originalColor = GUI.color; 263 | GUI.color = statusColor; 264 | GUILayout.Label(statusIcon, GUILayout.Width(20)); 265 | EditorGUILayout.LabelField(dep.Name, EditorStyles.boldLabel); 266 | GUI.color = originalColor; 267 | 268 | if (!dep.IsAvailable && !string.IsNullOrEmpty(dep.ErrorMessage)) 269 | { 270 | EditorGUILayout.LabelField($"({dep.ErrorMessage})", EditorStyles.miniLabel); 271 | } 272 | 273 | EditorGUILayout.EndHorizontal(); 274 | } 275 | 276 | private void DrawConfigureStep() 277 | { 278 | DrawSectionTitle("AI Client Configuration"); 279 | 280 | // Check dependencies first (with caching to avoid heavy operations on every repaint) 281 | if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2) 282 | { 283 | _dependencyResult = DependencyManager.CheckAllDependencies(); 284 | } 285 | if (!_dependencyResult.IsSystemReady) 286 | { 287 | DrawErrorStatus("Cannot Configure - System Requirements Not Met"); 288 | 289 | EditorGUILayout.HelpBox( 290 | "Client configuration requires system dependencies to be installed first. Please complete setup before proceeding.", 291 | MessageType.Warning 292 | ); 293 | 294 | if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30))) 295 | { 296 | _currentStep = 0; 297 | } 298 | return; 299 | } 300 | 301 | EditorGUILayout.LabelField( 302 | "Configure your AI assistants to work with Unity. Select a client below to set it up:", 303 | EditorStyles.wordWrappedLabel 304 | ); 305 | EditorGUILayout.Space(); 306 | 307 | // Client selection and configuration 308 | if (_mcpClients.clients.Count > 0) 309 | { 310 | // Client selector dropdown 311 | string[] clientNames = _mcpClients.clients.Select(c => c.name).ToArray(); 312 | EditorGUI.BeginChangeCheck(); 313 | _selectedClientIndex = EditorGUILayout.Popup("Select AI Client:", _selectedClientIndex, clientNames); 314 | if (EditorGUI.EndChangeCheck()) 315 | { 316 | _selectedClientIndex = Mathf.Clamp(_selectedClientIndex, 0, _mcpClients.clients.Count - 1); 317 | // Refresh client status when selection changes 318 | CheckClientConfiguration(_mcpClients.clients[_selectedClientIndex]); 319 | } 320 | 321 | EditorGUILayout.Space(); 322 | 323 | var selectedClient = _mcpClients.clients[_selectedClientIndex]; 324 | DrawClientConfigurationInWizard(selectedClient); 325 | 326 | EditorGUILayout.Space(); 327 | 328 | // Batch configuration option 329 | EditorGUILayout.BeginVertical(EditorStyles.helpBox); 330 | EditorGUILayout.LabelField("Quick Setup", EditorStyles.boldLabel); 331 | EditorGUILayout.LabelField( 332 | "Automatically configure all detected AI clients at once:", 333 | EditorStyles.wordWrappedLabel 334 | ); 335 | EditorGUILayout.Space(); 336 | 337 | if (GUILayout.Button("Configure All Detected Clients", GUILayout.Height(30))) 338 | { 339 | ConfigureAllClientsInWizard(); 340 | } 341 | EditorGUILayout.EndVertical(); 342 | } 343 | else 344 | { 345 | EditorGUILayout.HelpBox("No AI clients detected. Make sure you have Claude Code, Cursor, or VSCode installed.", MessageType.Info); 346 | } 347 | 348 | EditorGUILayout.Space(); 349 | EditorGUILayout.HelpBox( 350 | "💡 You might need to restart your AI client after configuring.", 351 | MessageType.Info 352 | ); 353 | } 354 | 355 | private void DrawFooter() 356 | { 357 | EditorGUILayout.Space(); 358 | EditorGUILayout.BeginHorizontal(); 359 | 360 | // Back button 361 | GUI.enabled = _currentStep > 0; 362 | if (GUILayout.Button("Back", GUILayout.Width(60))) 363 | { 364 | _currentStep--; 365 | } 366 | 367 | GUILayout.FlexibleSpace(); 368 | 369 | // Skip button 370 | if (GUILayout.Button("Skip", GUILayout.Width(60))) 371 | { 372 | bool dismiss = EditorUtility.DisplayDialog( 373 | "Skip Setup", 374 | "⚠️ Skipping setup will leave MCP for Unity non-functional!\n\n" + 375 | "You can restart setup from: Window > MCP for Unity > Setup Wizard (Required)", 376 | "Skip Anyway", 377 | "Cancel" 378 | ); 379 | 380 | if (dismiss) 381 | { 382 | SetupWizard.MarkSetupDismissed(); 383 | Close(); 384 | } 385 | } 386 | 387 | // Next/Done button 388 | GUI.enabled = true; 389 | string buttonText = _currentStep == _stepTitles.Length - 1 ? "Done" : "Next"; 390 | 391 | if (GUILayout.Button(buttonText, GUILayout.Width(80))) 392 | { 393 | if (_currentStep == _stepTitles.Length - 1) 394 | { 395 | SetupWizard.MarkSetupCompleted(); 396 | Close(); 397 | } 398 | else 399 | { 400 | _currentStep++; 401 | } 402 | } 403 | 404 | GUI.enabled = true; 405 | EditorGUILayout.EndHorizontal(); 406 | } 407 | 408 | private void DrawClientConfigurationInWizard(McpClient client) 409 | { 410 | EditorGUILayout.BeginVertical(EditorStyles.helpBox); 411 | 412 | EditorGUILayout.LabelField($"{client.name} Configuration", EditorStyles.boldLabel); 413 | EditorGUILayout.Space(); 414 | 415 | // Show current status 416 | var statusColor = GetClientStatusColor(client); 417 | var originalColor = GUI.color; 418 | GUI.color = statusColor; 419 | EditorGUILayout.LabelField($"Status: {client.configStatus}", EditorStyles.label); 420 | GUI.color = originalColor; 421 | 422 | EditorGUILayout.Space(); 423 | 424 | // Configuration buttons 425 | EditorGUILayout.BeginHorizontal(); 426 | 427 | if (client.mcpType == McpTypes.ClaudeCode) 428 | { 429 | // Special handling for Claude Code 430 | bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude()); 431 | if (claudeAvailable) 432 | { 433 | bool isConfigured = client.status == McpStatus.Configured; 434 | string buttonText = isConfigured ? "Unregister" : "Register"; 435 | if (GUILayout.Button($"{buttonText} with Claude Code")) 436 | { 437 | if (isConfigured) 438 | { 439 | UnregisterFromClaudeCode(client); 440 | } 441 | else 442 | { 443 | RegisterWithClaudeCode(client); 444 | } 445 | } 446 | } 447 | else 448 | { 449 | EditorGUILayout.HelpBox("Claude Code not found. Please install Claude Code first.", MessageType.Warning); 450 | if (GUILayout.Button("Open Claude Code Website")) 451 | { 452 | Application.OpenURL("https://claude.ai/download"); 453 | } 454 | } 455 | } 456 | else 457 | { 458 | // Standard client configuration 459 | if (GUILayout.Button($"Configure {client.name}")) 460 | { 461 | ConfigureClientInWizard(client); 462 | } 463 | 464 | if (GUILayout.Button("Manual Setup")) 465 | { 466 | ShowManualSetupInWizard(client); 467 | } 468 | } 469 | 470 | EditorGUILayout.EndHorizontal(); 471 | EditorGUILayout.EndVertical(); 472 | } 473 | 474 | private Color GetClientStatusColor(McpClient client) 475 | { 476 | return client.status switch 477 | { 478 | McpStatus.Configured => Color.green, 479 | McpStatus.Running => Color.green, 480 | McpStatus.Connected => Color.green, 481 | McpStatus.IncorrectPath => Color.yellow, 482 | McpStatus.CommunicationError => Color.yellow, 483 | McpStatus.NoResponse => Color.yellow, 484 | _ => Color.red 485 | }; 486 | } 487 | 488 | private void ConfigureClientInWizard(McpClient client) 489 | { 490 | try 491 | { 492 | string result = PerformClientConfiguration(client); 493 | 494 | EditorUtility.DisplayDialog( 495 | $"{client.name} Configuration", 496 | result, 497 | "OK" 498 | ); 499 | 500 | // Refresh client status 501 | CheckClientConfiguration(client); 502 | Repaint(); 503 | } 504 | catch (System.Exception ex) 505 | { 506 | EditorUtility.DisplayDialog( 507 | "Configuration Error", 508 | $"Failed to configure {client.name}: {ex.Message}", 509 | "OK" 510 | ); 511 | } 512 | } 513 | 514 | private void ConfigureAllClientsInWizard() 515 | { 516 | int successCount = 0; 517 | int totalCount = _mcpClients.clients.Count; 518 | 519 | foreach (var client in _mcpClients.clients) 520 | { 521 | try 522 | { 523 | if (client.mcpType == McpTypes.ClaudeCode) 524 | { 525 | if (!string.IsNullOrEmpty(ExecPath.ResolveClaude()) && client.status != McpStatus.Configured) 526 | { 527 | RegisterWithClaudeCode(client); 528 | successCount++; 529 | } 530 | else if (client.status == McpStatus.Configured) 531 | { 532 | successCount++; // Already configured 533 | } 534 | } 535 | else 536 | { 537 | string result = PerformClientConfiguration(client); 538 | if (result.Contains("success", System.StringComparison.OrdinalIgnoreCase)) 539 | { 540 | successCount++; 541 | } 542 | } 543 | 544 | CheckClientConfiguration(client); 545 | } 546 | catch (System.Exception ex) 547 | { 548 | McpLog.Error($"Failed to configure {client.name}: {ex.Message}"); 549 | } 550 | } 551 | 552 | EditorUtility.DisplayDialog( 553 | "Batch Configuration Complete", 554 | $"Successfully configured {successCount} out of {totalCount} clients.\n\n" + 555 | "Restart your AI clients for changes to take effect.", 556 | "OK" 557 | ); 558 | 559 | Repaint(); 560 | } 561 | 562 | private void RegisterWithClaudeCode(McpClient client) 563 | { 564 | try 565 | { 566 | string pythonDir = McpPathResolver.FindPackagePythonDirectory(); 567 | string claudePath = ExecPath.ResolveClaude(); 568 | string uvPath = ExecPath.ResolveUv() ?? "uv"; 569 | 570 | string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py"; 571 | 572 | if (!ExecPath.TryRun(claudePath, args, null, out var stdout, out var stderr, 15000, McpPathResolver.GetPathPrepend())) 573 | { 574 | if ((stdout + stderr).Contains("already exists", System.StringComparison.OrdinalIgnoreCase)) 575 | { 576 | CheckClientConfiguration(client); 577 | EditorUtility.DisplayDialog("Claude Code", "MCP for Unity is already registered with Claude Code.", "OK"); 578 | } 579 | else 580 | { 581 | throw new System.Exception($"Registration failed: {stderr}"); 582 | } 583 | } 584 | else 585 | { 586 | CheckClientConfiguration(client); 587 | EditorUtility.DisplayDialog("Claude Code", "Successfully registered MCP for Unity with Claude Code!", "OK"); 588 | } 589 | } 590 | catch (System.Exception ex) 591 | { 592 | EditorUtility.DisplayDialog("Registration Error", $"Failed to register with Claude Code: {ex.Message}", "OK"); 593 | } 594 | } 595 | 596 | private void UnregisterFromClaudeCode(McpClient client) 597 | { 598 | try 599 | { 600 | string claudePath = ExecPath.ResolveClaude(); 601 | if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", null, out var stdout, out var stderr, 10000, McpPathResolver.GetPathPrepend())) 602 | { 603 | CheckClientConfiguration(client); 604 | EditorUtility.DisplayDialog("Claude Code", "Successfully unregistered MCP for Unity from Claude Code.", "OK"); 605 | } 606 | else 607 | { 608 | throw new System.Exception($"Unregistration failed: {stderr}"); 609 | } 610 | } 611 | catch (System.Exception ex) 612 | { 613 | EditorUtility.DisplayDialog("Unregistration Error", $"Failed to unregister from Claude Code: {ex.Message}", "OK"); 614 | } 615 | } 616 | 617 | private string PerformClientConfiguration(McpClient client) 618 | { 619 | // This mirrors the logic from MCPForUnityEditorWindow.ConfigureMcpClient 620 | string configPath = McpConfigurationHelper.GetClientConfigPath(client); 621 | string pythonDir = McpPathResolver.FindPackagePythonDirectory(); 622 | 623 | if (string.IsNullOrEmpty(pythonDir)) 624 | { 625 | return "Manual configuration required - Python server directory not found."; 626 | } 627 | 628 | McpConfigurationHelper.EnsureConfigDirectoryExists(configPath); 629 | return McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client); 630 | } 631 | 632 | private void ShowManualSetupInWizard(McpClient client) 633 | { 634 | string configPath = McpConfigurationHelper.GetClientConfigPath(client); 635 | string pythonDir = McpPathResolver.FindPackagePythonDirectory(); 636 | string uvPath = ServerInstaller.FindUvPath(); 637 | 638 | if (string.IsNullOrEmpty(uvPath)) 639 | { 640 | EditorUtility.DisplayDialog("Manual Setup", "UV package manager not found. Please install UV first.", "OK"); 641 | return; 642 | } 643 | 644 | // Build manual configuration using the sophisticated helper logic 645 | string result = McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client); 646 | string manualConfig; 647 | 648 | if (result == "Configured successfully") 649 | { 650 | // Read back the configuration that was written 651 | try 652 | { 653 | manualConfig = System.IO.File.ReadAllText(configPath); 654 | } 655 | catch 656 | { 657 | manualConfig = "Configuration written successfully, but could not read back for display."; 658 | } 659 | } 660 | else 661 | { 662 | manualConfig = $"Configuration failed: {result}"; 663 | } 664 | 665 | EditorUtility.DisplayDialog( 666 | $"Manual Setup - {client.name}", 667 | $"Configuration file location:\n{configPath}\n\n" + 668 | $"Configuration result:\n{manualConfig}", 669 | "OK" 670 | ); 671 | } 672 | 673 | private void CheckClientConfiguration(McpClient client) 674 | { 675 | // Basic status check - could be enhanced to mirror MCPForUnityEditorWindow logic 676 | try 677 | { 678 | string configPath = McpConfigurationHelper.GetClientConfigPath(client); 679 | if (System.IO.File.Exists(configPath)) 680 | { 681 | client.configStatus = "Configured"; 682 | client.status = McpStatus.Configured; 683 | } 684 | else 685 | { 686 | client.configStatus = "Not Configured"; 687 | client.status = McpStatus.NotConfigured; 688 | } 689 | } 690 | catch 691 | { 692 | client.configStatus = "Error"; 693 | client.status = McpStatus.Error; 694 | } 695 | } 696 | 697 | private void OpenInstallationUrls() 698 | { 699 | var (pythonUrl, uvUrl) = DependencyManager.GetInstallationUrls(); 700 | 701 | bool openPython = EditorUtility.DisplayDialog( 702 | "Open Installation URLs", 703 | "Open Python installation page?", 704 | "Yes", 705 | "No" 706 | ); 707 | 708 | if (openPython) 709 | { 710 | Application.OpenURL(pythonUrl); 711 | } 712 | 713 | bool openUV = EditorUtility.DisplayDialog( 714 | "Open Installation URLs", 715 | "Open UV installation page?", 716 | "Yes", 717 | "No" 718 | ); 719 | 720 | if (openUV) 721 | { 722 | Application.OpenURL(uvUrl); 723 | } 724 | } 725 | } 726 | } 727 | ``` -------------------------------------------------------------------------------- /TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectTests.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections; 4 | using NUnit.Framework; 5 | using UnityEngine; 6 | using UnityEditor; 7 | using UnityEngine.TestTools; 8 | using Newtonsoft.Json.Linq; 9 | using MCPForUnity.Editor.Tools; 10 | 11 | namespace MCPForUnityTests.Editor.Tools 12 | { 13 | public class ManageGameObjectTests 14 | { 15 | private GameObject testGameObject; 16 | 17 | [SetUp] 18 | public void SetUp() 19 | { 20 | // Create a test GameObject for each test 21 | testGameObject = new GameObject("TestObject"); 22 | } 23 | 24 | [TearDown] 25 | public void TearDown() 26 | { 27 | // Clean up test GameObject 28 | if (testGameObject != null) 29 | { 30 | UnityEngine.Object.DestroyImmediate(testGameObject); 31 | } 32 | } 33 | 34 | [Test] 35 | public void HandleCommand_ReturnsError_ForNullParams() 36 | { 37 | var result = ManageGameObject.HandleCommand(null); 38 | 39 | Assert.IsNotNull(result, "Should return a result object"); 40 | // Note: Actual error checking would need access to Response structure 41 | } 42 | 43 | [Test] 44 | public void HandleCommand_ReturnsError_ForEmptyParams() 45 | { 46 | var emptyParams = new JObject(); 47 | var result = ManageGameObject.HandleCommand(emptyParams); 48 | 49 | Assert.IsNotNull(result, "Should return a result object for empty params"); 50 | } 51 | 52 | [Test] 53 | public void HandleCommand_ProcessesValidCreateAction() 54 | { 55 | var createParams = new JObject 56 | { 57 | ["action"] = "create", 58 | ["name"] = "TestCreateObject" 59 | }; 60 | 61 | var result = ManageGameObject.HandleCommand(createParams); 62 | 63 | Assert.IsNotNull(result, "Should return a result for valid create action"); 64 | 65 | // Clean up - find and destroy the created object 66 | var createdObject = GameObject.Find("TestCreateObject"); 67 | if (createdObject != null) 68 | { 69 | UnityEngine.Object.DestroyImmediate(createdObject); 70 | } 71 | } 72 | 73 | [Test] 74 | public void ComponentResolver_Integration_WorksWithRealComponents() 75 | { 76 | // Test that our ComponentResolver works with actual Unity components 77 | var transformResult = ComponentResolver.TryResolve("Transform", out Type transformType, out string error); 78 | 79 | Assert.IsTrue(transformResult, "Should resolve Transform component"); 80 | Assert.AreEqual(typeof(Transform), transformType, "Should return correct Transform type"); 81 | Assert.IsEmpty(error, "Should have no error for valid component"); 82 | } 83 | 84 | [Test] 85 | public void ComponentResolver_Integration_WorksWithBuiltInComponents() 86 | { 87 | var components = new[] 88 | { 89 | ("Rigidbody", typeof(Rigidbody)), 90 | ("Collider", typeof(Collider)), 91 | ("Renderer", typeof(Renderer)), 92 | ("Camera", typeof(Camera)), 93 | ("Light", typeof(Light)) 94 | }; 95 | 96 | foreach (var (componentName, expectedType) in components) 97 | { 98 | var result = ComponentResolver.TryResolve(componentName, out Type actualType, out string error); 99 | 100 | // Some components might not resolve (abstract classes), but the method should handle gracefully 101 | if (result) 102 | { 103 | Assert.IsTrue(expectedType.IsAssignableFrom(actualType), 104 | $"{componentName} should resolve to assignable type"); 105 | } 106 | else 107 | { 108 | Assert.IsNotEmpty(error, $"Should have error message for {componentName}"); 109 | } 110 | } 111 | } 112 | 113 | [Test] 114 | public void PropertyMatching_Integration_WorksWithRealGameObject() 115 | { 116 | // Add a Rigidbody to test real property matching 117 | var rigidbody = testGameObject.AddComponent<Rigidbody>(); 118 | 119 | var properties = ComponentResolver.GetAllComponentProperties(typeof(Rigidbody)); 120 | 121 | Assert.IsNotEmpty(properties, "Rigidbody should have properties"); 122 | Assert.Contains("mass", properties, "Rigidbody should have mass property"); 123 | Assert.Contains("useGravity", properties, "Rigidbody should have useGravity property"); 124 | 125 | // Test AI suggestions 126 | var suggestions = ComponentResolver.GetAIPropertySuggestions("Use Gravity", properties); 127 | Assert.Contains("useGravity", suggestions, "Should suggest useGravity for 'Use Gravity'"); 128 | } 129 | 130 | [Test] 131 | public void PropertyMatching_HandlesMonoBehaviourProperties() 132 | { 133 | var properties = ComponentResolver.GetAllComponentProperties(typeof(MonoBehaviour)); 134 | 135 | Assert.IsNotEmpty(properties, "MonoBehaviour should have properties"); 136 | Assert.Contains("enabled", properties, "MonoBehaviour should have enabled property"); 137 | Assert.Contains("name", properties, "MonoBehaviour should have name property"); 138 | Assert.Contains("tag", properties, "MonoBehaviour should have tag property"); 139 | } 140 | 141 | [Test] 142 | public void PropertyMatching_HandlesCaseVariations() 143 | { 144 | var testProperties = new List<string> { "maxReachDistance", "playerHealth", "movementSpeed" }; 145 | 146 | var testCases = new[] 147 | { 148 | ("max reach distance", "maxReachDistance"), 149 | ("Max Reach Distance", "maxReachDistance"), 150 | ("MAX_REACH_DISTANCE", "maxReachDistance"), 151 | ("player health", "playerHealth"), 152 | ("movement speed", "movementSpeed") 153 | }; 154 | 155 | foreach (var (input, expected) in testCases) 156 | { 157 | var suggestions = ComponentResolver.GetAIPropertySuggestions(input, testProperties); 158 | Assert.Contains(expected, suggestions, $"Should suggest {expected} for input '{input}'"); 159 | } 160 | } 161 | 162 | [Test] 163 | public void ErrorHandling_ReturnsHelpfulMessages() 164 | { 165 | // This test verifies that error messages are helpful and contain suggestions 166 | var testProperties = new List<string> { "mass", "velocity", "drag", "useGravity" }; 167 | var suggestions = ComponentResolver.GetAIPropertySuggestions("weight", testProperties); 168 | 169 | // Even if no perfect match, should return valid list 170 | Assert.IsNotNull(suggestions, "Should return valid suggestions list"); 171 | 172 | // Test with completely invalid input 173 | var badSuggestions = ComponentResolver.GetAIPropertySuggestions("xyz123invalid", testProperties); 174 | Assert.IsNotNull(badSuggestions, "Should handle invalid input gracefully"); 175 | } 176 | 177 | [Test] 178 | public void PerformanceTest_CachingWorks() 179 | { 180 | var properties = ComponentResolver.GetAllComponentProperties(typeof(Transform)); 181 | var input = "Test Property Name"; 182 | 183 | // First call - populate cache 184 | var startTime = System.DateTime.UtcNow; 185 | var suggestions1 = ComponentResolver.GetAIPropertySuggestions(input, properties); 186 | var firstCallTime = (System.DateTime.UtcNow - startTime).TotalMilliseconds; 187 | 188 | // Second call - should use cache 189 | startTime = System.DateTime.UtcNow; 190 | var suggestions2 = ComponentResolver.GetAIPropertySuggestions(input, properties); 191 | var secondCallTime = (System.DateTime.UtcNow - startTime).TotalMilliseconds; 192 | 193 | Assert.AreEqual(suggestions1.Count, suggestions2.Count, "Cached results should be identical"); 194 | CollectionAssert.AreEqual(suggestions1, suggestions2, "Cached results should match exactly"); 195 | 196 | // Second call should be faster (though this test might be flaky) 197 | Assert.LessOrEqual(secondCallTime, firstCallTime * 2, "Cached call should not be significantly slower"); 198 | } 199 | 200 | [Test] 201 | public void SetComponentProperties_CollectsAllFailuresAndAppliesValidOnes() 202 | { 203 | // Arrange - add Transform and Rigidbody components to test with 204 | var transform = testGameObject.transform; 205 | var rigidbody = testGameObject.AddComponent<Rigidbody>(); 206 | 207 | // Create a params object with mixed valid and invalid properties 208 | var setPropertiesParams = new JObject 209 | { 210 | ["action"] = "modify", 211 | ["target"] = testGameObject.name, 212 | ["search_method"] = "by_name", 213 | ["componentProperties"] = new JObject 214 | { 215 | ["Transform"] = new JObject 216 | { 217 | ["localPosition"] = new JObject { ["x"] = 1.0f, ["y"] = 2.0f, ["z"] = 3.0f }, // Valid 218 | ["rotatoin"] = new JObject { ["x"] = 0.0f, ["y"] = 90.0f, ["z"] = 0.0f }, // Invalid (typo - should be rotation) 219 | ["localScale"] = new JObject { ["x"] = 2.0f, ["y"] = 2.0f, ["z"] = 2.0f } // Valid 220 | }, 221 | ["Rigidbody"] = new JObject 222 | { 223 | ["mass"] = 5.0f, // Valid 224 | ["invalidProp"] = "test", // Invalid - doesn't exist 225 | ["useGravity"] = true // Valid 226 | } 227 | } 228 | }; 229 | 230 | // Store original values to verify changes 231 | var originalLocalPosition = transform.localPosition; 232 | var originalLocalScale = transform.localScale; 233 | var originalMass = rigidbody.mass; 234 | var originalUseGravity = rigidbody.useGravity; 235 | 236 | Debug.Log($"BEFORE TEST - Mass: {rigidbody.mass}, UseGravity: {rigidbody.useGravity}"); 237 | 238 | // Expect the warning logs from the invalid properties 239 | LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex("Property 'rotatoin' not found")); 240 | LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex("Property 'invalidProp' not found")); 241 | 242 | // Act 243 | var result = ManageGameObject.HandleCommand(setPropertiesParams); 244 | 245 | Debug.Log($"AFTER TEST - Mass: {rigidbody.mass}, UseGravity: {rigidbody.useGravity}"); 246 | Debug.Log($"AFTER TEST - LocalPosition: {transform.localPosition}"); 247 | Debug.Log($"AFTER TEST - LocalScale: {transform.localScale}"); 248 | 249 | // Assert - verify that valid properties were set despite invalid ones 250 | Assert.AreEqual(new Vector3(1.0f, 2.0f, 3.0f), transform.localPosition, 251 | "Valid localPosition should be set even with other invalid properties"); 252 | Assert.AreEqual(new Vector3(2.0f, 2.0f, 2.0f), transform.localScale, 253 | "Valid localScale should be set even with other invalid properties"); 254 | Assert.AreEqual(5.0f, rigidbody.mass, 0.001f, 255 | "Valid mass should be set even with other invalid properties"); 256 | Assert.AreEqual(true, rigidbody.useGravity, 257 | "Valid useGravity should be set even with other invalid properties"); 258 | 259 | // Verify the result indicates errors (since we had invalid properties) 260 | Assert.IsNotNull(result, "Should return a result object"); 261 | 262 | // The collect-and-continue behavior means we should get an error response 263 | // that contains info about the failed properties, but valid ones were still applied 264 | // This proves the collect-and-continue behavior is working 265 | 266 | // Harden: verify structured error response with failures list contains both invalid fields 267 | var successProp = result.GetType().GetProperty("success"); 268 | Assert.IsNotNull(successProp, "Result should expose 'success' property"); 269 | Assert.IsFalse((bool)successProp.GetValue(result), "Result.success should be false for partial failure"); 270 | 271 | var dataProp = result.GetType().GetProperty("data"); 272 | Assert.IsNotNull(dataProp, "Result should include 'data' with errors"); 273 | var dataVal = dataProp.GetValue(result); 274 | Assert.IsNotNull(dataVal, "Result.data should not be null"); 275 | var errorsProp = dataVal.GetType().GetProperty("errors"); 276 | Assert.IsNotNull(errorsProp, "Result.data should include 'errors' list"); 277 | var errorsEnum = errorsProp.GetValue(dataVal) as System.Collections.IEnumerable; 278 | Assert.IsNotNull(errorsEnum, "errors should be enumerable"); 279 | 280 | bool foundRotatoin = false; 281 | bool foundInvalidProp = false; 282 | foreach (var err in errorsEnum) 283 | { 284 | string s = err?.ToString() ?? string.Empty; 285 | if (s.Contains("rotatoin")) foundRotatoin = true; 286 | if (s.Contains("invalidProp")) foundInvalidProp = true; 287 | } 288 | Assert.IsTrue(foundRotatoin, "errors should mention the misspelled 'rotatoin' property"); 289 | Assert.IsTrue(foundInvalidProp, "errors should mention the 'invalidProp' property"); 290 | } 291 | 292 | [Test] 293 | public void SetComponentProperties_ContinuesAfterException() 294 | { 295 | // Arrange - create scenario that might cause exceptions 296 | var rigidbody = testGameObject.AddComponent<Rigidbody>(); 297 | 298 | // Set initial values that we'll change 299 | rigidbody.mass = 1.0f; 300 | rigidbody.useGravity = true; 301 | 302 | var setPropertiesParams = new JObject 303 | { 304 | ["action"] = "modify", 305 | ["target"] = testGameObject.name, 306 | ["search_method"] = "by_name", 307 | ["componentProperties"] = new JObject 308 | { 309 | ["Rigidbody"] = new JObject 310 | { 311 | ["mass"] = 2.5f, // Valid - should be set 312 | ["velocity"] = "invalid_type", // Invalid type - will cause exception 313 | ["useGravity"] = false // Valid - should still be set after exception 314 | } 315 | } 316 | }; 317 | 318 | // Expect the error logs from the invalid property 319 | LogAssert.Expect(LogType.Error, new System.Text.RegularExpressions.Regex("Unexpected error converting token to UnityEngine.Vector3")); 320 | LogAssert.Expect(LogType.Error, new System.Text.RegularExpressions.Regex("SetProperty.*Failed to set 'velocity'")); 321 | LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex("Property 'velocity' not found")); 322 | 323 | // Act 324 | var result = ManageGameObject.HandleCommand(setPropertiesParams); 325 | 326 | // Assert - verify that valid properties before AND after the exception were still set 327 | Assert.AreEqual(2.5f, rigidbody.mass, 0.001f, 328 | "Mass should be set even if later property causes exception"); 329 | Assert.AreEqual(false, rigidbody.useGravity, 330 | "UseGravity should be set even if previous property caused exception"); 331 | 332 | Assert.IsNotNull(result, "Should return a result even with exceptions"); 333 | 334 | // The key test: processing continued after the exception and set useGravity 335 | // This proves the collect-and-continue behavior works even with exceptions 336 | 337 | // Harden: verify structured error response contains velocity failure 338 | var successProp2 = result.GetType().GetProperty("success"); 339 | Assert.IsNotNull(successProp2, "Result should expose 'success' property"); 340 | Assert.IsFalse((bool)successProp2.GetValue(result), "Result.success should be false when an exception occurs for a property"); 341 | 342 | var dataProp2 = result.GetType().GetProperty("data"); 343 | Assert.IsNotNull(dataProp2, "Result should include 'data' with errors"); 344 | var dataVal2 = dataProp2.GetValue(result); 345 | Assert.IsNotNull(dataVal2, "Result.data should not be null"); 346 | var errorsProp2 = dataVal2.GetType().GetProperty("errors"); 347 | Assert.IsNotNull(errorsProp2, "Result.data should include 'errors' list"); 348 | var errorsEnum2 = errorsProp2.GetValue(dataVal2) as System.Collections.IEnumerable; 349 | Assert.IsNotNull(errorsEnum2, "errors should be enumerable"); 350 | 351 | bool foundVelocityError = false; 352 | foreach (var err in errorsEnum2) 353 | { 354 | string s = err?.ToString() ?? string.Empty; 355 | if (s.Contains("velocity")) { foundVelocityError = true; break; } 356 | } 357 | Assert.IsTrue(foundVelocityError, "errors should include a message referencing 'velocity'"); 358 | } 359 | 360 | [Test] 361 | public void GetComponentData_DoesNotInstantiateMaterialsInEditMode() 362 | { 363 | // Arrange - Create a GameObject with MeshRenderer and MeshFilter components 364 | var testObject = new GameObject("MaterialMeshTestObject"); 365 | var meshRenderer = testObject.AddComponent<MeshRenderer>(); 366 | var meshFilter = testObject.AddComponent<MeshFilter>(); 367 | 368 | // Create a simple material and mesh for testing 369 | var testMaterial = new Material(Shader.Find("Standard")); 370 | var tempCube = GameObject.CreatePrimitive(PrimitiveType.Cube); 371 | var testMesh = tempCube.GetComponent<MeshFilter>().sharedMesh; 372 | UnityEngine.Object.DestroyImmediate(tempCube); 373 | 374 | // Set the shared material and mesh (these should be used in edit mode) 375 | meshRenderer.sharedMaterial = testMaterial; 376 | meshFilter.sharedMesh = testMesh; 377 | 378 | // Act - Get component data which should trigger material/mesh property access 379 | var prevIgnore = LogAssert.ignoreFailingMessages; 380 | LogAssert.ignoreFailingMessages = true; // Avoid failing due to incidental editor logs during reflection 381 | object result; 382 | try 383 | { 384 | result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshRenderer); 385 | } 386 | finally 387 | { 388 | LogAssert.ignoreFailingMessages = prevIgnore; 389 | } 390 | 391 | // Assert - Basic success and shape tolerance 392 | Assert.IsNotNull(result, "GetComponentData should return a result"); 393 | if (result is Dictionary<string, object> dict && 394 | dict.TryGetValue("properties", out var propsObj) && 395 | propsObj is Dictionary<string, object> properties) 396 | { 397 | Assert.IsTrue(properties.ContainsKey("material") || properties.ContainsKey("sharedMaterial") || properties.ContainsKey("materials") || properties.ContainsKey("sharedMaterials"), 398 | "Serialized data should include a material-related key when present."); 399 | } 400 | 401 | // Clean up 402 | UnityEngine.Object.DestroyImmediate(testMaterial); 403 | UnityEngine.Object.DestroyImmediate(testObject); 404 | } 405 | 406 | [Test] 407 | public void GetComponentData_DoesNotInstantiateMeshesInEditMode() 408 | { 409 | // Arrange - Create a GameObject with MeshFilter component 410 | var testObject = new GameObject("MeshTestObject"); 411 | var meshFilter = testObject.AddComponent<MeshFilter>(); 412 | 413 | // Create a simple mesh for testing 414 | var tempSphere = GameObject.CreatePrimitive(PrimitiveType.Sphere); 415 | var testMesh = tempSphere.GetComponent<MeshFilter>().sharedMesh; 416 | UnityEngine.Object.DestroyImmediate(tempSphere); 417 | meshFilter.sharedMesh = testMesh; 418 | 419 | // Act - Get component data which should trigger mesh property access 420 | var prevIgnore2 = LogAssert.ignoreFailingMessages; 421 | LogAssert.ignoreFailingMessages = true; 422 | object result; 423 | try 424 | { 425 | result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshFilter); 426 | } 427 | finally 428 | { 429 | LogAssert.ignoreFailingMessages = prevIgnore2; 430 | } 431 | 432 | // Assert - Basic success and shape tolerance 433 | Assert.IsNotNull(result, "GetComponentData should return a result"); 434 | if (result is Dictionary<string, object> dict2 && 435 | dict2.TryGetValue("properties", out var propsObj2) && 436 | propsObj2 is Dictionary<string, object> properties2) 437 | { 438 | Assert.IsTrue(properties2.ContainsKey("mesh") || properties2.ContainsKey("sharedMesh"), 439 | "Serialized data should include a mesh-related key when present."); 440 | } 441 | 442 | // Clean up 443 | UnityEngine.Object.DestroyImmediate(testObject); 444 | } 445 | 446 | [Test] 447 | public void GetComponentData_UsesSharedMaterialInEditMode() 448 | { 449 | // Arrange - Create a GameObject with MeshRenderer 450 | var testObject = new GameObject("SharedMaterialTestObject"); 451 | var meshRenderer = testObject.AddComponent<MeshRenderer>(); 452 | 453 | // Create a test material 454 | var testMaterial = new Material(Shader.Find("Standard")); 455 | testMaterial.name = "TestMaterial"; 456 | meshRenderer.sharedMaterial = testMaterial; 457 | 458 | // Act - Get component data in edit mode 459 | var result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshRenderer); 460 | 461 | // Assert - Verify that the material property was accessed without instantiation 462 | Assert.IsNotNull(result, "GetComponentData should return a result"); 463 | 464 | // Check that result is a dictionary with properties key 465 | if (result is Dictionary<string, object> resultDict && 466 | resultDict.TryGetValue("properties", out var propertiesObj) && 467 | propertiesObj is Dictionary<string, object> properties) 468 | { 469 | Assert.IsTrue(properties.ContainsKey("material") || properties.ContainsKey("sharedMaterial"), 470 | "Serialized data should include 'material' or 'sharedMaterial' when present."); 471 | } 472 | 473 | // Clean up 474 | UnityEngine.Object.DestroyImmediate(testMaterial); 475 | UnityEngine.Object.DestroyImmediate(testObject); 476 | } 477 | 478 | [Test] 479 | public void GetComponentData_UsesSharedMeshInEditMode() 480 | { 481 | // Arrange - Create a GameObject with MeshFilter 482 | var testObject = new GameObject("SharedMeshTestObject"); 483 | var meshFilter = testObject.AddComponent<MeshFilter>(); 484 | 485 | // Create a test mesh 486 | var tempCylinder = GameObject.CreatePrimitive(PrimitiveType.Cylinder); 487 | var testMesh = tempCylinder.GetComponent<MeshFilter>().sharedMesh; 488 | UnityEngine.Object.DestroyImmediate(tempCylinder); 489 | testMesh.name = "TestMesh"; 490 | meshFilter.sharedMesh = testMesh; 491 | 492 | // Act - Get component data in edit mode 493 | var result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshFilter); 494 | 495 | // Assert - Verify that the mesh property was accessed without instantiation 496 | Assert.IsNotNull(result, "GetComponentData should return a result"); 497 | 498 | // Check that result is a dictionary with properties key 499 | if (result is Dictionary<string, object> resultDict && 500 | resultDict.TryGetValue("properties", out var propertiesObj) && 501 | propertiesObj is Dictionary<string, object> properties) 502 | { 503 | Assert.IsTrue(properties.ContainsKey("mesh") || properties.ContainsKey("sharedMesh"), 504 | "Serialized data should include 'mesh' or 'sharedMesh' when present."); 505 | } 506 | 507 | // Clean up 508 | UnityEngine.Object.DestroyImmediate(testObject); 509 | } 510 | 511 | [Test] 512 | public void GetComponentData_HandlesNullMaterialsAndMeshes() 513 | { 514 | // Arrange - Create a GameObject with MeshRenderer and MeshFilter but no materials/meshes 515 | var testObject = new GameObject("NullMaterialMeshTestObject"); 516 | var meshRenderer = testObject.AddComponent<MeshRenderer>(); 517 | var meshFilter = testObject.AddComponent<MeshFilter>(); 518 | 519 | // Don't set any materials or meshes - they should be null 520 | 521 | // Act - Get component data 522 | var rendererResult = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshRenderer); 523 | var meshFilterResult = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshFilter); 524 | 525 | // Assert - Verify that the operations succeeded even with null materials/meshes 526 | Assert.IsNotNull(rendererResult, "GetComponentData should handle null materials"); 527 | Assert.IsNotNull(meshFilterResult, "GetComponentData should handle null meshes"); 528 | 529 | // Clean up 530 | UnityEngine.Object.DestroyImmediate(testObject); 531 | } 532 | 533 | [Test] 534 | public void GetComponentData_WorksWithMultipleMaterials() 535 | { 536 | // Arrange - Create a GameObject with MeshRenderer that has multiple materials 537 | var testObject = new GameObject("MultiMaterialTestObject"); 538 | var meshRenderer = testObject.AddComponent<MeshRenderer>(); 539 | 540 | // Create multiple test materials 541 | var material1 = new Material(Shader.Find("Standard")); 542 | material1.name = "TestMaterial1"; 543 | var material2 = new Material(Shader.Find("Standard")); 544 | material2.name = "TestMaterial2"; 545 | 546 | meshRenderer.sharedMaterials = new Material[] { material1, material2 }; 547 | 548 | // Act - Get component data 549 | var result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshRenderer); 550 | 551 | // Assert - Verify that the operation succeeded with multiple materials 552 | Assert.IsNotNull(result, "GetComponentData should handle multiple materials"); 553 | 554 | // Clean up 555 | UnityEngine.Object.DestroyImmediate(material1); 556 | UnityEngine.Object.DestroyImmediate(material2); 557 | UnityEngine.Object.DestroyImmediate(testObject); 558 | } 559 | } 560 | } 561 | ``` -------------------------------------------------------------------------------- /MCPForUnity/Editor/Helpers/GameObjectSerializer.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Linq; 7 | using UnityEditor; 8 | using UnityEngine; 9 | using MCPForUnity.Runtime.Serialization; // For Converters 10 | 11 | namespace MCPForUnity.Editor.Helpers 12 | { 13 | /// <summary> 14 | /// Handles serialization of GameObjects and Components for MCP responses. 15 | /// Includes reflection helpers and caching for performance. 16 | /// </summary> 17 | public static class GameObjectSerializer 18 | { 19 | // --- Data Serialization --- 20 | 21 | /// <summary> 22 | /// Creates a serializable representation of a GameObject. 23 | /// </summary> 24 | public static object GetGameObjectData(GameObject go) 25 | { 26 | if (go == null) 27 | return null; 28 | return new 29 | { 30 | name = go.name, 31 | instanceID = go.GetInstanceID(), 32 | tag = go.tag, 33 | layer = go.layer, 34 | activeSelf = go.activeSelf, 35 | activeInHierarchy = go.activeInHierarchy, 36 | isStatic = go.isStatic, 37 | scenePath = go.scene.path, // Identify which scene it belongs to 38 | transform = new // Serialize transform components carefully to avoid JSON issues 39 | { 40 | // Serialize Vector3 components individually to prevent self-referencing loops. 41 | // The default serializer can struggle with properties like Vector3.normalized. 42 | position = new 43 | { 44 | x = go.transform.position.x, 45 | y = go.transform.position.y, 46 | z = go.transform.position.z, 47 | }, 48 | localPosition = new 49 | { 50 | x = go.transform.localPosition.x, 51 | y = go.transform.localPosition.y, 52 | z = go.transform.localPosition.z, 53 | }, 54 | rotation = new 55 | { 56 | x = go.transform.rotation.eulerAngles.x, 57 | y = go.transform.rotation.eulerAngles.y, 58 | z = go.transform.rotation.eulerAngles.z, 59 | }, 60 | localRotation = new 61 | { 62 | x = go.transform.localRotation.eulerAngles.x, 63 | y = go.transform.localRotation.eulerAngles.y, 64 | z = go.transform.localRotation.eulerAngles.z, 65 | }, 66 | scale = new 67 | { 68 | x = go.transform.localScale.x, 69 | y = go.transform.localScale.y, 70 | z = go.transform.localScale.z, 71 | }, 72 | forward = new 73 | { 74 | x = go.transform.forward.x, 75 | y = go.transform.forward.y, 76 | z = go.transform.forward.z, 77 | }, 78 | up = new 79 | { 80 | x = go.transform.up.x, 81 | y = go.transform.up.y, 82 | z = go.transform.up.z, 83 | }, 84 | right = new 85 | { 86 | x = go.transform.right.x, 87 | y = go.transform.right.y, 88 | z = go.transform.right.z, 89 | }, 90 | }, 91 | parentInstanceID = go.transform.parent?.gameObject.GetInstanceID() ?? 0, // 0 if no parent 92 | // Optionally include components, but can be large 93 | // components = go.GetComponents<Component>().Select(c => GetComponentData(c)).ToList() 94 | // Or just component names: 95 | componentNames = go.GetComponents<Component>() 96 | .Select(c => c.GetType().FullName) 97 | .ToList(), 98 | }; 99 | } 100 | 101 | // --- Metadata Caching for Reflection --- 102 | private class CachedMetadata 103 | { 104 | public readonly List<PropertyInfo> SerializableProperties; 105 | public readonly List<FieldInfo> SerializableFields; 106 | 107 | public CachedMetadata(List<PropertyInfo> properties, List<FieldInfo> fields) 108 | { 109 | SerializableProperties = properties; 110 | SerializableFields = fields; 111 | } 112 | } 113 | // Key becomes Tuple<Type, bool> 114 | private static readonly Dictionary<Tuple<Type, bool>, CachedMetadata> _metadataCache = new Dictionary<Tuple<Type, bool>, CachedMetadata>(); 115 | // --- End Metadata Caching --- 116 | 117 | /// <summary> 118 | /// Creates a serializable representation of a Component, attempting to serialize 119 | /// public properties and fields using reflection, with caching and control over non-public fields. 120 | /// </summary> 121 | // Add the flag parameter here 122 | public static object GetComponentData(Component c, bool includeNonPublicSerializedFields = true) 123 | { 124 | // --- Add Early Logging --- 125 | // Debug.Log($"[GetComponentData] Starting for component: {c?.GetType()?.FullName ?? "null"} (ID: {c?.GetInstanceID() ?? 0})"); 126 | // --- End Early Logging --- 127 | 128 | if (c == null) return null; 129 | Type componentType = c.GetType(); 130 | 131 | // --- Special handling for Transform to avoid reflection crashes and problematic properties --- 132 | if (componentType == typeof(Transform)) 133 | { 134 | Transform tr = c as Transform; 135 | // Debug.Log($"[GetComponentData] Manually serializing Transform (ID: {tr.GetInstanceID()})"); 136 | return new Dictionary<string, object> 137 | { 138 | { "typeName", componentType.FullName }, 139 | { "instanceID", tr.GetInstanceID() }, 140 | // Manually extract known-safe properties. Avoid Quaternion 'rotation' and 'lossyScale'. 141 | { "position", CreateTokenFromValue(tr.position, typeof(Vector3))?.ToObject<object>() ?? new JObject() }, 142 | { "localPosition", CreateTokenFromValue(tr.localPosition, typeof(Vector3))?.ToObject<object>() ?? new JObject() }, 143 | { "eulerAngles", CreateTokenFromValue(tr.eulerAngles, typeof(Vector3))?.ToObject<object>() ?? new JObject() }, // Use Euler angles 144 | { "localEulerAngles", CreateTokenFromValue(tr.localEulerAngles, typeof(Vector3))?.ToObject<object>() ?? new JObject() }, 145 | { "localScale", CreateTokenFromValue(tr.localScale, typeof(Vector3))?.ToObject<object>() ?? new JObject() }, 146 | { "right", CreateTokenFromValue(tr.right, typeof(Vector3))?.ToObject<object>() ?? new JObject() }, 147 | { "up", CreateTokenFromValue(tr.up, typeof(Vector3))?.ToObject<object>() ?? new JObject() }, 148 | { "forward", CreateTokenFromValue(tr.forward, typeof(Vector3))?.ToObject<object>() ?? new JObject() }, 149 | { "parentInstanceID", tr.parent?.gameObject.GetInstanceID() ?? 0 }, 150 | { "rootInstanceID", tr.root?.gameObject.GetInstanceID() ?? 0 }, 151 | { "childCount", tr.childCount }, 152 | // Include standard Object/Component properties 153 | { "name", tr.name }, 154 | { "tag", tr.tag }, 155 | { "gameObjectInstanceID", tr.gameObject?.GetInstanceID() ?? 0 } 156 | }; 157 | } 158 | // --- End Special handling for Transform --- 159 | 160 | // --- Special handling for Camera to avoid matrix-related crashes --- 161 | if (componentType == typeof(Camera)) 162 | { 163 | Camera cam = c as Camera; 164 | var cameraProperties = new Dictionary<string, object>(); 165 | 166 | // List of safe properties to serialize 167 | var safeProperties = new Dictionary<string, Func<object>> 168 | { 169 | { "nearClipPlane", () => cam.nearClipPlane }, 170 | { "farClipPlane", () => cam.farClipPlane }, 171 | { "fieldOfView", () => cam.fieldOfView }, 172 | { "renderingPath", () => (int)cam.renderingPath }, 173 | { "actualRenderingPath", () => (int)cam.actualRenderingPath }, 174 | { "allowHDR", () => cam.allowHDR }, 175 | { "allowMSAA", () => cam.allowMSAA }, 176 | { "allowDynamicResolution", () => cam.allowDynamicResolution }, 177 | { "forceIntoRenderTexture", () => cam.forceIntoRenderTexture }, 178 | { "orthographicSize", () => cam.orthographicSize }, 179 | { "orthographic", () => cam.orthographic }, 180 | { "opaqueSortMode", () => (int)cam.opaqueSortMode }, 181 | { "transparencySortMode", () => (int)cam.transparencySortMode }, 182 | { "depth", () => cam.depth }, 183 | { "aspect", () => cam.aspect }, 184 | { "cullingMask", () => cam.cullingMask }, 185 | { "eventMask", () => cam.eventMask }, 186 | { "backgroundColor", () => cam.backgroundColor }, 187 | { "clearFlags", () => (int)cam.clearFlags }, 188 | { "stereoEnabled", () => cam.stereoEnabled }, 189 | { "stereoSeparation", () => cam.stereoSeparation }, 190 | { "stereoConvergence", () => cam.stereoConvergence }, 191 | { "enabled", () => cam.enabled }, 192 | { "name", () => cam.name }, 193 | { "tag", () => cam.tag }, 194 | { "gameObject", () => new { name = cam.gameObject.name, instanceID = cam.gameObject.GetInstanceID() } } 195 | }; 196 | 197 | foreach (var prop in safeProperties) 198 | { 199 | try 200 | { 201 | var value = prop.Value(); 202 | if (value != null) 203 | { 204 | AddSerializableValue(cameraProperties, prop.Key, value.GetType(), value); 205 | } 206 | } 207 | catch (Exception) 208 | { 209 | // Silently skip any property that fails 210 | continue; 211 | } 212 | } 213 | 214 | return new Dictionary<string, object> 215 | { 216 | { "typeName", componentType.FullName }, 217 | { "instanceID", cam.GetInstanceID() }, 218 | { "properties", cameraProperties } 219 | }; 220 | } 221 | // --- End Special handling for Camera --- 222 | 223 | var data = new Dictionary<string, object> 224 | { 225 | { "typeName", componentType.FullName }, 226 | { "instanceID", c.GetInstanceID() } 227 | }; 228 | 229 | // --- Get Cached or Generate Metadata (using new cache key) --- 230 | Tuple<Type, bool> cacheKey = new Tuple<Type, bool>(componentType, includeNonPublicSerializedFields); 231 | if (!_metadataCache.TryGetValue(cacheKey, out CachedMetadata cachedData)) 232 | { 233 | var propertiesToCache = new List<PropertyInfo>(); 234 | var fieldsToCache = new List<FieldInfo>(); 235 | 236 | // Traverse the hierarchy from the component type up to MonoBehaviour 237 | Type currentType = componentType; 238 | while (currentType != null && currentType != typeof(MonoBehaviour) && currentType != typeof(object)) 239 | { 240 | // Get properties declared only at the current type level 241 | BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; 242 | foreach (var propInfo in currentType.GetProperties(propFlags)) 243 | { 244 | // Basic filtering (readable, not indexer, not transform which is handled elsewhere) 245 | if (!propInfo.CanRead || propInfo.GetIndexParameters().Length > 0 || propInfo.Name == "transform") continue; 246 | // Add if not already added (handles overrides - keep the most derived version) 247 | if (!propertiesToCache.Any(p => p.Name == propInfo.Name)) 248 | { 249 | propertiesToCache.Add(propInfo); 250 | } 251 | } 252 | 253 | // Get fields declared only at the current type level (both public and non-public) 254 | BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly; 255 | var declaredFields = currentType.GetFields(fieldFlags); 256 | 257 | // Process the declared Fields for caching 258 | foreach (var fieldInfo in declaredFields) 259 | { 260 | if (fieldInfo.Name.EndsWith("k__BackingField")) continue; // Skip backing fields 261 | 262 | // Add if not already added (handles hiding - keep the most derived version) 263 | if (fieldsToCache.Any(f => f.Name == fieldInfo.Name)) continue; 264 | 265 | bool shouldInclude = false; 266 | if (includeNonPublicSerializedFields) 267 | { 268 | // If TRUE, include Public OR any NonPublic with [SerializeField] (private/protected/internal) 269 | var hasSerializeField = fieldInfo.IsDefined(typeof(SerializeField), inherit: true); 270 | shouldInclude = fieldInfo.IsPublic || (!fieldInfo.IsPublic && hasSerializeField); 271 | } 272 | else // includeNonPublicSerializedFields is FALSE 273 | { 274 | // If FALSE, include ONLY if it is explicitly Public. 275 | shouldInclude = fieldInfo.IsPublic; 276 | } 277 | 278 | if (shouldInclude) 279 | { 280 | fieldsToCache.Add(fieldInfo); 281 | } 282 | } 283 | 284 | // Move to the base type 285 | currentType = currentType.BaseType; 286 | } 287 | // --- End Hierarchy Traversal --- 288 | 289 | cachedData = new CachedMetadata(propertiesToCache, fieldsToCache); 290 | _metadataCache[cacheKey] = cachedData; // Add to cache with combined key 291 | } 292 | // --- End Get Cached or Generate Metadata --- 293 | 294 | // --- Use cached metadata --- 295 | var serializablePropertiesOutput = new Dictionary<string, object>(); 296 | 297 | // --- Add Logging Before Property Loop --- 298 | // Debug.Log($"[GetComponentData] Starting property loop for {componentType.Name}..."); 299 | // --- End Logging Before Property Loop --- 300 | 301 | // Use cached properties 302 | foreach (var propInfo in cachedData.SerializableProperties) 303 | { 304 | string propName = propInfo.Name; 305 | 306 | // --- Skip known obsolete/problematic Component shortcut properties --- 307 | bool skipProperty = false; 308 | if (propName == "rigidbody" || propName == "rigidbody2D" || propName == "camera" || 309 | propName == "light" || propName == "animation" || propName == "constantForce" || 310 | propName == "renderer" || propName == "audio" || propName == "networkView" || 311 | propName == "collider" || propName == "collider2D" || propName == "hingeJoint" || 312 | propName == "particleSystem" || 313 | // Also skip potentially problematic Matrix properties prone to cycles/errors 314 | propName == "worldToLocalMatrix" || propName == "localToWorldMatrix") 315 | { 316 | // Debug.Log($"[GetComponentData] Explicitly skipping generic property: {propName}"); // Optional log 317 | skipProperty = true; 318 | } 319 | // --- End Skip Generic Properties --- 320 | 321 | // --- Skip specific potentially problematic Camera properties --- 322 | if (componentType == typeof(Camera) && 323 | (propName == "pixelRect" || 324 | propName == "rect" || 325 | propName == "cullingMatrix" || 326 | propName == "useOcclusionCulling" || 327 | propName == "worldToCameraMatrix" || 328 | propName == "projectionMatrix" || 329 | propName == "nonJitteredProjectionMatrix" || 330 | propName == "previousViewProjectionMatrix" || 331 | propName == "cameraToWorldMatrix")) 332 | { 333 | // Debug.Log($"[GetComponentData] Explicitly skipping Camera property: {propName}"); 334 | skipProperty = true; 335 | } 336 | // --- End Skip Camera Properties --- 337 | 338 | // --- Skip specific potentially problematic Transform properties --- 339 | if (componentType == typeof(Transform) && 340 | (propName == "lossyScale" || 341 | propName == "rotation" || 342 | propName == "worldToLocalMatrix" || 343 | propName == "localToWorldMatrix")) 344 | { 345 | // Debug.Log($"[GetComponentData] Explicitly skipping Transform property: {propName}"); 346 | skipProperty = true; 347 | } 348 | // --- End Skip Transform Properties --- 349 | 350 | // Skip if flagged 351 | if (skipProperty) 352 | { 353 | continue; 354 | } 355 | 356 | try 357 | { 358 | // --- Add detailed logging --- 359 | // Debug.Log($"[GetComponentData] Accessing: {componentType.Name}.{propName}"); 360 | // --- End detailed logging --- 361 | 362 | // --- Special handling for material/mesh properties in edit mode --- 363 | object value; 364 | if (!Application.isPlaying && (propName == "material" || propName == "materials" || propName == "mesh")) 365 | { 366 | // In edit mode, use sharedMaterial/sharedMesh to avoid instantiation warnings 367 | if ((propName == "material" || propName == "materials") && c is Renderer renderer) 368 | { 369 | if (propName == "material") 370 | value = renderer.sharedMaterial; 371 | else // materials 372 | value = renderer.sharedMaterials; 373 | } 374 | else if (propName == "mesh" && c is MeshFilter meshFilter) 375 | { 376 | value = meshFilter.sharedMesh; 377 | } 378 | else 379 | { 380 | // Fallback to normal property access if type doesn't match 381 | value = propInfo.GetValue(c); 382 | } 383 | } 384 | else 385 | { 386 | value = propInfo.GetValue(c); 387 | } 388 | // --- End special handling --- 389 | 390 | Type propType = propInfo.PropertyType; 391 | AddSerializableValue(serializablePropertiesOutput, propName, propType, value); 392 | } 393 | catch (Exception) 394 | { 395 | // Debug.LogWarning($"Could not read property {propName} on {componentType.Name}"); 396 | } 397 | } 398 | 399 | // --- Add Logging Before Field Loop --- 400 | // Debug.Log($"[GetComponentData] Starting field loop for {componentType.Name}..."); 401 | // --- End Logging Before Field Loop --- 402 | 403 | // Use cached fields 404 | foreach (var fieldInfo in cachedData.SerializableFields) 405 | { 406 | try 407 | { 408 | // --- Add detailed logging for fields --- 409 | // Debug.Log($"[GetComponentData] Accessing Field: {componentType.Name}.{fieldInfo.Name}"); 410 | // --- End detailed logging for fields --- 411 | object value = fieldInfo.GetValue(c); 412 | string fieldName = fieldInfo.Name; 413 | Type fieldType = fieldInfo.FieldType; 414 | AddSerializableValue(serializablePropertiesOutput, fieldName, fieldType, value); 415 | } 416 | catch (Exception) 417 | { 418 | // Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}"); 419 | } 420 | } 421 | // --- End Use cached metadata --- 422 | 423 | if (serializablePropertiesOutput.Count > 0) 424 | { 425 | data["properties"] = serializablePropertiesOutput; 426 | } 427 | 428 | return data; 429 | } 430 | 431 | // Helper function to decide how to serialize different types 432 | private static void AddSerializableValue(Dictionary<string, object> dict, string name, Type type, object value) 433 | { 434 | // Simplified: Directly use CreateTokenFromValue which uses the serializer 435 | if (value == null) 436 | { 437 | dict[name] = null; 438 | return; 439 | } 440 | 441 | try 442 | { 443 | // Use the helper that employs our custom serializer settings 444 | JToken token = CreateTokenFromValue(value, type); 445 | if (token != null) // Check if serialization succeeded in the helper 446 | { 447 | // Convert JToken back to a basic object structure for the dictionary 448 | dict[name] = ConvertJTokenToPlainObject(token); 449 | } 450 | // If token is null, it means serialization failed and a warning was logged. 451 | } 452 | catch (Exception e) 453 | { 454 | // Catch potential errors during JToken conversion or addition to dictionary 455 | Debug.LogWarning($"[AddSerializableValue] Error processing value for '{name}' (Type: {type.FullName}): {e.Message}. Skipping."); 456 | } 457 | } 458 | 459 | // Helper to convert JToken back to basic object structure 460 | private static object ConvertJTokenToPlainObject(JToken token) 461 | { 462 | if (token == null) return null; 463 | 464 | switch (token.Type) 465 | { 466 | case JTokenType.Object: 467 | var objDict = new Dictionary<string, object>(); 468 | foreach (var prop in ((JObject)token).Properties()) 469 | { 470 | objDict[prop.Name] = ConvertJTokenToPlainObject(prop.Value); 471 | } 472 | return objDict; 473 | 474 | case JTokenType.Array: 475 | var list = new List<object>(); 476 | foreach (var item in (JArray)token) 477 | { 478 | list.Add(ConvertJTokenToPlainObject(item)); 479 | } 480 | return list; 481 | 482 | case JTokenType.Integer: 483 | return token.ToObject<long>(); // Use long for safety 484 | case JTokenType.Float: 485 | return token.ToObject<double>(); // Use double for safety 486 | case JTokenType.String: 487 | return token.ToObject<string>(); 488 | case JTokenType.Boolean: 489 | return token.ToObject<bool>(); 490 | case JTokenType.Date: 491 | return token.ToObject<DateTime>(); 492 | case JTokenType.Guid: 493 | return token.ToObject<Guid>(); 494 | case JTokenType.Uri: 495 | return token.ToObject<Uri>(); 496 | case JTokenType.TimeSpan: 497 | return token.ToObject<TimeSpan>(); 498 | case JTokenType.Bytes: 499 | return token.ToObject<byte[]>(); 500 | case JTokenType.Null: 501 | return null; 502 | case JTokenType.Undefined: 503 | return null; // Treat undefined as null 504 | 505 | default: 506 | // Fallback for simple value types not explicitly listed 507 | if (token is JValue jValue && jValue.Value != null) 508 | { 509 | return jValue.Value; 510 | } 511 | // Debug.LogWarning($"Unsupported JTokenType encountered: {token.Type}. Returning null."); 512 | return null; 513 | } 514 | } 515 | 516 | // --- Define custom JsonSerializerSettings for OUTPUT --- 517 | private static readonly JsonSerializerSettings _outputSerializerSettings = new JsonSerializerSettings 518 | { 519 | Converters = new List<JsonConverter> 520 | { 521 | new Vector3Converter(), 522 | new Vector2Converter(), 523 | new QuaternionConverter(), 524 | new ColorConverter(), 525 | new RectConverter(), 526 | new BoundsConverter(), 527 | new UnityEngineObjectConverter() // Handles serialization of references 528 | }, 529 | ReferenceLoopHandling = ReferenceLoopHandling.Ignore, 530 | // ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() } // Example if needed 531 | }; 532 | private static readonly JsonSerializer _outputSerializer = JsonSerializer.Create(_outputSerializerSettings); 533 | // --- End Define custom JsonSerializerSettings --- 534 | 535 | // Helper to create JToken using the output serializer 536 | private static JToken CreateTokenFromValue(object value, Type type) 537 | { 538 | if (value == null) return JValue.CreateNull(); 539 | 540 | try 541 | { 542 | // Use the pre-configured OUTPUT serializer instance 543 | return JToken.FromObject(value, _outputSerializer); 544 | } 545 | catch (JsonSerializationException e) 546 | { 547 | Debug.LogWarning($"[GameObjectSerializer] Newtonsoft.Json Error serializing value of type {type.FullName}: {e.Message}. Skipping property/field."); 548 | return null; // Indicate serialization failure 549 | } 550 | catch (Exception e) // Catch other unexpected errors 551 | { 552 | Debug.LogWarning($"[GameObjectSerializer] Unexpected error serializing value of type {type.FullName}: {e}. Skipping property/field."); 553 | return null; // Indicate serialization failure 554 | } 555 | } 556 | } 557 | } 558 | ``` -------------------------------------------------------------------------------- /UnityMcpBridge/Editor/Setup/SetupWizardWindow.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System; 2 | using System.Linq; 3 | using MCPForUnity.Editor.Data; 4 | using MCPForUnity.Editor.Dependencies; 5 | using MCPForUnity.Editor.Dependencies.Models; 6 | using MCPForUnity.Editor.Helpers; 7 | using MCPForUnity.Editor.Models; 8 | using UnityEditor; 9 | using UnityEngine; 10 | 11 | namespace MCPForUnity.Editor.Setup 12 | { 13 | /// <summary> 14 | /// Setup wizard window for guiding users through dependency installation 15 | /// </summary> 16 | public class SetupWizardWindow : EditorWindow 17 | { 18 | private DependencyCheckResult _dependencyResult; 19 | private Vector2 _scrollPosition; 20 | private int _currentStep = 0; 21 | private McpClients _mcpClients; 22 | private int _selectedClientIndex = 0; 23 | 24 | private readonly string[] _stepTitles = { 25 | "Setup", 26 | "Configure", 27 | "Complete" 28 | }; 29 | 30 | public static void ShowWindow(DependencyCheckResult dependencyResult = null) 31 | { 32 | var window = GetWindow<SetupWizardWindow>("MCP for Unity Setup"); 33 | window.minSize = new Vector2(500, 400); 34 | window.maxSize = new Vector2(800, 600); 35 | window._dependencyResult = dependencyResult ?? DependencyManager.CheckAllDependencies(); 36 | window.Show(); 37 | } 38 | 39 | private void OnEnable() 40 | { 41 | if (_dependencyResult == null) 42 | { 43 | _dependencyResult = DependencyManager.CheckAllDependencies(); 44 | } 45 | 46 | _mcpClients = new McpClients(); 47 | 48 | // Check client configurations on startup 49 | foreach (var client in _mcpClients.clients) 50 | { 51 | CheckClientConfiguration(client); 52 | } 53 | } 54 | 55 | private void OnGUI() 56 | { 57 | DrawHeader(); 58 | DrawProgressBar(); 59 | 60 | _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition); 61 | 62 | switch (_currentStep) 63 | { 64 | case 0: DrawSetupStep(); break; 65 | case 1: DrawConfigureStep(); break; 66 | case 2: DrawCompleteStep(); break; 67 | } 68 | 69 | EditorGUILayout.EndScrollView(); 70 | 71 | DrawFooter(); 72 | } 73 | 74 | private void DrawHeader() 75 | { 76 | EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); 77 | GUILayout.Label("MCP for Unity Setup Wizard", EditorStyles.boldLabel); 78 | GUILayout.FlexibleSpace(); 79 | GUILayout.Label($"Step {_currentStep + 1} of {_stepTitles.Length}"); 80 | EditorGUILayout.EndHorizontal(); 81 | 82 | EditorGUILayout.Space(); 83 | 84 | // Step title 85 | var titleStyle = new GUIStyle(EditorStyles.largeLabel) 86 | { 87 | fontSize = 16, 88 | fontStyle = FontStyle.Bold 89 | }; 90 | EditorGUILayout.LabelField(_stepTitles[_currentStep], titleStyle); 91 | EditorGUILayout.Space(); 92 | } 93 | 94 | private void DrawProgressBar() 95 | { 96 | var rect = EditorGUILayout.GetControlRect(false, 4); 97 | var progress = (_currentStep + 1) / (float)_stepTitles.Length; 98 | EditorGUI.ProgressBar(rect, progress, ""); 99 | EditorGUILayout.Space(); 100 | } 101 | 102 | private void DrawSetupStep() 103 | { 104 | // Welcome section 105 | DrawSectionTitle("MCP for Unity Setup"); 106 | 107 | EditorGUILayout.LabelField( 108 | "This wizard will help you set up MCP for Unity to connect AI assistants with your Unity Editor.", 109 | EditorStyles.wordWrappedLabel 110 | ); 111 | EditorGUILayout.Space(); 112 | 113 | // Dependency check section 114 | EditorGUILayout.BeginHorizontal(); 115 | DrawSectionTitle("System Check", 14); 116 | GUILayout.FlexibleSpace(); 117 | if (GUILayout.Button("Refresh", GUILayout.Width(60), GUILayout.Height(20))) 118 | { 119 | _dependencyResult = DependencyManager.CheckAllDependencies(); 120 | } 121 | EditorGUILayout.EndHorizontal(); 122 | 123 | // Show simplified dependency status 124 | foreach (var dep in _dependencyResult.Dependencies) 125 | { 126 | DrawSimpleDependencyStatus(dep); 127 | } 128 | 129 | // Overall status and installation guidance 130 | EditorGUILayout.Space(); 131 | if (!_dependencyResult.IsSystemReady) 132 | { 133 | // Only show critical warnings when dependencies are actually missing 134 | EditorGUILayout.HelpBox( 135 | "⚠️ Missing Dependencies: MCP for Unity requires Python 3.10+ and UV package manager to function properly.", 136 | MessageType.Warning 137 | ); 138 | 139 | EditorGUILayout.Space(); 140 | EditorGUILayout.BeginVertical(EditorStyles.helpBox); 141 | DrawErrorStatus("Installation Required"); 142 | 143 | var recommendations = DependencyManager.GetInstallationRecommendations(); 144 | EditorGUILayout.LabelField(recommendations, EditorStyles.wordWrappedLabel); 145 | 146 | EditorGUILayout.Space(); 147 | if (GUILayout.Button("Open Installation Links", GUILayout.Height(25))) 148 | { 149 | OpenInstallationUrls(); 150 | } 151 | EditorGUILayout.EndVertical(); 152 | } 153 | else 154 | { 155 | DrawSuccessStatus("System Ready"); 156 | EditorGUILayout.LabelField("All requirements are met. You can proceed to configure your AI clients.", EditorStyles.wordWrappedLabel); 157 | } 158 | } 159 | 160 | 161 | 162 | private void DrawCompleteStep() 163 | { 164 | DrawSectionTitle("Setup Complete"); 165 | 166 | // Refresh dependency check with caching to avoid heavy operations on every repaint 167 | if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2) 168 | { 169 | _dependencyResult = DependencyManager.CheckAllDependencies(); 170 | } 171 | 172 | if (_dependencyResult.IsSystemReady) 173 | { 174 | DrawSuccessStatus("MCP for Unity Ready!"); 175 | 176 | EditorGUILayout.HelpBox( 177 | "🎉 MCP for Unity is now set up and ready to use!\n\n" + 178 | "• Dependencies verified\n" + 179 | "• MCP server ready\n" + 180 | "• Client configuration accessible", 181 | MessageType.Info 182 | ); 183 | 184 | EditorGUILayout.Space(); 185 | EditorGUILayout.BeginHorizontal(); 186 | if (GUILayout.Button("Documentation", GUILayout.Height(30))) 187 | { 188 | Application.OpenURL("https://github.com/CoplayDev/unity-mcp"); 189 | } 190 | if (GUILayout.Button("Client Settings", GUILayout.Height(30))) 191 | { 192 | Windows.MCPForUnityEditorWindow.ShowWindow(); 193 | } 194 | EditorGUILayout.EndHorizontal(); 195 | } 196 | else 197 | { 198 | DrawErrorStatus("Setup Incomplete - Package Non-Functional"); 199 | 200 | EditorGUILayout.HelpBox( 201 | "🚨 MCP for Unity CANNOT work - dependencies still missing!\n\n" + 202 | "Install ALL required dependencies before the package will function.", 203 | MessageType.Error 204 | ); 205 | 206 | var missingDeps = _dependencyResult.GetMissingRequired(); 207 | if (missingDeps.Count > 0) 208 | { 209 | EditorGUILayout.Space(); 210 | EditorGUILayout.LabelField("Still Missing:", EditorStyles.boldLabel); 211 | foreach (var dep in missingDeps) 212 | { 213 | EditorGUILayout.LabelField($"✗ {dep.Name}", EditorStyles.label); 214 | } 215 | } 216 | 217 | EditorGUILayout.Space(); 218 | if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30))) 219 | { 220 | _currentStep = 0; 221 | } 222 | } 223 | } 224 | 225 | // Helper methods for consistent UI components 226 | private void DrawSectionTitle(string title, int fontSize = 16) 227 | { 228 | var titleStyle = new GUIStyle(EditorStyles.boldLabel) 229 | { 230 | fontSize = fontSize, 231 | fontStyle = FontStyle.Bold 232 | }; 233 | EditorGUILayout.LabelField(title, titleStyle); 234 | EditorGUILayout.Space(); 235 | } 236 | 237 | private void DrawSuccessStatus(string message) 238 | { 239 | var originalColor = GUI.color; 240 | GUI.color = Color.green; 241 | EditorGUILayout.LabelField($"✓ {message}", EditorStyles.boldLabel); 242 | GUI.color = originalColor; 243 | EditorGUILayout.Space(); 244 | } 245 | 246 | private void DrawErrorStatus(string message) 247 | { 248 | var originalColor = GUI.color; 249 | GUI.color = Color.red; 250 | EditorGUILayout.LabelField($"✗ {message}", EditorStyles.boldLabel); 251 | GUI.color = originalColor; 252 | EditorGUILayout.Space(); 253 | } 254 | 255 | private void DrawSimpleDependencyStatus(DependencyStatus dep) 256 | { 257 | EditorGUILayout.BeginHorizontal(); 258 | 259 | var statusIcon = dep.IsAvailable ? "✓" : "✗"; 260 | var statusColor = dep.IsAvailable ? Color.green : Color.red; 261 | 262 | var originalColor = GUI.color; 263 | GUI.color = statusColor; 264 | GUILayout.Label(statusIcon, GUILayout.Width(20)); 265 | EditorGUILayout.LabelField(dep.Name, EditorStyles.boldLabel); 266 | GUI.color = originalColor; 267 | 268 | if (!dep.IsAvailable && !string.IsNullOrEmpty(dep.ErrorMessage)) 269 | { 270 | EditorGUILayout.LabelField($"({dep.ErrorMessage})", EditorStyles.miniLabel); 271 | } 272 | 273 | EditorGUILayout.EndHorizontal(); 274 | } 275 | 276 | private void DrawConfigureStep() 277 | { 278 | DrawSectionTitle("AI Client Configuration"); 279 | 280 | // Check dependencies first (with caching to avoid heavy operations on every repaint) 281 | if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2) 282 | { 283 | _dependencyResult = DependencyManager.CheckAllDependencies(); 284 | } 285 | if (!_dependencyResult.IsSystemReady) 286 | { 287 | DrawErrorStatus("Cannot Configure - System Requirements Not Met"); 288 | 289 | EditorGUILayout.HelpBox( 290 | "Client configuration requires system dependencies to be installed first. Please complete setup before proceeding.", 291 | MessageType.Warning 292 | ); 293 | 294 | if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30))) 295 | { 296 | _currentStep = 0; 297 | } 298 | return; 299 | } 300 | 301 | EditorGUILayout.LabelField( 302 | "Configure your AI assistants to work with Unity. Select a client below to set it up:", 303 | EditorStyles.wordWrappedLabel 304 | ); 305 | EditorGUILayout.Space(); 306 | 307 | // Client selection and configuration 308 | if (_mcpClients.clients.Count > 0) 309 | { 310 | // Client selector dropdown 311 | string[] clientNames = _mcpClients.clients.Select(c => c.name).ToArray(); 312 | EditorGUI.BeginChangeCheck(); 313 | _selectedClientIndex = EditorGUILayout.Popup("Select AI Client:", _selectedClientIndex, clientNames); 314 | if (EditorGUI.EndChangeCheck()) 315 | { 316 | _selectedClientIndex = Mathf.Clamp(_selectedClientIndex, 0, _mcpClients.clients.Count - 1); 317 | // Refresh client status when selection changes 318 | CheckClientConfiguration(_mcpClients.clients[_selectedClientIndex]); 319 | } 320 | 321 | EditorGUILayout.Space(); 322 | 323 | var selectedClient = _mcpClients.clients[_selectedClientIndex]; 324 | DrawClientConfigurationInWizard(selectedClient); 325 | 326 | EditorGUILayout.Space(); 327 | 328 | // Batch configuration option 329 | EditorGUILayout.BeginVertical(EditorStyles.helpBox); 330 | EditorGUILayout.LabelField("Quick Setup", EditorStyles.boldLabel); 331 | EditorGUILayout.LabelField( 332 | "Automatically configure all detected AI clients at once:", 333 | EditorStyles.wordWrappedLabel 334 | ); 335 | EditorGUILayout.Space(); 336 | 337 | if (GUILayout.Button("Configure All Detected Clients", GUILayout.Height(30))) 338 | { 339 | ConfigureAllClientsInWizard(); 340 | } 341 | EditorGUILayout.EndVertical(); 342 | } 343 | else 344 | { 345 | EditorGUILayout.HelpBox("No AI clients detected. Make sure you have Claude Code, Cursor, or VSCode installed.", MessageType.Info); 346 | } 347 | 348 | EditorGUILayout.Space(); 349 | EditorGUILayout.HelpBox( 350 | "💡 You might need to restart your AI client after configuring.", 351 | MessageType.Info 352 | ); 353 | } 354 | 355 | private void DrawFooter() 356 | { 357 | EditorGUILayout.Space(); 358 | EditorGUILayout.BeginHorizontal(); 359 | 360 | // Back button 361 | GUI.enabled = _currentStep > 0; 362 | if (GUILayout.Button("Back", GUILayout.Width(60))) 363 | { 364 | _currentStep--; 365 | } 366 | 367 | GUILayout.FlexibleSpace(); 368 | 369 | // Skip button 370 | if (GUILayout.Button("Skip", GUILayout.Width(60))) 371 | { 372 | bool dismiss = EditorUtility.DisplayDialog( 373 | "Skip Setup", 374 | "⚠️ Skipping setup will leave MCP for Unity non-functional!\n\n" + 375 | "You can restart setup from: Window > MCP for Unity > Setup Wizard (Required)", 376 | "Skip Anyway", 377 | "Cancel" 378 | ); 379 | 380 | if (dismiss) 381 | { 382 | SetupWizard.MarkSetupDismissed(); 383 | Close(); 384 | } 385 | } 386 | 387 | // Next/Done button 388 | GUI.enabled = true; 389 | string buttonText = _currentStep == _stepTitles.Length - 1 ? "Done" : "Next"; 390 | 391 | if (GUILayout.Button(buttonText, GUILayout.Width(80))) 392 | { 393 | if (_currentStep == _stepTitles.Length - 1) 394 | { 395 | SetupWizard.MarkSetupCompleted(); 396 | Close(); 397 | } 398 | else 399 | { 400 | _currentStep++; 401 | } 402 | } 403 | 404 | GUI.enabled = true; 405 | EditorGUILayout.EndHorizontal(); 406 | } 407 | 408 | private void DrawClientConfigurationInWizard(McpClient client) 409 | { 410 | EditorGUILayout.BeginVertical(EditorStyles.helpBox); 411 | 412 | EditorGUILayout.LabelField($"{client.name} Configuration", EditorStyles.boldLabel); 413 | EditorGUILayout.Space(); 414 | 415 | // Show current status 416 | var statusColor = GetClientStatusColor(client); 417 | var originalColor = GUI.color; 418 | GUI.color = statusColor; 419 | EditorGUILayout.LabelField($"Status: {client.configStatus}", EditorStyles.label); 420 | GUI.color = originalColor; 421 | 422 | EditorGUILayout.Space(); 423 | 424 | // Configuration buttons 425 | EditorGUILayout.BeginHorizontal(); 426 | 427 | if (client.mcpType == McpTypes.ClaudeCode) 428 | { 429 | // Special handling for Claude Code 430 | bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude()); 431 | if (claudeAvailable) 432 | { 433 | bool isConfigured = client.status == McpStatus.Configured; 434 | string buttonText = isConfigured ? "Unregister" : "Register"; 435 | if (GUILayout.Button($"{buttonText} with Claude Code")) 436 | { 437 | if (isConfigured) 438 | { 439 | UnregisterFromClaudeCode(client); 440 | } 441 | else 442 | { 443 | RegisterWithClaudeCode(client); 444 | } 445 | } 446 | } 447 | else 448 | { 449 | EditorGUILayout.HelpBox("Claude Code not found. Please install Claude Code first.", MessageType.Warning); 450 | if (GUILayout.Button("Open Claude Code Website")) 451 | { 452 | Application.OpenURL("https://claude.ai/download"); 453 | } 454 | } 455 | } 456 | else 457 | { 458 | // Standard client configuration 459 | if (GUILayout.Button($"Configure {client.name}")) 460 | { 461 | ConfigureClientInWizard(client); 462 | } 463 | 464 | if (GUILayout.Button("Manual Setup")) 465 | { 466 | ShowManualSetupInWizard(client); 467 | } 468 | } 469 | 470 | EditorGUILayout.EndHorizontal(); 471 | EditorGUILayout.EndVertical(); 472 | } 473 | 474 | private Color GetClientStatusColor(McpClient client) 475 | { 476 | return client.status switch 477 | { 478 | McpStatus.Configured => Color.green, 479 | McpStatus.Running => Color.green, 480 | McpStatus.Connected => Color.green, 481 | McpStatus.IncorrectPath => Color.yellow, 482 | McpStatus.CommunicationError => Color.yellow, 483 | McpStatus.NoResponse => Color.yellow, 484 | _ => Color.red 485 | }; 486 | } 487 | 488 | private void ConfigureClientInWizard(McpClient client) 489 | { 490 | try 491 | { 492 | string result = PerformClientConfiguration(client); 493 | 494 | EditorUtility.DisplayDialog( 495 | $"{client.name} Configuration", 496 | result, 497 | "OK" 498 | ); 499 | 500 | // Refresh client status 501 | CheckClientConfiguration(client); 502 | Repaint(); 503 | } 504 | catch (System.Exception ex) 505 | { 506 | EditorUtility.DisplayDialog( 507 | "Configuration Error", 508 | $"Failed to configure {client.name}: {ex.Message}", 509 | "OK" 510 | ); 511 | } 512 | } 513 | 514 | private void ConfigureAllClientsInWizard() 515 | { 516 | int successCount = 0; 517 | int totalCount = _mcpClients.clients.Count; 518 | 519 | foreach (var client in _mcpClients.clients) 520 | { 521 | try 522 | { 523 | if (client.mcpType == McpTypes.ClaudeCode) 524 | { 525 | if (!string.IsNullOrEmpty(ExecPath.ResolveClaude()) && client.status != McpStatus.Configured) 526 | { 527 | RegisterWithClaudeCode(client); 528 | successCount++; 529 | } 530 | else if (client.status == McpStatus.Configured) 531 | { 532 | successCount++; // Already configured 533 | } 534 | } 535 | else 536 | { 537 | string result = PerformClientConfiguration(client); 538 | if (result.Contains("success", System.StringComparison.OrdinalIgnoreCase)) 539 | { 540 | successCount++; 541 | } 542 | } 543 | 544 | CheckClientConfiguration(client); 545 | } 546 | catch (System.Exception ex) 547 | { 548 | McpLog.Error($"Failed to configure {client.name}: {ex.Message}"); 549 | } 550 | } 551 | 552 | EditorUtility.DisplayDialog( 553 | "Batch Configuration Complete", 554 | $"Successfully configured {successCount} out of {totalCount} clients.\n\n" + 555 | "Restart your AI clients for changes to take effect.", 556 | "OK" 557 | ); 558 | 559 | Repaint(); 560 | } 561 | 562 | private void RegisterWithClaudeCode(McpClient client) 563 | { 564 | try 565 | { 566 | string pythonDir = McpPathResolver.FindPackagePythonDirectory(); 567 | string claudePath = ExecPath.ResolveClaude(); 568 | string uvPath = ExecPath.ResolveUv() ?? "uv"; 569 | 570 | string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py"; 571 | 572 | if (!ExecPath.TryRun(claudePath, args, null, out var stdout, out var stderr, 15000, McpPathResolver.GetPathPrepend())) 573 | { 574 | if ((stdout + stderr).Contains("already exists", System.StringComparison.OrdinalIgnoreCase)) 575 | { 576 | CheckClientConfiguration(client); 577 | EditorUtility.DisplayDialog("Claude Code", "MCP for Unity is already registered with Claude Code.", "OK"); 578 | } 579 | else 580 | { 581 | throw new System.Exception($"Registration failed: {stderr}"); 582 | } 583 | } 584 | else 585 | { 586 | CheckClientConfiguration(client); 587 | EditorUtility.DisplayDialog("Claude Code", "Successfully registered MCP for Unity with Claude Code!", "OK"); 588 | } 589 | } 590 | catch (System.Exception ex) 591 | { 592 | EditorUtility.DisplayDialog("Registration Error", $"Failed to register with Claude Code: {ex.Message}", "OK"); 593 | } 594 | } 595 | 596 | private void UnregisterFromClaudeCode(McpClient client) 597 | { 598 | try 599 | { 600 | string claudePath = ExecPath.ResolveClaude(); 601 | if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", null, out var stdout, out var stderr, 10000, McpPathResolver.GetPathPrepend())) 602 | { 603 | CheckClientConfiguration(client); 604 | EditorUtility.DisplayDialog("Claude Code", "Successfully unregistered MCP for Unity from Claude Code.", "OK"); 605 | } 606 | else 607 | { 608 | throw new System.Exception($"Unregistration failed: {stderr}"); 609 | } 610 | } 611 | catch (System.Exception ex) 612 | { 613 | EditorUtility.DisplayDialog("Unregistration Error", $"Failed to unregister from Claude Code: {ex.Message}", "OK"); 614 | } 615 | } 616 | 617 | private string PerformClientConfiguration(McpClient client) 618 | { 619 | // This mirrors the logic from MCPForUnityEditorWindow.ConfigureMcpClient 620 | string configPath = McpConfigurationHelper.GetClientConfigPath(client); 621 | string pythonDir = McpPathResolver.FindPackagePythonDirectory(); 622 | 623 | if (string.IsNullOrEmpty(pythonDir)) 624 | { 625 | return "Manual configuration required - Python server directory not found."; 626 | } 627 | 628 | McpConfigurationHelper.EnsureConfigDirectoryExists(configPath); 629 | // Use TOML writer for Codex; JSON writer for others 630 | if (client != null && client.mcpType == McpTypes.Codex) 631 | { 632 | return McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, client); 633 | } 634 | else 635 | { 636 | return McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client); 637 | } 638 | } 639 | 640 | private void ShowManualSetupInWizard(McpClient client) 641 | { 642 | string configPath = McpConfigurationHelper.GetClientConfigPath(client); 643 | string pythonDir = McpPathResolver.FindPackagePythonDirectory(); 644 | string uvPath = ServerInstaller.FindUvPath(); 645 | 646 | if (string.IsNullOrEmpty(uvPath)) 647 | { 648 | EditorUtility.DisplayDialog("Manual Setup", "UV package manager not found. Please install UV first.", "OK"); 649 | return; 650 | } 651 | 652 | // Build manual configuration using the sophisticated helper logic 653 | string result = (client != null && client.mcpType == McpTypes.Codex) 654 | ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, client) 655 | : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client); 656 | string manualConfig; 657 | 658 | if (result == "Configured successfully") 659 | { 660 | // Read back the configuration that was written 661 | try 662 | { 663 | manualConfig = System.IO.File.ReadAllText(configPath); 664 | } 665 | catch 666 | { 667 | manualConfig = "Configuration written successfully, but could not read back for display."; 668 | } 669 | } 670 | else 671 | { 672 | manualConfig = $"Configuration failed: {result}"; 673 | } 674 | 675 | EditorUtility.DisplayDialog( 676 | $"Manual Setup - {client.name}", 677 | $"Configuration file location:\n{configPath}\n\n" + 678 | $"Configuration result:\n{manualConfig}", 679 | "OK" 680 | ); 681 | } 682 | 683 | private void CheckClientConfiguration(McpClient client) 684 | { 685 | // Basic status check - could be enhanced to mirror MCPForUnityEditorWindow logic 686 | try 687 | { 688 | string configPath = McpConfigurationHelper.GetClientConfigPath(client); 689 | if (System.IO.File.Exists(configPath)) 690 | { 691 | client.configStatus = "Configured"; 692 | client.status = McpStatus.Configured; 693 | } 694 | else 695 | { 696 | client.configStatus = "Not Configured"; 697 | client.status = McpStatus.NotConfigured; 698 | } 699 | } 700 | catch 701 | { 702 | client.configStatus = "Error"; 703 | client.status = McpStatus.Error; 704 | } 705 | } 706 | 707 | private void OpenInstallationUrls() 708 | { 709 | var (pythonUrl, uvUrl) = DependencyManager.GetInstallationUrls(); 710 | 711 | bool openPython = EditorUtility.DisplayDialog( 712 | "Open Installation URLs", 713 | "Open Python installation page?", 714 | "Yes", 715 | "No" 716 | ); 717 | 718 | if (openPython) 719 | { 720 | Application.OpenURL(pythonUrl); 721 | } 722 | 723 | bool openUV = EditorUtility.DisplayDialog( 724 | "Open Installation URLs", 725 | "Open UV installation page?", 726 | "Yes", 727 | "No" 728 | ); 729 | 730 | if (openUV) 731 | { 732 | Application.OpenURL(uvUrl); 733 | } 734 | } 735 | } 736 | } 737 | ``` -------------------------------------------------------------------------------- /UnityMcpBridge/Editor/Helpers/ServerInstaller.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System; 2 | using System.IO; 3 | using System.Runtime.InteropServices; 4 | using System.Text; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using UnityEditor; 8 | using UnityEngine; 9 | 10 | namespace MCPForUnity.Editor.Helpers 11 | { 12 | public static class ServerInstaller 13 | { 14 | private const string RootFolder = "UnityMCP"; 15 | private const string ServerFolder = "UnityMcpServer"; 16 | private const string VersionFileName = "server_version.txt"; 17 | 18 | /// <summary> 19 | /// Ensures the mcp-for-unity-server is installed locally by copying from the embedded package source. 20 | /// No network calls or Git operations are performed. 21 | /// </summary> 22 | public static void EnsureServerInstalled() 23 | { 24 | try 25 | { 26 | string saveLocation = GetSaveLocation(); 27 | TryCreateMacSymlinkForAppSupport(); 28 | string destRoot = Path.Combine(saveLocation, ServerFolder); 29 | string destSrc = Path.Combine(destRoot, "src"); 30 | 31 | // Detect legacy installs and version state (logs) 32 | DetectAndLogLegacyInstallStates(destRoot); 33 | 34 | // Resolve embedded source and versions 35 | if (!TryGetEmbeddedServerSource(out string embeddedSrc)) 36 | { 37 | throw new Exception("Could not find embedded UnityMcpServer/src in the package."); 38 | } 39 | string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc, VersionFileName)) ?? "unknown"; 40 | string installedVer = ReadVersionFile(Path.Combine(destSrc, VersionFileName)); 41 | 42 | bool destHasServer = File.Exists(Path.Combine(destSrc, "server.py")); 43 | bool needOverwrite = !destHasServer 44 | || string.IsNullOrEmpty(installedVer) 45 | || (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(installedVer, embeddedVer) < 0); 46 | 47 | // Ensure destination exists 48 | Directory.CreateDirectory(destRoot); 49 | 50 | if (needOverwrite) 51 | { 52 | // Copy the entire UnityMcpServer folder (parent of src) 53 | string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc; // go up from src to UnityMcpServer 54 | CopyDirectoryRecursive(embeddedRoot, destRoot); 55 | // Write/refresh version file 56 | try { File.WriteAllText(Path.Combine(destSrc, VersionFileName), embeddedVer ?? "unknown"); } catch { } 57 | McpLog.Info($"Installed/updated server to {destRoot} (version {embeddedVer})."); 58 | } 59 | 60 | // Cleanup legacy installs that are missing version or older than embedded 61 | foreach (var legacyRoot in GetLegacyRootsForDetection()) 62 | { 63 | try 64 | { 65 | string legacySrc = Path.Combine(legacyRoot, "src"); 66 | if (!File.Exists(Path.Combine(legacySrc, "server.py"))) continue; 67 | string legacyVer = ReadVersionFile(Path.Combine(legacySrc, VersionFileName)); 68 | bool legacyOlder = string.IsNullOrEmpty(legacyVer) 69 | || (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(legacyVer, embeddedVer) < 0); 70 | if (legacyOlder) 71 | { 72 | TryKillUvForPath(legacySrc); 73 | try 74 | { 75 | Directory.Delete(legacyRoot, recursive: true); 76 | McpLog.Info($"Removed legacy server at '{legacyRoot}'."); 77 | } 78 | catch (Exception ex) 79 | { 80 | McpLog.Warn($"Failed to remove legacy server at '{legacyRoot}': {ex.Message}"); 81 | } 82 | } 83 | } 84 | catch { } 85 | } 86 | 87 | // Clear overrides that might point at legacy locations 88 | try 89 | { 90 | EditorPrefs.DeleteKey("MCPForUnity.ServerSrc"); 91 | EditorPrefs.DeleteKey("MCPForUnity.PythonDirOverride"); 92 | } 93 | catch { } 94 | return; 95 | } 96 | catch (Exception ex) 97 | { 98 | // If a usable server is already present (installed or embedded), don't fail hard—just warn. 99 | bool hasInstalled = false; 100 | try { hasInstalled = File.Exists(Path.Combine(GetServerPath(), "server.py")); } catch { } 101 | 102 | if (hasInstalled || TryGetEmbeddedServerSource(out _)) 103 | { 104 | McpLog.Warn($"Using existing server; skipped install. Details: {ex.Message}"); 105 | return; 106 | } 107 | 108 | McpLog.Error($"Failed to ensure server installation: {ex.Message}"); 109 | } 110 | } 111 | 112 | public static string GetServerPath() 113 | { 114 | return Path.Combine(GetSaveLocation(), ServerFolder, "src"); 115 | } 116 | 117 | /// <summary> 118 | /// Gets the platform-specific save location for the server. 119 | /// </summary> 120 | private static string GetSaveLocation() 121 | { 122 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 123 | { 124 | // Use per-user LocalApplicationData for canonical install location 125 | var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) 126 | ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, "AppData", "Local"); 127 | return Path.Combine(localAppData, RootFolder); 128 | } 129 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 130 | { 131 | var xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); 132 | if (string.IsNullOrEmpty(xdg)) 133 | { 134 | xdg = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, 135 | ".local", "share"); 136 | } 137 | return Path.Combine(xdg, RootFolder); 138 | } 139 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 140 | { 141 | // On macOS, use LocalApplicationData (~/Library/Application Support) 142 | var localAppSupport = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); 143 | // Unity/Mono may map LocalApplicationData to ~/.local/share on macOS; normalize to Application Support 144 | bool looksLikeXdg = !string.IsNullOrEmpty(localAppSupport) && localAppSupport.Replace('\\', '/').Contains("/.local/share"); 145 | if (string.IsNullOrEmpty(localAppSupport) || looksLikeXdg) 146 | { 147 | // Fallback: construct from $HOME 148 | var home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty; 149 | localAppSupport = Path.Combine(home, "Library", "Application Support"); 150 | } 151 | TryCreateMacSymlinkForAppSupport(); 152 | return Path.Combine(localAppSupport, RootFolder); 153 | } 154 | throw new Exception("Unsupported operating system."); 155 | } 156 | 157 | /// <summary> 158 | /// On macOS, create a no-spaces symlink ~/Library/AppSupport -> ~/Library/Application Support 159 | /// to mitigate arg parsing and quoting issues in some MCP clients. 160 | /// Safe to call repeatedly. 161 | /// </summary> 162 | private static void TryCreateMacSymlinkForAppSupport() 163 | { 164 | try 165 | { 166 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return; 167 | string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty; 168 | if (string.IsNullOrEmpty(home)) return; 169 | 170 | string canonical = Path.Combine(home, "Library", "Application Support"); 171 | string symlink = Path.Combine(home, "Library", "AppSupport"); 172 | 173 | // If symlink exists already, nothing to do 174 | if (Directory.Exists(symlink) || File.Exists(symlink)) return; 175 | 176 | // Create symlink only if canonical exists 177 | if (!Directory.Exists(canonical)) return; 178 | 179 | // Use 'ln -s' to create a directory symlink (macOS) 180 | var psi = new System.Diagnostics.ProcessStartInfo 181 | { 182 | FileName = "/bin/ln", 183 | Arguments = $"-s \"{canonical}\" \"{symlink}\"", 184 | UseShellExecute = false, 185 | RedirectStandardOutput = true, 186 | RedirectStandardError = true, 187 | CreateNoWindow = true 188 | }; 189 | using var p = System.Diagnostics.Process.Start(psi); 190 | p?.WaitForExit(2000); 191 | } 192 | catch { /* best-effort */ } 193 | } 194 | 195 | private static bool IsDirectoryWritable(string path) 196 | { 197 | try 198 | { 199 | File.Create(Path.Combine(path, "test.txt")).Dispose(); 200 | File.Delete(Path.Combine(path, "test.txt")); 201 | return true; 202 | } 203 | catch 204 | { 205 | return false; 206 | } 207 | } 208 | 209 | /// <summary> 210 | /// Checks if the server is installed at the specified location. 211 | /// </summary> 212 | private static bool IsServerInstalled(string location) 213 | { 214 | return Directory.Exists(location) 215 | && File.Exists(Path.Combine(location, ServerFolder, "src", "server.py")); 216 | } 217 | 218 | /// <summary> 219 | /// Detects legacy installs or older versions and logs findings (no deletion yet). 220 | /// </summary> 221 | private static void DetectAndLogLegacyInstallStates(string canonicalRoot) 222 | { 223 | try 224 | { 225 | string canonicalSrc = Path.Combine(canonicalRoot, "src"); 226 | // Normalize canonical root for comparisons 227 | string normCanonicalRoot = NormalizePathSafe(canonicalRoot); 228 | string embeddedSrc = null; 229 | TryGetEmbeddedServerSource(out embeddedSrc); 230 | 231 | string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc ?? string.Empty, VersionFileName)); 232 | string installedVer = ReadVersionFile(Path.Combine(canonicalSrc, VersionFileName)); 233 | 234 | // Legacy paths (macOS/Linux .config; Windows roaming as example) 235 | foreach (var legacyRoot in GetLegacyRootsForDetection()) 236 | { 237 | // Skip logging for the canonical root itself 238 | if (PathsEqualSafe(legacyRoot, normCanonicalRoot)) 239 | continue; 240 | string legacySrc = Path.Combine(legacyRoot, "src"); 241 | bool hasServer = File.Exists(Path.Combine(legacySrc, "server.py")); 242 | string legacyVer = ReadVersionFile(Path.Combine(legacySrc, VersionFileName)); 243 | 244 | if (hasServer) 245 | { 246 | // Case 1: No version file 247 | if (string.IsNullOrEmpty(legacyVer)) 248 | { 249 | McpLog.Info("Detected legacy install without version file at: " + legacyRoot, always: false); 250 | } 251 | 252 | // Case 2: Lives in legacy path 253 | McpLog.Info("Detected legacy install path: " + legacyRoot, always: false); 254 | 255 | // Case 3: Has version but appears older than embedded 256 | if (!string.IsNullOrEmpty(embeddedVer) && !string.IsNullOrEmpty(legacyVer) && CompareSemverSafe(legacyVer, embeddedVer) < 0) 257 | { 258 | McpLog.Info($"Legacy install version {legacyVer} is older than embedded {embeddedVer}", always: false); 259 | } 260 | } 261 | } 262 | 263 | // Also log if canonical is missing version (treated as older) 264 | if (Directory.Exists(canonicalRoot)) 265 | { 266 | if (string.IsNullOrEmpty(installedVer)) 267 | { 268 | McpLog.Info("Canonical install missing version file (treat as older). Path: " + canonicalRoot, always: false); 269 | } 270 | else if (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(installedVer, embeddedVer) < 0) 271 | { 272 | McpLog.Info($"Canonical install version {installedVer} is older than embedded {embeddedVer}", always: false); 273 | } 274 | } 275 | } 276 | catch (Exception ex) 277 | { 278 | McpLog.Warn("Detect legacy/version state failed: " + ex.Message); 279 | } 280 | } 281 | 282 | private static string NormalizePathSafe(string path) 283 | { 284 | try { return string.IsNullOrEmpty(path) ? path : Path.GetFullPath(path.Trim()); } 285 | catch { return path; } 286 | } 287 | 288 | private static bool PathsEqualSafe(string a, string b) 289 | { 290 | if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false; 291 | string na = NormalizePathSafe(a); 292 | string nb = NormalizePathSafe(b); 293 | try 294 | { 295 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 296 | { 297 | return string.Equals(na, nb, StringComparison.OrdinalIgnoreCase); 298 | } 299 | return string.Equals(na, nb, StringComparison.Ordinal); 300 | } 301 | catch { return false; } 302 | } 303 | 304 | private static IEnumerable<string> GetLegacyRootsForDetection() 305 | { 306 | var roots = new System.Collections.Generic.List<string>(); 307 | string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; 308 | // macOS/Linux legacy 309 | roots.Add(Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer")); 310 | roots.Add(Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer")); 311 | // Windows roaming example 312 | try 313 | { 314 | string roaming = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty; 315 | if (!string.IsNullOrEmpty(roaming)) 316 | roots.Add(Path.Combine(roaming, "UnityMCP", "UnityMcpServer")); 317 | // Windows legacy: early installers/dev scripts used %LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer 318 | // Detect this location so we can clean up older copies during install/update. 319 | string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty; 320 | if (!string.IsNullOrEmpty(localAppData)) 321 | roots.Add(Path.Combine(localAppData, "Programs", "UnityMCP", "UnityMcpServer")); 322 | } 323 | catch { } 324 | return roots; 325 | } 326 | 327 | private static void TryKillUvForPath(string serverSrcPath) 328 | { 329 | try 330 | { 331 | if (string.IsNullOrEmpty(serverSrcPath)) return; 332 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return; 333 | 334 | var psi = new System.Diagnostics.ProcessStartInfo 335 | { 336 | FileName = "/usr/bin/pgrep", 337 | Arguments = $"-f \"uv .*--directory {serverSrcPath}\"", 338 | UseShellExecute = false, 339 | RedirectStandardOutput = true, 340 | RedirectStandardError = true, 341 | CreateNoWindow = true 342 | }; 343 | using var p = System.Diagnostics.Process.Start(psi); 344 | if (p == null) return; 345 | string outp = p.StandardOutput.ReadToEnd(); 346 | p.WaitForExit(1500); 347 | if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp)) 348 | { 349 | foreach (var line in outp.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)) 350 | { 351 | if (int.TryParse(line.Trim(), out int pid)) 352 | { 353 | try { System.Diagnostics.Process.GetProcessById(pid).Kill(); } catch { } 354 | } 355 | } 356 | } 357 | } 358 | catch { } 359 | } 360 | 361 | private static string ReadVersionFile(string path) 362 | { 363 | try 364 | { 365 | if (string.IsNullOrEmpty(path) || !File.Exists(path)) return null; 366 | string v = File.ReadAllText(path).Trim(); 367 | return string.IsNullOrEmpty(v) ? null : v; 368 | } 369 | catch { return null; } 370 | } 371 | 372 | private static int CompareSemverSafe(string a, string b) 373 | { 374 | try 375 | { 376 | if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return 0; 377 | var ap = a.Split('.'); 378 | var bp = b.Split('.'); 379 | for (int i = 0; i < Math.Max(ap.Length, bp.Length); i++) 380 | { 381 | int ai = (i < ap.Length && int.TryParse(ap[i], out var t1)) ? t1 : 0; 382 | int bi = (i < bp.Length && int.TryParse(bp[i], out var t2)) ? t2 : 0; 383 | if (ai != bi) return ai.CompareTo(bi); 384 | } 385 | return 0; 386 | } 387 | catch { return 0; } 388 | } 389 | 390 | /// <summary> 391 | /// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package 392 | /// or common development locations. 393 | /// </summary> 394 | private static bool TryGetEmbeddedServerSource(out string srcPath) 395 | { 396 | return ServerPathResolver.TryFindEmbeddedServerSource(out srcPath); 397 | } 398 | 399 | private static readonly string[] _skipDirs = { ".venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".git" }; 400 | private static void CopyDirectoryRecursive(string sourceDir, string destinationDir) 401 | { 402 | Directory.CreateDirectory(destinationDir); 403 | 404 | foreach (string filePath in Directory.GetFiles(sourceDir)) 405 | { 406 | string fileName = Path.GetFileName(filePath); 407 | string destFile = Path.Combine(destinationDir, fileName); 408 | File.Copy(filePath, destFile, overwrite: true); 409 | } 410 | 411 | foreach (string dirPath in Directory.GetDirectories(sourceDir)) 412 | { 413 | string dirName = Path.GetFileName(dirPath); 414 | foreach (var skip in _skipDirs) 415 | { 416 | if (dirName.Equals(skip, StringComparison.OrdinalIgnoreCase)) 417 | goto NextDir; 418 | } 419 | try { if ((File.GetAttributes(dirPath) & FileAttributes.ReparsePoint) != 0) continue; } catch { } 420 | string destSubDir = Path.Combine(destinationDir, dirName); 421 | CopyDirectoryRecursive(dirPath, destSubDir); 422 | NextDir:; 423 | } 424 | } 425 | 426 | public static bool RebuildMcpServer() 427 | { 428 | try 429 | { 430 | // Find embedded source 431 | if (!TryGetEmbeddedServerSource(out string embeddedSrc)) 432 | { 433 | Debug.LogError("RebuildMcpServer: Could not find embedded server source."); 434 | return false; 435 | } 436 | 437 | string saveLocation = GetSaveLocation(); 438 | string destRoot = Path.Combine(saveLocation, ServerFolder); 439 | string destSrc = Path.Combine(destRoot, "src"); 440 | 441 | // Kill any running uv processes for this server 442 | TryKillUvForPath(destSrc); 443 | 444 | // Delete the entire installed server directory 445 | if (Directory.Exists(destRoot)) 446 | { 447 | try 448 | { 449 | Directory.Delete(destRoot, recursive: true); 450 | Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Deleted existing server at {destRoot}"); 451 | } 452 | catch (Exception ex) 453 | { 454 | Debug.LogError($"Failed to delete existing server: {ex.Message}"); 455 | return false; 456 | } 457 | } 458 | 459 | // Re-copy from embedded source 460 | string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc; 461 | Directory.CreateDirectory(destRoot); 462 | CopyDirectoryRecursive(embeddedRoot, destRoot); 463 | 464 | // Write version file 465 | string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc, VersionFileName)) ?? "unknown"; 466 | try 467 | { 468 | File.WriteAllText(Path.Combine(destSrc, VersionFileName), embeddedVer); 469 | } 470 | catch (Exception ex) 471 | { 472 | Debug.LogWarning($"Failed to write version file: {ex.Message}"); 473 | } 474 | 475 | Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Server rebuilt successfully at {destRoot} (version {embeddedVer})"); 476 | return true; 477 | } 478 | catch (Exception ex) 479 | { 480 | Debug.LogError($"RebuildMcpServer failed: {ex.Message}"); 481 | return false; 482 | } 483 | } 484 | 485 | internal static string FindUvPath() 486 | { 487 | // Allow user override via EditorPrefs 488 | try 489 | { 490 | string overridePath = EditorPrefs.GetString("MCPForUnity.UvPath", string.Empty); 491 | if (!string.IsNullOrEmpty(overridePath) && File.Exists(overridePath)) 492 | { 493 | if (ValidateUvBinary(overridePath)) return overridePath; 494 | } 495 | } 496 | catch { } 497 | 498 | string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; 499 | 500 | // Platform-specific candidate lists 501 | string[] candidates; 502 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 503 | { 504 | string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty; 505 | string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) ?? string.Empty; 506 | string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty; 507 | 508 | // Fast path: resolve from PATH first 509 | try 510 | { 511 | var wherePsi = new System.Diagnostics.ProcessStartInfo 512 | { 513 | FileName = "where", 514 | Arguments = "uv.exe", 515 | UseShellExecute = false, 516 | RedirectStandardOutput = true, 517 | RedirectStandardError = true, 518 | CreateNoWindow = true 519 | }; 520 | using var wp = System.Diagnostics.Process.Start(wherePsi); 521 | string output = wp.StandardOutput.ReadToEnd().Trim(); 522 | wp.WaitForExit(1500); 523 | if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output)) 524 | { 525 | foreach (var line in output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) 526 | { 527 | string path = line.Trim(); 528 | if (File.Exists(path) && ValidateUvBinary(path)) return path; 529 | } 530 | } 531 | } 532 | catch { } 533 | 534 | // Windows Store (PythonSoftwareFoundation) install location probe 535 | // Example: %LOCALAPPDATA%\Packages\PythonSoftwareFoundation.Python.3.13_*\LocalCache\local-packages\Python313\Scripts\uv.exe 536 | try 537 | { 538 | string pkgsRoot = Path.Combine(localAppData, "Packages"); 539 | if (Directory.Exists(pkgsRoot)) 540 | { 541 | var pythonPkgs = Directory.GetDirectories(pkgsRoot, "PythonSoftwareFoundation.Python.*", SearchOption.TopDirectoryOnly) 542 | .OrderByDescending(p => p, StringComparer.OrdinalIgnoreCase); 543 | foreach (var pkg in pythonPkgs) 544 | { 545 | string localCache = Path.Combine(pkg, "LocalCache", "local-packages"); 546 | if (!Directory.Exists(localCache)) continue; 547 | var pyRoots = Directory.GetDirectories(localCache, "Python*", SearchOption.TopDirectoryOnly) 548 | .OrderByDescending(d => d, StringComparer.OrdinalIgnoreCase); 549 | foreach (var pyRoot in pyRoots) 550 | { 551 | string uvExe = Path.Combine(pyRoot, "Scripts", "uv.exe"); 552 | if (File.Exists(uvExe) && ValidateUvBinary(uvExe)) return uvExe; 553 | } 554 | } 555 | } 556 | } 557 | catch { } 558 | 559 | candidates = new[] 560 | { 561 | // Preferred: WinGet Links shims (stable entrypoints) 562 | // Per-user shim (LOCALAPPDATA) → machine-wide shim (Program Files\WinGet\Links) 563 | Path.Combine(localAppData, "Microsoft", "WinGet", "Links", "uv.exe"), 564 | Path.Combine(programFiles, "WinGet", "Links", "uv.exe"), 565 | 566 | // Common per-user installs 567 | Path.Combine(localAppData, @"Programs\Python\Python313\Scripts\uv.exe"), 568 | Path.Combine(localAppData, @"Programs\Python\Python312\Scripts\uv.exe"), 569 | Path.Combine(localAppData, @"Programs\Python\Python311\Scripts\uv.exe"), 570 | Path.Combine(localAppData, @"Programs\Python\Python310\Scripts\uv.exe"), 571 | Path.Combine(appData, @"Python\Python313\Scripts\uv.exe"), 572 | Path.Combine(appData, @"Python\Python312\Scripts\uv.exe"), 573 | Path.Combine(appData, @"Python\Python311\Scripts\uv.exe"), 574 | Path.Combine(appData, @"Python\Python310\Scripts\uv.exe"), 575 | 576 | // Program Files style installs (if a native installer was used) 577 | Path.Combine(programFiles, @"uv\uv.exe"), 578 | 579 | // Try simple name resolution later via PATH 580 | "uv.exe", 581 | "uv" 582 | }; 583 | } 584 | else 585 | { 586 | candidates = new[] 587 | { 588 | "/opt/homebrew/bin/uv", 589 | "/usr/local/bin/uv", 590 | "/usr/bin/uv", 591 | "/opt/local/bin/uv", 592 | Path.Combine(home, ".local", "bin", "uv"), 593 | "/opt/homebrew/opt/uv/bin/uv", 594 | // Framework Python installs 595 | "/Library/Frameworks/Python.framework/Versions/3.13/bin/uv", 596 | "/Library/Frameworks/Python.framework/Versions/3.12/bin/uv", 597 | // Fallback to PATH resolution by name 598 | "uv" 599 | }; 600 | } 601 | 602 | foreach (string c in candidates) 603 | { 604 | try 605 | { 606 | if (File.Exists(c) && ValidateUvBinary(c)) return c; 607 | } 608 | catch { /* ignore */ } 609 | } 610 | 611 | // Use platform-appropriate which/where to resolve from PATH (non-Windows handled here; Windows tried earlier) 612 | try 613 | { 614 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 615 | { 616 | var whichPsi = new System.Diagnostics.ProcessStartInfo 617 | { 618 | FileName = "/usr/bin/which", 619 | Arguments = "uv", 620 | UseShellExecute = false, 621 | RedirectStandardOutput = true, 622 | RedirectStandardError = true, 623 | CreateNoWindow = true 624 | }; 625 | try 626 | { 627 | // Prepend common user-local and package manager locations so 'which' can see them in Unity's GUI env 628 | string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; 629 | string prepend = string.Join(":", new[] 630 | { 631 | System.IO.Path.Combine(homeDir, ".local", "bin"), 632 | "/opt/homebrew/bin", 633 | "/usr/local/bin", 634 | "/usr/bin", 635 | "/bin" 636 | }); 637 | string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; 638 | whichPsi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath) ? prepend : (prepend + ":" + currentPath); 639 | } 640 | catch { } 641 | using var wp = System.Diagnostics.Process.Start(whichPsi); 642 | string output = wp.StandardOutput.ReadToEnd().Trim(); 643 | wp.WaitForExit(3000); 644 | if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output)) 645 | { 646 | if (ValidateUvBinary(output)) return output; 647 | } 648 | } 649 | } 650 | catch { } 651 | 652 | // Manual PATH scan 653 | try 654 | { 655 | string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; 656 | string[] parts = pathEnv.Split(Path.PathSeparator); 657 | foreach (string part in parts) 658 | { 659 | try 660 | { 661 | // Check both uv and uv.exe 662 | string candidateUv = Path.Combine(part, "uv"); 663 | string candidateUvExe = Path.Combine(part, "uv.exe"); 664 | if (File.Exists(candidateUv) && ValidateUvBinary(candidateUv)) return candidateUv; 665 | if (File.Exists(candidateUvExe) && ValidateUvBinary(candidateUvExe)) return candidateUvExe; 666 | } 667 | catch { } 668 | } 669 | } 670 | catch { } 671 | 672 | return null; 673 | } 674 | 675 | private static bool ValidateUvBinary(string uvPath) 676 | { 677 | try 678 | { 679 | var psi = new System.Diagnostics.ProcessStartInfo 680 | { 681 | FileName = uvPath, 682 | Arguments = "--version", 683 | UseShellExecute = false, 684 | RedirectStandardOutput = true, 685 | RedirectStandardError = true, 686 | CreateNoWindow = true 687 | }; 688 | using var p = System.Diagnostics.Process.Start(psi); 689 | if (!p.WaitForExit(5000)) { try { p.Kill(); } catch { } return false; } 690 | if (p.ExitCode == 0) 691 | { 692 | string output = p.StandardOutput.ReadToEnd().Trim(); 693 | return output.StartsWith("uv "); 694 | } 695 | } 696 | catch { } 697 | return false; 698 | } 699 | } 700 | } 701 | ```