This is page 7 of 13. Use http://codebase.md/justinpbarnett/unity-mcp?lines=false&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 using System; using System.Linq; using MCPForUnity.Editor.Data; using MCPForUnity.Editor.Dependencies; using MCPForUnity.Editor.Dependencies.Models; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Models; using UnityEditor; using UnityEngine; namespace MCPForUnity.Editor.Setup { /// <summary> /// Setup wizard window for guiding users through dependency installation /// </summary> public class SetupWizardWindow : EditorWindow { private DependencyCheckResult _dependencyResult; private Vector2 _scrollPosition; private int _currentStep = 0; private McpClients _mcpClients; private int _selectedClientIndex = 0; private readonly string[] _stepTitles = { "Setup", "Configure", "Complete" }; public static void ShowWindow(DependencyCheckResult dependencyResult = null) { var window = GetWindow<SetupWizardWindow>("MCP for Unity Setup"); window.minSize = new Vector2(500, 400); window.maxSize = new Vector2(800, 600); window._dependencyResult = dependencyResult ?? DependencyManager.CheckAllDependencies(); window.Show(); } private void OnEnable() { if (_dependencyResult == null) { _dependencyResult = DependencyManager.CheckAllDependencies(); } _mcpClients = new McpClients(); // Check client configurations on startup foreach (var client in _mcpClients.clients) { CheckClientConfiguration(client); } } private void OnGUI() { DrawHeader(); DrawProgressBar(); _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition); switch (_currentStep) { case 0: DrawSetupStep(); break; case 1: DrawConfigureStep(); break; case 2: DrawCompleteStep(); break; } EditorGUILayout.EndScrollView(); DrawFooter(); } private void DrawHeader() { EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); GUILayout.Label("MCP for Unity Setup Wizard", EditorStyles.boldLabel); GUILayout.FlexibleSpace(); GUILayout.Label($"Step {_currentStep + 1} of {_stepTitles.Length}"); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); // Step title var titleStyle = new GUIStyle(EditorStyles.largeLabel) { fontSize = 16, fontStyle = FontStyle.Bold }; EditorGUILayout.LabelField(_stepTitles[_currentStep], titleStyle); EditorGUILayout.Space(); } private void DrawProgressBar() { var rect = EditorGUILayout.GetControlRect(false, 4); var progress = (_currentStep + 1) / (float)_stepTitles.Length; EditorGUI.ProgressBar(rect, progress, ""); EditorGUILayout.Space(); } private void DrawSetupStep() { // Welcome section DrawSectionTitle("MCP for Unity Setup"); EditorGUILayout.LabelField( "This wizard will help you set up MCP for Unity to connect AI assistants with your Unity Editor.", EditorStyles.wordWrappedLabel ); EditorGUILayout.Space(); // Dependency check section EditorGUILayout.BeginHorizontal(); DrawSectionTitle("System Check", 14); GUILayout.FlexibleSpace(); if (GUILayout.Button("Refresh", GUILayout.Width(60), GUILayout.Height(20))) { _dependencyResult = DependencyManager.CheckAllDependencies(); } EditorGUILayout.EndHorizontal(); // Show simplified dependency status foreach (var dep in _dependencyResult.Dependencies) { DrawSimpleDependencyStatus(dep); } // Overall status and installation guidance EditorGUILayout.Space(); if (!_dependencyResult.IsSystemReady) { // Only show critical warnings when dependencies are actually missing EditorGUILayout.HelpBox( "⚠️ Missing Dependencies: MCP for Unity requires Python 3.10+ and UV package manager to function properly.", MessageType.Warning ); EditorGUILayout.Space(); EditorGUILayout.BeginVertical(EditorStyles.helpBox); DrawErrorStatus("Installation Required"); var recommendations = DependencyManager.GetInstallationRecommendations(); EditorGUILayout.LabelField(recommendations, EditorStyles.wordWrappedLabel); EditorGUILayout.Space(); if (GUILayout.Button("Open Installation Links", GUILayout.Height(25))) { OpenInstallationUrls(); } EditorGUILayout.EndVertical(); } else { DrawSuccessStatus("System Ready"); EditorGUILayout.LabelField("All requirements are met. You can proceed to configure your AI clients.", EditorStyles.wordWrappedLabel); } } private void DrawCompleteStep() { DrawSectionTitle("Setup Complete"); // Refresh dependency check with caching to avoid heavy operations on every repaint if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2) { _dependencyResult = DependencyManager.CheckAllDependencies(); } if (_dependencyResult.IsSystemReady) { DrawSuccessStatus("MCP for Unity Ready!"); EditorGUILayout.HelpBox( "🎉 MCP for Unity is now set up and ready to use!\n\n" + "• Dependencies verified\n" + "• MCP server ready\n" + "• Client configuration accessible", MessageType.Info ); EditorGUILayout.Space(); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Documentation", GUILayout.Height(30))) { Application.OpenURL("https://github.com/CoplayDev/unity-mcp"); } if (GUILayout.Button("Client Settings", GUILayout.Height(30))) { Windows.MCPForUnityEditorWindow.ShowWindow(); } EditorGUILayout.EndHorizontal(); } else { DrawErrorStatus("Setup Incomplete - Package Non-Functional"); EditorGUILayout.HelpBox( "🚨 MCP for Unity CANNOT work - dependencies still missing!\n\n" + "Install ALL required dependencies before the package will function.", MessageType.Error ); var missingDeps = _dependencyResult.GetMissingRequired(); if (missingDeps.Count > 0) { EditorGUILayout.Space(); EditorGUILayout.LabelField("Still Missing:", EditorStyles.boldLabel); foreach (var dep in missingDeps) { EditorGUILayout.LabelField($"✗ {dep.Name}", EditorStyles.label); } } EditorGUILayout.Space(); if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30))) { _currentStep = 0; } } } // Helper methods for consistent UI components private void DrawSectionTitle(string title, int fontSize = 16) { var titleStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = fontSize, fontStyle = FontStyle.Bold }; EditorGUILayout.LabelField(title, titleStyle); EditorGUILayout.Space(); } private void DrawSuccessStatus(string message) { var originalColor = GUI.color; GUI.color = Color.green; EditorGUILayout.LabelField($"✓ {message}", EditorStyles.boldLabel); GUI.color = originalColor; EditorGUILayout.Space(); } private void DrawErrorStatus(string message) { var originalColor = GUI.color; GUI.color = Color.red; EditorGUILayout.LabelField($"✗ {message}", EditorStyles.boldLabel); GUI.color = originalColor; EditorGUILayout.Space(); } private void DrawSimpleDependencyStatus(DependencyStatus dep) { EditorGUILayout.BeginHorizontal(); var statusIcon = dep.IsAvailable ? "✓" : "✗"; var statusColor = dep.IsAvailable ? Color.green : Color.red; var originalColor = GUI.color; GUI.color = statusColor; GUILayout.Label(statusIcon, GUILayout.Width(20)); EditorGUILayout.LabelField(dep.Name, EditorStyles.boldLabel); GUI.color = originalColor; if (!dep.IsAvailable && !string.IsNullOrEmpty(dep.ErrorMessage)) { EditorGUILayout.LabelField($"({dep.ErrorMessage})", EditorStyles.miniLabel); } EditorGUILayout.EndHorizontal(); } private void DrawConfigureStep() { DrawSectionTitle("AI Client Configuration"); // Check dependencies first (with caching to avoid heavy operations on every repaint) if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2) { _dependencyResult = DependencyManager.CheckAllDependencies(); } if (!_dependencyResult.IsSystemReady) { DrawErrorStatus("Cannot Configure - System Requirements Not Met"); EditorGUILayout.HelpBox( "Client configuration requires system dependencies to be installed first. Please complete setup before proceeding.", MessageType.Warning ); if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30))) { _currentStep = 0; } return; } EditorGUILayout.LabelField( "Configure your AI assistants to work with Unity. Select a client below to set it up:", EditorStyles.wordWrappedLabel ); EditorGUILayout.Space(); // Client selection and configuration if (_mcpClients.clients.Count > 0) { // Client selector dropdown string[] clientNames = _mcpClients.clients.Select(c => c.name).ToArray(); EditorGUI.BeginChangeCheck(); _selectedClientIndex = EditorGUILayout.Popup("Select AI Client:", _selectedClientIndex, clientNames); if (EditorGUI.EndChangeCheck()) { _selectedClientIndex = Mathf.Clamp(_selectedClientIndex, 0, _mcpClients.clients.Count - 1); // Refresh client status when selection changes CheckClientConfiguration(_mcpClients.clients[_selectedClientIndex]); } EditorGUILayout.Space(); var selectedClient = _mcpClients.clients[_selectedClientIndex]; DrawClientConfigurationInWizard(selectedClient); EditorGUILayout.Space(); // Batch configuration option EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.LabelField("Quick Setup", EditorStyles.boldLabel); EditorGUILayout.LabelField( "Automatically configure all detected AI clients at once:", EditorStyles.wordWrappedLabel ); EditorGUILayout.Space(); if (GUILayout.Button("Configure All Detected Clients", GUILayout.Height(30))) { ConfigureAllClientsInWizard(); } EditorGUILayout.EndVertical(); } else { EditorGUILayout.HelpBox("No AI clients detected. Make sure you have Claude Code, Cursor, or VSCode installed.", MessageType.Info); } EditorGUILayout.Space(); EditorGUILayout.HelpBox( "💡 You might need to restart your AI client after configuring.", MessageType.Info ); } private void DrawFooter() { EditorGUILayout.Space(); EditorGUILayout.BeginHorizontal(); // Back button GUI.enabled = _currentStep > 0; if (GUILayout.Button("Back", GUILayout.Width(60))) { _currentStep--; } GUILayout.FlexibleSpace(); // Skip button if (GUILayout.Button("Skip", GUILayout.Width(60))) { bool dismiss = EditorUtility.DisplayDialog( "Skip Setup", "⚠️ Skipping setup will leave MCP for Unity non-functional!\n\n" + "You can restart setup from: Window > MCP for Unity > Setup Wizard (Required)", "Skip Anyway", "Cancel" ); if (dismiss) { SetupWizard.MarkSetupDismissed(); Close(); } } // Next/Done button GUI.enabled = true; string buttonText = _currentStep == _stepTitles.Length - 1 ? "Done" : "Next"; if (GUILayout.Button(buttonText, GUILayout.Width(80))) { if (_currentStep == _stepTitles.Length - 1) { SetupWizard.MarkSetupCompleted(); Close(); } else { _currentStep++; } } GUI.enabled = true; EditorGUILayout.EndHorizontal(); } private void DrawClientConfigurationInWizard(McpClient client) { EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.LabelField($"{client.name} Configuration", EditorStyles.boldLabel); EditorGUILayout.Space(); // Show current status var statusColor = GetClientStatusColor(client); var originalColor = GUI.color; GUI.color = statusColor; EditorGUILayout.LabelField($"Status: {client.configStatus}", EditorStyles.label); GUI.color = originalColor; EditorGUILayout.Space(); // Configuration buttons EditorGUILayout.BeginHorizontal(); if (client.mcpType == McpTypes.ClaudeCode) { // Special handling for Claude Code bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude()); if (claudeAvailable) { bool isConfigured = client.status == McpStatus.Configured; string buttonText = isConfigured ? "Unregister" : "Register"; if (GUILayout.Button($"{buttonText} with Claude Code")) { if (isConfigured) { UnregisterFromClaudeCode(client); } else { RegisterWithClaudeCode(client); } } } else { EditorGUILayout.HelpBox("Claude Code not found. Please install Claude Code first.", MessageType.Warning); if (GUILayout.Button("Open Claude Code Website")) { Application.OpenURL("https://claude.ai/download"); } } } else { // Standard client configuration if (GUILayout.Button($"Configure {client.name}")) { ConfigureClientInWizard(client); } if (GUILayout.Button("Manual Setup")) { ShowManualSetupInWizard(client); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); } private Color GetClientStatusColor(McpClient client) { return client.status switch { McpStatus.Configured => Color.green, McpStatus.Running => Color.green, McpStatus.Connected => Color.green, McpStatus.IncorrectPath => Color.yellow, McpStatus.CommunicationError => Color.yellow, McpStatus.NoResponse => Color.yellow, _ => Color.red }; } private void ConfigureClientInWizard(McpClient client) { try { string result = PerformClientConfiguration(client); EditorUtility.DisplayDialog( $"{client.name} Configuration", result, "OK" ); // Refresh client status CheckClientConfiguration(client); Repaint(); } catch (System.Exception ex) { EditorUtility.DisplayDialog( "Configuration Error", $"Failed to configure {client.name}: {ex.Message}", "OK" ); } } private void ConfigureAllClientsInWizard() { int successCount = 0; int totalCount = _mcpClients.clients.Count; foreach (var client in _mcpClients.clients) { try { if (client.mcpType == McpTypes.ClaudeCode) { if (!string.IsNullOrEmpty(ExecPath.ResolveClaude()) && client.status != McpStatus.Configured) { RegisterWithClaudeCode(client); successCount++; } else if (client.status == McpStatus.Configured) { successCount++; // Already configured } } else { string result = PerformClientConfiguration(client); if (result.Contains("success", System.StringComparison.OrdinalIgnoreCase)) { successCount++; } } CheckClientConfiguration(client); } catch (System.Exception ex) { McpLog.Error($"Failed to configure {client.name}: {ex.Message}"); } } EditorUtility.DisplayDialog( "Batch Configuration Complete", $"Successfully configured {successCount} out of {totalCount} clients.\n\n" + "Restart your AI clients for changes to take effect.", "OK" ); Repaint(); } private void RegisterWithClaudeCode(McpClient client) { try { string pythonDir = McpPathResolver.FindPackagePythonDirectory(); string claudePath = ExecPath.ResolveClaude(); string uvPath = ExecPath.ResolveUv() ?? "uv"; string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py"; if (!ExecPath.TryRun(claudePath, args, null, out var stdout, out var stderr, 15000, McpPathResolver.GetPathPrepend())) { if ((stdout + stderr).Contains("already exists", System.StringComparison.OrdinalIgnoreCase)) { CheckClientConfiguration(client); EditorUtility.DisplayDialog("Claude Code", "MCP for Unity is already registered with Claude Code.", "OK"); } else { throw new System.Exception($"Registration failed: {stderr}"); } } else { CheckClientConfiguration(client); EditorUtility.DisplayDialog("Claude Code", "Successfully registered MCP for Unity with Claude Code!", "OK"); } } catch (System.Exception ex) { EditorUtility.DisplayDialog("Registration Error", $"Failed to register with Claude Code: {ex.Message}", "OK"); } } private void UnregisterFromClaudeCode(McpClient client) { try { string claudePath = ExecPath.ResolveClaude(); if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", null, out var stdout, out var stderr, 10000, McpPathResolver.GetPathPrepend())) { CheckClientConfiguration(client); EditorUtility.DisplayDialog("Claude Code", "Successfully unregistered MCP for Unity from Claude Code.", "OK"); } else { throw new System.Exception($"Unregistration failed: {stderr}"); } } catch (System.Exception ex) { EditorUtility.DisplayDialog("Unregistration Error", $"Failed to unregister from Claude Code: {ex.Message}", "OK"); } } private string PerformClientConfiguration(McpClient client) { // This mirrors the logic from MCPForUnityEditorWindow.ConfigureMcpClient string configPath = McpConfigurationHelper.GetClientConfigPath(client); string pythonDir = McpPathResolver.FindPackagePythonDirectory(); if (string.IsNullOrEmpty(pythonDir)) { return "Manual configuration required - Python server directory not found."; } McpConfigurationHelper.EnsureConfigDirectoryExists(configPath); return McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client); } private void ShowManualSetupInWizard(McpClient client) { string configPath = McpConfigurationHelper.GetClientConfigPath(client); string pythonDir = McpPathResolver.FindPackagePythonDirectory(); string uvPath = ServerInstaller.FindUvPath(); if (string.IsNullOrEmpty(uvPath)) { EditorUtility.DisplayDialog("Manual Setup", "UV package manager not found. Please install UV first.", "OK"); return; } // Build manual configuration using the sophisticated helper logic string result = McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client); string manualConfig; if (result == "Configured successfully") { // Read back the configuration that was written try { manualConfig = System.IO.File.ReadAllText(configPath); } catch { manualConfig = "Configuration written successfully, but could not read back for display."; } } else { manualConfig = $"Configuration failed: {result}"; } EditorUtility.DisplayDialog( $"Manual Setup - {client.name}", $"Configuration file location:\n{configPath}\n\n" + $"Configuration result:\n{manualConfig}", "OK" ); } private void CheckClientConfiguration(McpClient client) { // Basic status check - could be enhanced to mirror MCPForUnityEditorWindow logic try { string configPath = McpConfigurationHelper.GetClientConfigPath(client); if (System.IO.File.Exists(configPath)) { client.configStatus = "Configured"; client.status = McpStatus.Configured; } else { client.configStatus = "Not Configured"; client.status = McpStatus.NotConfigured; } } catch { client.configStatus = "Error"; client.status = McpStatus.Error; } } private void OpenInstallationUrls() { var (pythonUrl, uvUrl) = DependencyManager.GetInstallationUrls(); bool openPython = EditorUtility.DisplayDialog( "Open Installation URLs", "Open Python installation page?", "Yes", "No" ); if (openPython) { Application.OpenURL(pythonUrl); } bool openUV = EditorUtility.DisplayDialog( "Open Installation URLs", "Open UV installation page?", "Yes", "No" ); if (openUV) { Application.OpenURL(uvUrl); } } } } ``` -------------------------------------------------------------------------------- /TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectTests.cs: -------------------------------------------------------------------------------- ```csharp using System; using System.Collections.Generic; using System.Collections; using NUnit.Framework; using UnityEngine; using UnityEditor; using UnityEngine.TestTools; using Newtonsoft.Json.Linq; using MCPForUnity.Editor.Tools; namespace MCPForUnityTests.Editor.Tools { public class ManageGameObjectTests { private GameObject testGameObject; [SetUp] public void SetUp() { // Create a test GameObject for each test testGameObject = new GameObject("TestObject"); } [TearDown] public void TearDown() { // Clean up test GameObject if (testGameObject != null) { UnityEngine.Object.DestroyImmediate(testGameObject); } } [Test] public void HandleCommand_ReturnsError_ForNullParams() { var result = ManageGameObject.HandleCommand(null); Assert.IsNotNull(result, "Should return a result object"); // Note: Actual error checking would need access to Response structure } [Test] public void HandleCommand_ReturnsError_ForEmptyParams() { var emptyParams = new JObject(); var result = ManageGameObject.HandleCommand(emptyParams); Assert.IsNotNull(result, "Should return a result object for empty params"); } [Test] public void HandleCommand_ProcessesValidCreateAction() { var createParams = new JObject { ["action"] = "create", ["name"] = "TestCreateObject" }; var result = ManageGameObject.HandleCommand(createParams); Assert.IsNotNull(result, "Should return a result for valid create action"); // Clean up - find and destroy the created object var createdObject = GameObject.Find("TestCreateObject"); if (createdObject != null) { UnityEngine.Object.DestroyImmediate(createdObject); } } [Test] public void ComponentResolver_Integration_WorksWithRealComponents() { // Test that our ComponentResolver works with actual Unity components var transformResult = ComponentResolver.TryResolve("Transform", out Type transformType, out string error); Assert.IsTrue(transformResult, "Should resolve Transform component"); Assert.AreEqual(typeof(Transform), transformType, "Should return correct Transform type"); Assert.IsEmpty(error, "Should have no error for valid component"); } [Test] public void ComponentResolver_Integration_WorksWithBuiltInComponents() { var components = new[] { ("Rigidbody", typeof(Rigidbody)), ("Collider", typeof(Collider)), ("Renderer", typeof(Renderer)), ("Camera", typeof(Camera)), ("Light", typeof(Light)) }; foreach (var (componentName, expectedType) in components) { var result = ComponentResolver.TryResolve(componentName, out Type actualType, out string error); // Some components might not resolve (abstract classes), but the method should handle gracefully if (result) { Assert.IsTrue(expectedType.IsAssignableFrom(actualType), $"{componentName} should resolve to assignable type"); } else { Assert.IsNotEmpty(error, $"Should have error message for {componentName}"); } } } [Test] public void PropertyMatching_Integration_WorksWithRealGameObject() { // Add a Rigidbody to test real property matching var rigidbody = testGameObject.AddComponent<Rigidbody>(); var properties = ComponentResolver.GetAllComponentProperties(typeof(Rigidbody)); Assert.IsNotEmpty(properties, "Rigidbody should have properties"); Assert.Contains("mass", properties, "Rigidbody should have mass property"); Assert.Contains("useGravity", properties, "Rigidbody should have useGravity property"); // Test AI suggestions var suggestions = ComponentResolver.GetAIPropertySuggestions("Use Gravity", properties); Assert.Contains("useGravity", suggestions, "Should suggest useGravity for 'Use Gravity'"); } [Test] public void PropertyMatching_HandlesMonoBehaviourProperties() { var properties = ComponentResolver.GetAllComponentProperties(typeof(MonoBehaviour)); Assert.IsNotEmpty(properties, "MonoBehaviour should have properties"); Assert.Contains("enabled", properties, "MonoBehaviour should have enabled property"); Assert.Contains("name", properties, "MonoBehaviour should have name property"); Assert.Contains("tag", properties, "MonoBehaviour should have tag property"); } [Test] public void PropertyMatching_HandlesCaseVariations() { var testProperties = new List<string> { "maxReachDistance", "playerHealth", "movementSpeed" }; var testCases = new[] { ("max reach distance", "maxReachDistance"), ("Max Reach Distance", "maxReachDistance"), ("MAX_REACH_DISTANCE", "maxReachDistance"), ("player health", "playerHealth"), ("movement speed", "movementSpeed") }; foreach (var (input, expected) in testCases) { var suggestions = ComponentResolver.GetAIPropertySuggestions(input, testProperties); Assert.Contains(expected, suggestions, $"Should suggest {expected} for input '{input}'"); } } [Test] public void ErrorHandling_ReturnsHelpfulMessages() { // This test verifies that error messages are helpful and contain suggestions var testProperties = new List<string> { "mass", "velocity", "drag", "useGravity" }; var suggestions = ComponentResolver.GetAIPropertySuggestions("weight", testProperties); // Even if no perfect match, should return valid list Assert.IsNotNull(suggestions, "Should return valid suggestions list"); // Test with completely invalid input var badSuggestions = ComponentResolver.GetAIPropertySuggestions("xyz123invalid", testProperties); Assert.IsNotNull(badSuggestions, "Should handle invalid input gracefully"); } [Test] public void PerformanceTest_CachingWorks() { var properties = ComponentResolver.GetAllComponentProperties(typeof(Transform)); var input = "Test Property Name"; // First call - populate cache var startTime = System.DateTime.UtcNow; var suggestions1 = ComponentResolver.GetAIPropertySuggestions(input, properties); var firstCallTime = (System.DateTime.UtcNow - startTime).TotalMilliseconds; // Second call - should use cache startTime = System.DateTime.UtcNow; var suggestions2 = ComponentResolver.GetAIPropertySuggestions(input, properties); var secondCallTime = (System.DateTime.UtcNow - startTime).TotalMilliseconds; Assert.AreEqual(suggestions1.Count, suggestions2.Count, "Cached results should be identical"); CollectionAssert.AreEqual(suggestions1, suggestions2, "Cached results should match exactly"); // Second call should be faster (though this test might be flaky) Assert.LessOrEqual(secondCallTime, firstCallTime * 2, "Cached call should not be significantly slower"); } [Test] public void SetComponentProperties_CollectsAllFailuresAndAppliesValidOnes() { // Arrange - add Transform and Rigidbody components to test with var transform = testGameObject.transform; var rigidbody = testGameObject.AddComponent<Rigidbody>(); // Create a params object with mixed valid and invalid properties var setPropertiesParams = new JObject { ["action"] = "modify", ["target"] = testGameObject.name, ["search_method"] = "by_name", ["componentProperties"] = new JObject { ["Transform"] = new JObject { ["localPosition"] = new JObject { ["x"] = 1.0f, ["y"] = 2.0f, ["z"] = 3.0f }, // Valid ["rotatoin"] = new JObject { ["x"] = 0.0f, ["y"] = 90.0f, ["z"] = 0.0f }, // Invalid (typo - should be rotation) ["localScale"] = new JObject { ["x"] = 2.0f, ["y"] = 2.0f, ["z"] = 2.0f } // Valid }, ["Rigidbody"] = new JObject { ["mass"] = 5.0f, // Valid ["invalidProp"] = "test", // Invalid - doesn't exist ["useGravity"] = true // Valid } } }; // Store original values to verify changes var originalLocalPosition = transform.localPosition; var originalLocalScale = transform.localScale; var originalMass = rigidbody.mass; var originalUseGravity = rigidbody.useGravity; Debug.Log($"BEFORE TEST - Mass: {rigidbody.mass}, UseGravity: {rigidbody.useGravity}"); // Expect the warning logs from the invalid properties LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex("Property 'rotatoin' not found")); LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex("Property 'invalidProp' not found")); // Act var result = ManageGameObject.HandleCommand(setPropertiesParams); Debug.Log($"AFTER TEST - Mass: {rigidbody.mass}, UseGravity: {rigidbody.useGravity}"); Debug.Log($"AFTER TEST - LocalPosition: {transform.localPosition}"); Debug.Log($"AFTER TEST - LocalScale: {transform.localScale}"); // Assert - verify that valid properties were set despite invalid ones Assert.AreEqual(new Vector3(1.0f, 2.0f, 3.0f), transform.localPosition, "Valid localPosition should be set even with other invalid properties"); Assert.AreEqual(new Vector3(2.0f, 2.0f, 2.0f), transform.localScale, "Valid localScale should be set even with other invalid properties"); Assert.AreEqual(5.0f, rigidbody.mass, 0.001f, "Valid mass should be set even with other invalid properties"); Assert.AreEqual(true, rigidbody.useGravity, "Valid useGravity should be set even with other invalid properties"); // Verify the result indicates errors (since we had invalid properties) Assert.IsNotNull(result, "Should return a result object"); // The collect-and-continue behavior means we should get an error response // that contains info about the failed properties, but valid ones were still applied // This proves the collect-and-continue behavior is working // Harden: verify structured error response with failures list contains both invalid fields var successProp = result.GetType().GetProperty("success"); Assert.IsNotNull(successProp, "Result should expose 'success' property"); Assert.IsFalse((bool)successProp.GetValue(result), "Result.success should be false for partial failure"); var dataProp = result.GetType().GetProperty("data"); Assert.IsNotNull(dataProp, "Result should include 'data' with errors"); var dataVal = dataProp.GetValue(result); Assert.IsNotNull(dataVal, "Result.data should not be null"); var errorsProp = dataVal.GetType().GetProperty("errors"); Assert.IsNotNull(errorsProp, "Result.data should include 'errors' list"); var errorsEnum = errorsProp.GetValue(dataVal) as System.Collections.IEnumerable; Assert.IsNotNull(errorsEnum, "errors should be enumerable"); bool foundRotatoin = false; bool foundInvalidProp = false; foreach (var err in errorsEnum) { string s = err?.ToString() ?? string.Empty; if (s.Contains("rotatoin")) foundRotatoin = true; if (s.Contains("invalidProp")) foundInvalidProp = true; } Assert.IsTrue(foundRotatoin, "errors should mention the misspelled 'rotatoin' property"); Assert.IsTrue(foundInvalidProp, "errors should mention the 'invalidProp' property"); } [Test] public void SetComponentProperties_ContinuesAfterException() { // Arrange - create scenario that might cause exceptions var rigidbody = testGameObject.AddComponent<Rigidbody>(); // Set initial values that we'll change rigidbody.mass = 1.0f; rigidbody.useGravity = true; var setPropertiesParams = new JObject { ["action"] = "modify", ["target"] = testGameObject.name, ["search_method"] = "by_name", ["componentProperties"] = new JObject { ["Rigidbody"] = new JObject { ["mass"] = 2.5f, // Valid - should be set ["velocity"] = "invalid_type", // Invalid type - will cause exception ["useGravity"] = false // Valid - should still be set after exception } } }; // Expect the error logs from the invalid property LogAssert.Expect(LogType.Error, new System.Text.RegularExpressions.Regex("Unexpected error converting token to UnityEngine.Vector3")); LogAssert.Expect(LogType.Error, new System.Text.RegularExpressions.Regex("SetProperty.*Failed to set 'velocity'")); LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex("Property 'velocity' not found")); // Act var result = ManageGameObject.HandleCommand(setPropertiesParams); // Assert - verify that valid properties before AND after the exception were still set Assert.AreEqual(2.5f, rigidbody.mass, 0.001f, "Mass should be set even if later property causes exception"); Assert.AreEqual(false, rigidbody.useGravity, "UseGravity should be set even if previous property caused exception"); Assert.IsNotNull(result, "Should return a result even with exceptions"); // The key test: processing continued after the exception and set useGravity // This proves the collect-and-continue behavior works even with exceptions // Harden: verify structured error response contains velocity failure var successProp2 = result.GetType().GetProperty("success"); Assert.IsNotNull(successProp2, "Result should expose 'success' property"); Assert.IsFalse((bool)successProp2.GetValue(result), "Result.success should be false when an exception occurs for a property"); var dataProp2 = result.GetType().GetProperty("data"); Assert.IsNotNull(dataProp2, "Result should include 'data' with errors"); var dataVal2 = dataProp2.GetValue(result); Assert.IsNotNull(dataVal2, "Result.data should not be null"); var errorsProp2 = dataVal2.GetType().GetProperty("errors"); Assert.IsNotNull(errorsProp2, "Result.data should include 'errors' list"); var errorsEnum2 = errorsProp2.GetValue(dataVal2) as System.Collections.IEnumerable; Assert.IsNotNull(errorsEnum2, "errors should be enumerable"); bool foundVelocityError = false; foreach (var err in errorsEnum2) { string s = err?.ToString() ?? string.Empty; if (s.Contains("velocity")) { foundVelocityError = true; break; } } Assert.IsTrue(foundVelocityError, "errors should include a message referencing 'velocity'"); } [Test] public void GetComponentData_DoesNotInstantiateMaterialsInEditMode() { // Arrange - Create a GameObject with MeshRenderer and MeshFilter components var testObject = new GameObject("MaterialMeshTestObject"); var meshRenderer = testObject.AddComponent<MeshRenderer>(); var meshFilter = testObject.AddComponent<MeshFilter>(); // Create a simple material and mesh for testing var testMaterial = new Material(Shader.Find("Standard")); var tempCube = GameObject.CreatePrimitive(PrimitiveType.Cube); var testMesh = tempCube.GetComponent<MeshFilter>().sharedMesh; UnityEngine.Object.DestroyImmediate(tempCube); // Set the shared material and mesh (these should be used in edit mode) meshRenderer.sharedMaterial = testMaterial; meshFilter.sharedMesh = testMesh; // Act - Get component data which should trigger material/mesh property access var prevIgnore = LogAssert.ignoreFailingMessages; LogAssert.ignoreFailingMessages = true; // Avoid failing due to incidental editor logs during reflection object result; try { result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshRenderer); } finally { LogAssert.ignoreFailingMessages = prevIgnore; } // Assert - Basic success and shape tolerance Assert.IsNotNull(result, "GetComponentData should return a result"); if (result is Dictionary<string, object> dict && dict.TryGetValue("properties", out var propsObj) && propsObj is Dictionary<string, object> properties) { Assert.IsTrue(properties.ContainsKey("material") || properties.ContainsKey("sharedMaterial") || properties.ContainsKey("materials") || properties.ContainsKey("sharedMaterials"), "Serialized data should include a material-related key when present."); } // Clean up UnityEngine.Object.DestroyImmediate(testMaterial); UnityEngine.Object.DestroyImmediate(testObject); } [Test] public void GetComponentData_DoesNotInstantiateMeshesInEditMode() { // Arrange - Create a GameObject with MeshFilter component var testObject = new GameObject("MeshTestObject"); var meshFilter = testObject.AddComponent<MeshFilter>(); // Create a simple mesh for testing var tempSphere = GameObject.CreatePrimitive(PrimitiveType.Sphere); var testMesh = tempSphere.GetComponent<MeshFilter>().sharedMesh; UnityEngine.Object.DestroyImmediate(tempSphere); meshFilter.sharedMesh = testMesh; // Act - Get component data which should trigger mesh property access var prevIgnore2 = LogAssert.ignoreFailingMessages; LogAssert.ignoreFailingMessages = true; object result; try { result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshFilter); } finally { LogAssert.ignoreFailingMessages = prevIgnore2; } // Assert - Basic success and shape tolerance Assert.IsNotNull(result, "GetComponentData should return a result"); if (result is Dictionary<string, object> dict2 && dict2.TryGetValue("properties", out var propsObj2) && propsObj2 is Dictionary<string, object> properties2) { Assert.IsTrue(properties2.ContainsKey("mesh") || properties2.ContainsKey("sharedMesh"), "Serialized data should include a mesh-related key when present."); } // Clean up UnityEngine.Object.DestroyImmediate(testObject); } [Test] public void GetComponentData_UsesSharedMaterialInEditMode() { // Arrange - Create a GameObject with MeshRenderer var testObject = new GameObject("SharedMaterialTestObject"); var meshRenderer = testObject.AddComponent<MeshRenderer>(); // Create a test material var testMaterial = new Material(Shader.Find("Standard")); testMaterial.name = "TestMaterial"; meshRenderer.sharedMaterial = testMaterial; // Act - Get component data in edit mode var result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshRenderer); // Assert - Verify that the material property was accessed without instantiation Assert.IsNotNull(result, "GetComponentData should return a result"); // Check that result is a dictionary with properties key if (result is Dictionary<string, object> resultDict && resultDict.TryGetValue("properties", out var propertiesObj) && propertiesObj is Dictionary<string, object> properties) { Assert.IsTrue(properties.ContainsKey("material") || properties.ContainsKey("sharedMaterial"), "Serialized data should include 'material' or 'sharedMaterial' when present."); } // Clean up UnityEngine.Object.DestroyImmediate(testMaterial); UnityEngine.Object.DestroyImmediate(testObject); } [Test] public void GetComponentData_UsesSharedMeshInEditMode() { // Arrange - Create a GameObject with MeshFilter var testObject = new GameObject("SharedMeshTestObject"); var meshFilter = testObject.AddComponent<MeshFilter>(); // Create a test mesh var tempCylinder = GameObject.CreatePrimitive(PrimitiveType.Cylinder); var testMesh = tempCylinder.GetComponent<MeshFilter>().sharedMesh; UnityEngine.Object.DestroyImmediate(tempCylinder); testMesh.name = "TestMesh"; meshFilter.sharedMesh = testMesh; // Act - Get component data in edit mode var result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshFilter); // Assert - Verify that the mesh property was accessed without instantiation Assert.IsNotNull(result, "GetComponentData should return a result"); // Check that result is a dictionary with properties key if (result is Dictionary<string, object> resultDict && resultDict.TryGetValue("properties", out var propertiesObj) && propertiesObj is Dictionary<string, object> properties) { Assert.IsTrue(properties.ContainsKey("mesh") || properties.ContainsKey("sharedMesh"), "Serialized data should include 'mesh' or 'sharedMesh' when present."); } // Clean up UnityEngine.Object.DestroyImmediate(testObject); } [Test] public void GetComponentData_HandlesNullMaterialsAndMeshes() { // Arrange - Create a GameObject with MeshRenderer and MeshFilter but no materials/meshes var testObject = new GameObject("NullMaterialMeshTestObject"); var meshRenderer = testObject.AddComponent<MeshRenderer>(); var meshFilter = testObject.AddComponent<MeshFilter>(); // Don't set any materials or meshes - they should be null // Act - Get component data var rendererResult = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshRenderer); var meshFilterResult = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshFilter); // Assert - Verify that the operations succeeded even with null materials/meshes Assert.IsNotNull(rendererResult, "GetComponentData should handle null materials"); Assert.IsNotNull(meshFilterResult, "GetComponentData should handle null meshes"); // Clean up UnityEngine.Object.DestroyImmediate(testObject); } [Test] public void GetComponentData_WorksWithMultipleMaterials() { // Arrange - Create a GameObject with MeshRenderer that has multiple materials var testObject = new GameObject("MultiMaterialTestObject"); var meshRenderer = testObject.AddComponent<MeshRenderer>(); // Create multiple test materials var material1 = new Material(Shader.Find("Standard")); material1.name = "TestMaterial1"; var material2 = new Material(Shader.Find("Standard")); material2.name = "TestMaterial2"; meshRenderer.sharedMaterials = new Material[] { material1, material2 }; // Act - Get component data var result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshRenderer); // Assert - Verify that the operation succeeded with multiple materials Assert.IsNotNull(result, "GetComponentData should handle multiple materials"); // Clean up UnityEngine.Object.DestroyImmediate(material1); UnityEngine.Object.DestroyImmediate(material2); UnityEngine.Object.DestroyImmediate(testObject); } } } ``` -------------------------------------------------------------------------------- /MCPForUnity/Editor/Helpers/GameObjectSerializer.cs: -------------------------------------------------------------------------------- ```csharp using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; using MCPForUnity.Runtime.Serialization; // For Converters namespace MCPForUnity.Editor.Helpers { /// <summary> /// Handles serialization of GameObjects and Components for MCP responses. /// Includes reflection helpers and caching for performance. /// </summary> public static class GameObjectSerializer { // --- Data Serialization --- /// <summary> /// Creates a serializable representation of a GameObject. /// </summary> public static object GetGameObjectData(GameObject go) { if (go == null) return null; return new { name = go.name, instanceID = go.GetInstanceID(), tag = go.tag, layer = go.layer, activeSelf = go.activeSelf, activeInHierarchy = go.activeInHierarchy, isStatic = go.isStatic, scenePath = go.scene.path, // Identify which scene it belongs to transform = new // Serialize transform components carefully to avoid JSON issues { // Serialize Vector3 components individually to prevent self-referencing loops. // The default serializer can struggle with properties like Vector3.normalized. position = new { x = go.transform.position.x, y = go.transform.position.y, z = go.transform.position.z, }, localPosition = new { x = go.transform.localPosition.x, y = go.transform.localPosition.y, z = go.transform.localPosition.z, }, rotation = new { x = go.transform.rotation.eulerAngles.x, y = go.transform.rotation.eulerAngles.y, z = go.transform.rotation.eulerAngles.z, }, localRotation = new { x = go.transform.localRotation.eulerAngles.x, y = go.transform.localRotation.eulerAngles.y, z = go.transform.localRotation.eulerAngles.z, }, scale = new { x = go.transform.localScale.x, y = go.transform.localScale.y, z = go.transform.localScale.z, }, forward = new { x = go.transform.forward.x, y = go.transform.forward.y, z = go.transform.forward.z, }, up = new { x = go.transform.up.x, y = go.transform.up.y, z = go.transform.up.z, }, right = new { x = go.transform.right.x, y = go.transform.right.y, z = go.transform.right.z, }, }, parentInstanceID = go.transform.parent?.gameObject.GetInstanceID() ?? 0, // 0 if no parent // Optionally include components, but can be large // components = go.GetComponents<Component>().Select(c => GetComponentData(c)).ToList() // Or just component names: componentNames = go.GetComponents<Component>() .Select(c => c.GetType().FullName) .ToList(), }; } // --- Metadata Caching for Reflection --- private class CachedMetadata { public readonly List<PropertyInfo> SerializableProperties; public readonly List<FieldInfo> SerializableFields; public CachedMetadata(List<PropertyInfo> properties, List<FieldInfo> fields) { SerializableProperties = properties; SerializableFields = fields; } } // Key becomes Tuple<Type, bool> private static readonly Dictionary<Tuple<Type, bool>, CachedMetadata> _metadataCache = new Dictionary<Tuple<Type, bool>, CachedMetadata>(); // --- End Metadata Caching --- /// <summary> /// Creates a serializable representation of a Component, attempting to serialize /// public properties and fields using reflection, with caching and control over non-public fields. /// </summary> // Add the flag parameter here public static object GetComponentData(Component c, bool includeNonPublicSerializedFields = true) { // --- Add Early Logging --- // Debug.Log($"[GetComponentData] Starting for component: {c?.GetType()?.FullName ?? "null"} (ID: {c?.GetInstanceID() ?? 0})"); // --- End Early Logging --- if (c == null) return null; Type componentType = c.GetType(); // --- Special handling for Transform to avoid reflection crashes and problematic properties --- if (componentType == typeof(Transform)) { Transform tr = c as Transform; // Debug.Log($"[GetComponentData] Manually serializing Transform (ID: {tr.GetInstanceID()})"); return new Dictionary<string, object> { { "typeName", componentType.FullName }, { "instanceID", tr.GetInstanceID() }, // Manually extract known-safe properties. Avoid Quaternion 'rotation' and 'lossyScale'. { "position", CreateTokenFromValue(tr.position, typeof(Vector3))?.ToObject<object>() ?? new JObject() }, { "localPosition", CreateTokenFromValue(tr.localPosition, typeof(Vector3))?.ToObject<object>() ?? new JObject() }, { "eulerAngles", CreateTokenFromValue(tr.eulerAngles, typeof(Vector3))?.ToObject<object>() ?? new JObject() }, // Use Euler angles { "localEulerAngles", CreateTokenFromValue(tr.localEulerAngles, typeof(Vector3))?.ToObject<object>() ?? new JObject() }, { "localScale", CreateTokenFromValue(tr.localScale, typeof(Vector3))?.ToObject<object>() ?? new JObject() }, { "right", CreateTokenFromValue(tr.right, typeof(Vector3))?.ToObject<object>() ?? new JObject() }, { "up", CreateTokenFromValue(tr.up, typeof(Vector3))?.ToObject<object>() ?? new JObject() }, { "forward", CreateTokenFromValue(tr.forward, typeof(Vector3))?.ToObject<object>() ?? new JObject() }, { "parentInstanceID", tr.parent?.gameObject.GetInstanceID() ?? 0 }, { "rootInstanceID", tr.root?.gameObject.GetInstanceID() ?? 0 }, { "childCount", tr.childCount }, // Include standard Object/Component properties { "name", tr.name }, { "tag", tr.tag }, { "gameObjectInstanceID", tr.gameObject?.GetInstanceID() ?? 0 } }; } // --- End Special handling for Transform --- // --- Special handling for Camera to avoid matrix-related crashes --- if (componentType == typeof(Camera)) { Camera cam = c as Camera; var cameraProperties = new Dictionary<string, object>(); // List of safe properties to serialize var safeProperties = new Dictionary<string, Func<object>> { { "nearClipPlane", () => cam.nearClipPlane }, { "farClipPlane", () => cam.farClipPlane }, { "fieldOfView", () => cam.fieldOfView }, { "renderingPath", () => (int)cam.renderingPath }, { "actualRenderingPath", () => (int)cam.actualRenderingPath }, { "allowHDR", () => cam.allowHDR }, { "allowMSAA", () => cam.allowMSAA }, { "allowDynamicResolution", () => cam.allowDynamicResolution }, { "forceIntoRenderTexture", () => cam.forceIntoRenderTexture }, { "orthographicSize", () => cam.orthographicSize }, { "orthographic", () => cam.orthographic }, { "opaqueSortMode", () => (int)cam.opaqueSortMode }, { "transparencySortMode", () => (int)cam.transparencySortMode }, { "depth", () => cam.depth }, { "aspect", () => cam.aspect }, { "cullingMask", () => cam.cullingMask }, { "eventMask", () => cam.eventMask }, { "backgroundColor", () => cam.backgroundColor }, { "clearFlags", () => (int)cam.clearFlags }, { "stereoEnabled", () => cam.stereoEnabled }, { "stereoSeparation", () => cam.stereoSeparation }, { "stereoConvergence", () => cam.stereoConvergence }, { "enabled", () => cam.enabled }, { "name", () => cam.name }, { "tag", () => cam.tag }, { "gameObject", () => new { name = cam.gameObject.name, instanceID = cam.gameObject.GetInstanceID() } } }; foreach (var prop in safeProperties) { try { var value = prop.Value(); if (value != null) { AddSerializableValue(cameraProperties, prop.Key, value.GetType(), value); } } catch (Exception) { // Silently skip any property that fails continue; } } return new Dictionary<string, object> { { "typeName", componentType.FullName }, { "instanceID", cam.GetInstanceID() }, { "properties", cameraProperties } }; } // --- End Special handling for Camera --- var data = new Dictionary<string, object> { { "typeName", componentType.FullName }, { "instanceID", c.GetInstanceID() } }; // --- Get Cached or Generate Metadata (using new cache key) --- Tuple<Type, bool> cacheKey = new Tuple<Type, bool>(componentType, includeNonPublicSerializedFields); if (!_metadataCache.TryGetValue(cacheKey, out CachedMetadata cachedData)) { var propertiesToCache = new List<PropertyInfo>(); var fieldsToCache = new List<FieldInfo>(); // Traverse the hierarchy from the component type up to MonoBehaviour Type currentType = componentType; while (currentType != null && currentType != typeof(MonoBehaviour) && currentType != typeof(object)) { // Get properties declared only at the current type level BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; foreach (var propInfo in currentType.GetProperties(propFlags)) { // Basic filtering (readable, not indexer, not transform which is handled elsewhere) if (!propInfo.CanRead || propInfo.GetIndexParameters().Length > 0 || propInfo.Name == "transform") continue; // Add if not already added (handles overrides - keep the most derived version) if (!propertiesToCache.Any(p => p.Name == propInfo.Name)) { propertiesToCache.Add(propInfo); } } // Get fields declared only at the current type level (both public and non-public) BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly; var declaredFields = currentType.GetFields(fieldFlags); // Process the declared Fields for caching foreach (var fieldInfo in declaredFields) { if (fieldInfo.Name.EndsWith("k__BackingField")) continue; // Skip backing fields // Add if not already added (handles hiding - keep the most derived version) if (fieldsToCache.Any(f => f.Name == fieldInfo.Name)) continue; bool shouldInclude = false; if (includeNonPublicSerializedFields) { // If TRUE, include Public OR any NonPublic with [SerializeField] (private/protected/internal) var hasSerializeField = fieldInfo.IsDefined(typeof(SerializeField), inherit: true); shouldInclude = fieldInfo.IsPublic || (!fieldInfo.IsPublic && hasSerializeField); } else // includeNonPublicSerializedFields is FALSE { // If FALSE, include ONLY if it is explicitly Public. shouldInclude = fieldInfo.IsPublic; } if (shouldInclude) { fieldsToCache.Add(fieldInfo); } } // Move to the base type currentType = currentType.BaseType; } // --- End Hierarchy Traversal --- cachedData = new CachedMetadata(propertiesToCache, fieldsToCache); _metadataCache[cacheKey] = cachedData; // Add to cache with combined key } // --- End Get Cached or Generate Metadata --- // --- Use cached metadata --- var serializablePropertiesOutput = new Dictionary<string, object>(); // --- Add Logging Before Property Loop --- // Debug.Log($"[GetComponentData] Starting property loop for {componentType.Name}..."); // --- End Logging Before Property Loop --- // Use cached properties foreach (var propInfo in cachedData.SerializableProperties) { string propName = propInfo.Name; // --- Skip known obsolete/problematic Component shortcut properties --- bool skipProperty = false; if (propName == "rigidbody" || propName == "rigidbody2D" || propName == "camera" || propName == "light" || propName == "animation" || propName == "constantForce" || propName == "renderer" || propName == "audio" || propName == "networkView" || propName == "collider" || propName == "collider2D" || propName == "hingeJoint" || propName == "particleSystem" || // Also skip potentially problematic Matrix properties prone to cycles/errors propName == "worldToLocalMatrix" || propName == "localToWorldMatrix") { // Debug.Log($"[GetComponentData] Explicitly skipping generic property: {propName}"); // Optional log skipProperty = true; } // --- End Skip Generic Properties --- // --- Skip specific potentially problematic Camera properties --- if (componentType == typeof(Camera) && (propName == "pixelRect" || propName == "rect" || propName == "cullingMatrix" || propName == "useOcclusionCulling" || propName == "worldToCameraMatrix" || propName == "projectionMatrix" || propName == "nonJitteredProjectionMatrix" || propName == "previousViewProjectionMatrix" || propName == "cameraToWorldMatrix")) { // Debug.Log($"[GetComponentData] Explicitly skipping Camera property: {propName}"); skipProperty = true; } // --- End Skip Camera Properties --- // --- Skip specific potentially problematic Transform properties --- if (componentType == typeof(Transform) && (propName == "lossyScale" || propName == "rotation" || propName == "worldToLocalMatrix" || propName == "localToWorldMatrix")) { // Debug.Log($"[GetComponentData] Explicitly skipping Transform property: {propName}"); skipProperty = true; } // --- End Skip Transform Properties --- // Skip if flagged if (skipProperty) { continue; } try { // --- Add detailed logging --- // Debug.Log($"[GetComponentData] Accessing: {componentType.Name}.{propName}"); // --- End detailed logging --- // --- Special handling for material/mesh properties in edit mode --- object value; if (!Application.isPlaying && (propName == "material" || propName == "materials" || propName == "mesh")) { // In edit mode, use sharedMaterial/sharedMesh to avoid instantiation warnings if ((propName == "material" || propName == "materials") && c is Renderer renderer) { if (propName == "material") value = renderer.sharedMaterial; else // materials value = renderer.sharedMaterials; } else if (propName == "mesh" && c is MeshFilter meshFilter) { value = meshFilter.sharedMesh; } else { // Fallback to normal property access if type doesn't match value = propInfo.GetValue(c); } } else { value = propInfo.GetValue(c); } // --- End special handling --- Type propType = propInfo.PropertyType; AddSerializableValue(serializablePropertiesOutput, propName, propType, value); } catch (Exception) { // Debug.LogWarning($"Could not read property {propName} on {componentType.Name}"); } } // --- Add Logging Before Field Loop --- // Debug.Log($"[GetComponentData] Starting field loop for {componentType.Name}..."); // --- End Logging Before Field Loop --- // Use cached fields foreach (var fieldInfo in cachedData.SerializableFields) { try { // --- Add detailed logging for fields --- // Debug.Log($"[GetComponentData] Accessing Field: {componentType.Name}.{fieldInfo.Name}"); // --- End detailed logging for fields --- object value = fieldInfo.GetValue(c); string fieldName = fieldInfo.Name; Type fieldType = fieldInfo.FieldType; AddSerializableValue(serializablePropertiesOutput, fieldName, fieldType, value); } catch (Exception) { // Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}"); } } // --- End Use cached metadata --- if (serializablePropertiesOutput.Count > 0) { data["properties"] = serializablePropertiesOutput; } return data; } // Helper function to decide how to serialize different types private static void AddSerializableValue(Dictionary<string, object> dict, string name, Type type, object value) { // Simplified: Directly use CreateTokenFromValue which uses the serializer if (value == null) { dict[name] = null; return; } try { // Use the helper that employs our custom serializer settings JToken token = CreateTokenFromValue(value, type); if (token != null) // Check if serialization succeeded in the helper { // Convert JToken back to a basic object structure for the dictionary dict[name] = ConvertJTokenToPlainObject(token); } // If token is null, it means serialization failed and a warning was logged. } catch (Exception e) { // Catch potential errors during JToken conversion or addition to dictionary Debug.LogWarning($"[AddSerializableValue] Error processing value for '{name}' (Type: {type.FullName}): {e.Message}. Skipping."); } } // Helper to convert JToken back to basic object structure private static object ConvertJTokenToPlainObject(JToken token) { if (token == null) return null; switch (token.Type) { case JTokenType.Object: var objDict = new Dictionary<string, object>(); foreach (var prop in ((JObject)token).Properties()) { objDict[prop.Name] = ConvertJTokenToPlainObject(prop.Value); } return objDict; case JTokenType.Array: var list = new List<object>(); foreach (var item in (JArray)token) { list.Add(ConvertJTokenToPlainObject(item)); } return list; case JTokenType.Integer: return token.ToObject<long>(); // Use long for safety case JTokenType.Float: return token.ToObject<double>(); // Use double for safety case JTokenType.String: return token.ToObject<string>(); case JTokenType.Boolean: return token.ToObject<bool>(); case JTokenType.Date: return token.ToObject<DateTime>(); case JTokenType.Guid: return token.ToObject<Guid>(); case JTokenType.Uri: return token.ToObject<Uri>(); case JTokenType.TimeSpan: return token.ToObject<TimeSpan>(); case JTokenType.Bytes: return token.ToObject<byte[]>(); case JTokenType.Null: return null; case JTokenType.Undefined: return null; // Treat undefined as null default: // Fallback for simple value types not explicitly listed if (token is JValue jValue && jValue.Value != null) { return jValue.Value; } // Debug.LogWarning($"Unsupported JTokenType encountered: {token.Type}. Returning null."); return null; } } // --- Define custom JsonSerializerSettings for OUTPUT --- private static readonly JsonSerializerSettings _outputSerializerSettings = new JsonSerializerSettings { Converters = new List<JsonConverter> { new Vector3Converter(), new Vector2Converter(), new QuaternionConverter(), new ColorConverter(), new RectConverter(), new BoundsConverter(), new UnityEngineObjectConverter() // Handles serialization of references }, ReferenceLoopHandling = ReferenceLoopHandling.Ignore, // ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() } // Example if needed }; private static readonly JsonSerializer _outputSerializer = JsonSerializer.Create(_outputSerializerSettings); // --- End Define custom JsonSerializerSettings --- // Helper to create JToken using the output serializer private static JToken CreateTokenFromValue(object value, Type type) { if (value == null) return JValue.CreateNull(); try { // Use the pre-configured OUTPUT serializer instance return JToken.FromObject(value, _outputSerializer); } catch (JsonSerializationException e) { Debug.LogWarning($"[GameObjectSerializer] Newtonsoft.Json Error serializing value of type {type.FullName}: {e.Message}. Skipping property/field."); return null; // Indicate serialization failure } catch (Exception e) // Catch other unexpected errors { Debug.LogWarning($"[GameObjectSerializer] Unexpected error serializing value of type {type.FullName}: {e}. Skipping property/field."); return null; // Indicate serialization failure } } } } ``` -------------------------------------------------------------------------------- /UnityMcpBridge/Editor/Setup/SetupWizardWindow.cs: -------------------------------------------------------------------------------- ```csharp using System; using System.Linq; using MCPForUnity.Editor.Data; using MCPForUnity.Editor.Dependencies; using MCPForUnity.Editor.Dependencies.Models; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Models; using UnityEditor; using UnityEngine; namespace MCPForUnity.Editor.Setup { /// <summary> /// Setup wizard window for guiding users through dependency installation /// </summary> public class SetupWizardWindow : EditorWindow { private DependencyCheckResult _dependencyResult; private Vector2 _scrollPosition; private int _currentStep = 0; private McpClients _mcpClients; private int _selectedClientIndex = 0; private readonly string[] _stepTitles = { "Setup", "Configure", "Complete" }; public static void ShowWindow(DependencyCheckResult dependencyResult = null) { var window = GetWindow<SetupWizardWindow>("MCP for Unity Setup"); window.minSize = new Vector2(500, 400); window.maxSize = new Vector2(800, 600); window._dependencyResult = dependencyResult ?? DependencyManager.CheckAllDependencies(); window.Show(); } private void OnEnable() { if (_dependencyResult == null) { _dependencyResult = DependencyManager.CheckAllDependencies(); } _mcpClients = new McpClients(); // Check client configurations on startup foreach (var client in _mcpClients.clients) { CheckClientConfiguration(client); } } private void OnGUI() { DrawHeader(); DrawProgressBar(); _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition); switch (_currentStep) { case 0: DrawSetupStep(); break; case 1: DrawConfigureStep(); break; case 2: DrawCompleteStep(); break; } EditorGUILayout.EndScrollView(); DrawFooter(); } private void DrawHeader() { EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); GUILayout.Label("MCP for Unity Setup Wizard", EditorStyles.boldLabel); GUILayout.FlexibleSpace(); GUILayout.Label($"Step {_currentStep + 1} of {_stepTitles.Length}"); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); // Step title var titleStyle = new GUIStyle(EditorStyles.largeLabel) { fontSize = 16, fontStyle = FontStyle.Bold }; EditorGUILayout.LabelField(_stepTitles[_currentStep], titleStyle); EditorGUILayout.Space(); } private void DrawProgressBar() { var rect = EditorGUILayout.GetControlRect(false, 4); var progress = (_currentStep + 1) / (float)_stepTitles.Length; EditorGUI.ProgressBar(rect, progress, ""); EditorGUILayout.Space(); } private void DrawSetupStep() { // Welcome section DrawSectionTitle("MCP for Unity Setup"); EditorGUILayout.LabelField( "This wizard will help you set up MCP for Unity to connect AI assistants with your Unity Editor.", EditorStyles.wordWrappedLabel ); EditorGUILayout.Space(); // Dependency check section EditorGUILayout.BeginHorizontal(); DrawSectionTitle("System Check", 14); GUILayout.FlexibleSpace(); if (GUILayout.Button("Refresh", GUILayout.Width(60), GUILayout.Height(20))) { _dependencyResult = DependencyManager.CheckAllDependencies(); } EditorGUILayout.EndHorizontal(); // Show simplified dependency status foreach (var dep in _dependencyResult.Dependencies) { DrawSimpleDependencyStatus(dep); } // Overall status and installation guidance EditorGUILayout.Space(); if (!_dependencyResult.IsSystemReady) { // Only show critical warnings when dependencies are actually missing EditorGUILayout.HelpBox( "⚠️ Missing Dependencies: MCP for Unity requires Python 3.10+ and UV package manager to function properly.", MessageType.Warning ); EditorGUILayout.Space(); EditorGUILayout.BeginVertical(EditorStyles.helpBox); DrawErrorStatus("Installation Required"); var recommendations = DependencyManager.GetInstallationRecommendations(); EditorGUILayout.LabelField(recommendations, EditorStyles.wordWrappedLabel); EditorGUILayout.Space(); if (GUILayout.Button("Open Installation Links", GUILayout.Height(25))) { OpenInstallationUrls(); } EditorGUILayout.EndVertical(); } else { DrawSuccessStatus("System Ready"); EditorGUILayout.LabelField("All requirements are met. You can proceed to configure your AI clients.", EditorStyles.wordWrappedLabel); } } private void DrawCompleteStep() { DrawSectionTitle("Setup Complete"); // Refresh dependency check with caching to avoid heavy operations on every repaint if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2) { _dependencyResult = DependencyManager.CheckAllDependencies(); } if (_dependencyResult.IsSystemReady) { DrawSuccessStatus("MCP for Unity Ready!"); EditorGUILayout.HelpBox( "🎉 MCP for Unity is now set up and ready to use!\n\n" + "• Dependencies verified\n" + "• MCP server ready\n" + "• Client configuration accessible", MessageType.Info ); EditorGUILayout.Space(); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Documentation", GUILayout.Height(30))) { Application.OpenURL("https://github.com/CoplayDev/unity-mcp"); } if (GUILayout.Button("Client Settings", GUILayout.Height(30))) { Windows.MCPForUnityEditorWindow.ShowWindow(); } EditorGUILayout.EndHorizontal(); } else { DrawErrorStatus("Setup Incomplete - Package Non-Functional"); EditorGUILayout.HelpBox( "🚨 MCP for Unity CANNOT work - dependencies still missing!\n\n" + "Install ALL required dependencies before the package will function.", MessageType.Error ); var missingDeps = _dependencyResult.GetMissingRequired(); if (missingDeps.Count > 0) { EditorGUILayout.Space(); EditorGUILayout.LabelField("Still Missing:", EditorStyles.boldLabel); foreach (var dep in missingDeps) { EditorGUILayout.LabelField($"✗ {dep.Name}", EditorStyles.label); } } EditorGUILayout.Space(); if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30))) { _currentStep = 0; } } } // Helper methods for consistent UI components private void DrawSectionTitle(string title, int fontSize = 16) { var titleStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = fontSize, fontStyle = FontStyle.Bold }; EditorGUILayout.LabelField(title, titleStyle); EditorGUILayout.Space(); } private void DrawSuccessStatus(string message) { var originalColor = GUI.color; GUI.color = Color.green; EditorGUILayout.LabelField($"✓ {message}", EditorStyles.boldLabel); GUI.color = originalColor; EditorGUILayout.Space(); } private void DrawErrorStatus(string message) { var originalColor = GUI.color; GUI.color = Color.red; EditorGUILayout.LabelField($"✗ {message}", EditorStyles.boldLabel); GUI.color = originalColor; EditorGUILayout.Space(); } private void DrawSimpleDependencyStatus(DependencyStatus dep) { EditorGUILayout.BeginHorizontal(); var statusIcon = dep.IsAvailable ? "✓" : "✗"; var statusColor = dep.IsAvailable ? Color.green : Color.red; var originalColor = GUI.color; GUI.color = statusColor; GUILayout.Label(statusIcon, GUILayout.Width(20)); EditorGUILayout.LabelField(dep.Name, EditorStyles.boldLabel); GUI.color = originalColor; if (!dep.IsAvailable && !string.IsNullOrEmpty(dep.ErrorMessage)) { EditorGUILayout.LabelField($"({dep.ErrorMessage})", EditorStyles.miniLabel); } EditorGUILayout.EndHorizontal(); } private void DrawConfigureStep() { DrawSectionTitle("AI Client Configuration"); // Check dependencies first (with caching to avoid heavy operations on every repaint) if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2) { _dependencyResult = DependencyManager.CheckAllDependencies(); } if (!_dependencyResult.IsSystemReady) { DrawErrorStatus("Cannot Configure - System Requirements Not Met"); EditorGUILayout.HelpBox( "Client configuration requires system dependencies to be installed first. Please complete setup before proceeding.", MessageType.Warning ); if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30))) { _currentStep = 0; } return; } EditorGUILayout.LabelField( "Configure your AI assistants to work with Unity. Select a client below to set it up:", EditorStyles.wordWrappedLabel ); EditorGUILayout.Space(); // Client selection and configuration if (_mcpClients.clients.Count > 0) { // Client selector dropdown string[] clientNames = _mcpClients.clients.Select(c => c.name).ToArray(); EditorGUI.BeginChangeCheck(); _selectedClientIndex = EditorGUILayout.Popup("Select AI Client:", _selectedClientIndex, clientNames); if (EditorGUI.EndChangeCheck()) { _selectedClientIndex = Mathf.Clamp(_selectedClientIndex, 0, _mcpClients.clients.Count - 1); // Refresh client status when selection changes CheckClientConfiguration(_mcpClients.clients[_selectedClientIndex]); } EditorGUILayout.Space(); var selectedClient = _mcpClients.clients[_selectedClientIndex]; DrawClientConfigurationInWizard(selectedClient); EditorGUILayout.Space(); // Batch configuration option EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.LabelField("Quick Setup", EditorStyles.boldLabel); EditorGUILayout.LabelField( "Automatically configure all detected AI clients at once:", EditorStyles.wordWrappedLabel ); EditorGUILayout.Space(); if (GUILayout.Button("Configure All Detected Clients", GUILayout.Height(30))) { ConfigureAllClientsInWizard(); } EditorGUILayout.EndVertical(); } else { EditorGUILayout.HelpBox("No AI clients detected. Make sure you have Claude Code, Cursor, or VSCode installed.", MessageType.Info); } EditorGUILayout.Space(); EditorGUILayout.HelpBox( "💡 You might need to restart your AI client after configuring.", MessageType.Info ); } private void DrawFooter() { EditorGUILayout.Space(); EditorGUILayout.BeginHorizontal(); // Back button GUI.enabled = _currentStep > 0; if (GUILayout.Button("Back", GUILayout.Width(60))) { _currentStep--; } GUILayout.FlexibleSpace(); // Skip button if (GUILayout.Button("Skip", GUILayout.Width(60))) { bool dismiss = EditorUtility.DisplayDialog( "Skip Setup", "⚠️ Skipping setup will leave MCP for Unity non-functional!\n\n" + "You can restart setup from: Window > MCP for Unity > Setup Wizard (Required)", "Skip Anyway", "Cancel" ); if (dismiss) { SetupWizard.MarkSetupDismissed(); Close(); } } // Next/Done button GUI.enabled = true; string buttonText = _currentStep == _stepTitles.Length - 1 ? "Done" : "Next"; if (GUILayout.Button(buttonText, GUILayout.Width(80))) { if (_currentStep == _stepTitles.Length - 1) { SetupWizard.MarkSetupCompleted(); Close(); } else { _currentStep++; } } GUI.enabled = true; EditorGUILayout.EndHorizontal(); } private void DrawClientConfigurationInWizard(McpClient client) { EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.LabelField($"{client.name} Configuration", EditorStyles.boldLabel); EditorGUILayout.Space(); // Show current status var statusColor = GetClientStatusColor(client); var originalColor = GUI.color; GUI.color = statusColor; EditorGUILayout.LabelField($"Status: {client.configStatus}", EditorStyles.label); GUI.color = originalColor; EditorGUILayout.Space(); // Configuration buttons EditorGUILayout.BeginHorizontal(); if (client.mcpType == McpTypes.ClaudeCode) { // Special handling for Claude Code bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude()); if (claudeAvailable) { bool isConfigured = client.status == McpStatus.Configured; string buttonText = isConfigured ? "Unregister" : "Register"; if (GUILayout.Button($"{buttonText} with Claude Code")) { if (isConfigured) { UnregisterFromClaudeCode(client); } else { RegisterWithClaudeCode(client); } } } else { EditorGUILayout.HelpBox("Claude Code not found. Please install Claude Code first.", MessageType.Warning); if (GUILayout.Button("Open Claude Code Website")) { Application.OpenURL("https://claude.ai/download"); } } } else { // Standard client configuration if (GUILayout.Button($"Configure {client.name}")) { ConfigureClientInWizard(client); } if (GUILayout.Button("Manual Setup")) { ShowManualSetupInWizard(client); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); } private Color GetClientStatusColor(McpClient client) { return client.status switch { McpStatus.Configured => Color.green, McpStatus.Running => Color.green, McpStatus.Connected => Color.green, McpStatus.IncorrectPath => Color.yellow, McpStatus.CommunicationError => Color.yellow, McpStatus.NoResponse => Color.yellow, _ => Color.red }; } private void ConfigureClientInWizard(McpClient client) { try { string result = PerformClientConfiguration(client); EditorUtility.DisplayDialog( $"{client.name} Configuration", result, "OK" ); // Refresh client status CheckClientConfiguration(client); Repaint(); } catch (System.Exception ex) { EditorUtility.DisplayDialog( "Configuration Error", $"Failed to configure {client.name}: {ex.Message}", "OK" ); } } private void ConfigureAllClientsInWizard() { int successCount = 0; int totalCount = _mcpClients.clients.Count; foreach (var client in _mcpClients.clients) { try { if (client.mcpType == McpTypes.ClaudeCode) { if (!string.IsNullOrEmpty(ExecPath.ResolveClaude()) && client.status != McpStatus.Configured) { RegisterWithClaudeCode(client); successCount++; } else if (client.status == McpStatus.Configured) { successCount++; // Already configured } } else { string result = PerformClientConfiguration(client); if (result.Contains("success", System.StringComparison.OrdinalIgnoreCase)) { successCount++; } } CheckClientConfiguration(client); } catch (System.Exception ex) { McpLog.Error($"Failed to configure {client.name}: {ex.Message}"); } } EditorUtility.DisplayDialog( "Batch Configuration Complete", $"Successfully configured {successCount} out of {totalCount} clients.\n\n" + "Restart your AI clients for changes to take effect.", "OK" ); Repaint(); } private void RegisterWithClaudeCode(McpClient client) { try { string pythonDir = McpPathResolver.FindPackagePythonDirectory(); string claudePath = ExecPath.ResolveClaude(); string uvPath = ExecPath.ResolveUv() ?? "uv"; string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py"; if (!ExecPath.TryRun(claudePath, args, null, out var stdout, out var stderr, 15000, McpPathResolver.GetPathPrepend())) { if ((stdout + stderr).Contains("already exists", System.StringComparison.OrdinalIgnoreCase)) { CheckClientConfiguration(client); EditorUtility.DisplayDialog("Claude Code", "MCP for Unity is already registered with Claude Code.", "OK"); } else { throw new System.Exception($"Registration failed: {stderr}"); } } else { CheckClientConfiguration(client); EditorUtility.DisplayDialog("Claude Code", "Successfully registered MCP for Unity with Claude Code!", "OK"); } } catch (System.Exception ex) { EditorUtility.DisplayDialog("Registration Error", $"Failed to register with Claude Code: {ex.Message}", "OK"); } } private void UnregisterFromClaudeCode(McpClient client) { try { string claudePath = ExecPath.ResolveClaude(); if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", null, out var stdout, out var stderr, 10000, McpPathResolver.GetPathPrepend())) { CheckClientConfiguration(client); EditorUtility.DisplayDialog("Claude Code", "Successfully unregistered MCP for Unity from Claude Code.", "OK"); } else { throw new System.Exception($"Unregistration failed: {stderr}"); } } catch (System.Exception ex) { EditorUtility.DisplayDialog("Unregistration Error", $"Failed to unregister from Claude Code: {ex.Message}", "OK"); } } private string PerformClientConfiguration(McpClient client) { // This mirrors the logic from MCPForUnityEditorWindow.ConfigureMcpClient string configPath = McpConfigurationHelper.GetClientConfigPath(client); string pythonDir = McpPathResolver.FindPackagePythonDirectory(); if (string.IsNullOrEmpty(pythonDir)) { return "Manual configuration required - Python server directory not found."; } McpConfigurationHelper.EnsureConfigDirectoryExists(configPath); // Use TOML writer for Codex; JSON writer for others if (client != null && client.mcpType == McpTypes.Codex) { return McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, client); } else { return McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client); } } private void ShowManualSetupInWizard(McpClient client) { string configPath = McpConfigurationHelper.GetClientConfigPath(client); string pythonDir = McpPathResolver.FindPackagePythonDirectory(); string uvPath = ServerInstaller.FindUvPath(); if (string.IsNullOrEmpty(uvPath)) { EditorUtility.DisplayDialog("Manual Setup", "UV package manager not found. Please install UV first.", "OK"); return; } // Build manual configuration using the sophisticated helper logic string result = (client != null && client.mcpType == McpTypes.Codex) ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, client) : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client); string manualConfig; if (result == "Configured successfully") { // Read back the configuration that was written try { manualConfig = System.IO.File.ReadAllText(configPath); } catch { manualConfig = "Configuration written successfully, but could not read back for display."; } } else { manualConfig = $"Configuration failed: {result}"; } EditorUtility.DisplayDialog( $"Manual Setup - {client.name}", $"Configuration file location:\n{configPath}\n\n" + $"Configuration result:\n{manualConfig}", "OK" ); } private void CheckClientConfiguration(McpClient client) { // Basic status check - could be enhanced to mirror MCPForUnityEditorWindow logic try { string configPath = McpConfigurationHelper.GetClientConfigPath(client); if (System.IO.File.Exists(configPath)) { client.configStatus = "Configured"; client.status = McpStatus.Configured; } else { client.configStatus = "Not Configured"; client.status = McpStatus.NotConfigured; } } catch { client.configStatus = "Error"; client.status = McpStatus.Error; } } private void OpenInstallationUrls() { var (pythonUrl, uvUrl) = DependencyManager.GetInstallationUrls(); bool openPython = EditorUtility.DisplayDialog( "Open Installation URLs", "Open Python installation page?", "Yes", "No" ); if (openPython) { Application.OpenURL(pythonUrl); } bool openUV = EditorUtility.DisplayDialog( "Open Installation URLs", "Open UV installation page?", "Yes", "No" ); if (openUV) { Application.OpenURL(uvUrl); } } } } ``` -------------------------------------------------------------------------------- /UnityMcpBridge/Editor/Helpers/ServerInstaller.cs: -------------------------------------------------------------------------------- ```csharp using System; using System.IO; using System.Runtime.InteropServices; using System.Text; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; namespace MCPForUnity.Editor.Helpers { public static class ServerInstaller { private const string RootFolder = "UnityMCP"; private const string ServerFolder = "UnityMcpServer"; private const string VersionFileName = "server_version.txt"; /// <summary> /// Ensures the mcp-for-unity-server is installed locally by copying from the embedded package source. /// No network calls or Git operations are performed. /// </summary> public static void EnsureServerInstalled() { try { string saveLocation = GetSaveLocation(); TryCreateMacSymlinkForAppSupport(); string destRoot = Path.Combine(saveLocation, ServerFolder); string destSrc = Path.Combine(destRoot, "src"); // Detect legacy installs and version state (logs) DetectAndLogLegacyInstallStates(destRoot); // Resolve embedded source and versions if (!TryGetEmbeddedServerSource(out string embeddedSrc)) { throw new Exception("Could not find embedded UnityMcpServer/src in the package."); } string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc, VersionFileName)) ?? "unknown"; string installedVer = ReadVersionFile(Path.Combine(destSrc, VersionFileName)); bool destHasServer = File.Exists(Path.Combine(destSrc, "server.py")); bool needOverwrite = !destHasServer || string.IsNullOrEmpty(installedVer) || (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(installedVer, embeddedVer) < 0); // Ensure destination exists Directory.CreateDirectory(destRoot); if (needOverwrite) { // Copy the entire UnityMcpServer folder (parent of src) string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc; // go up from src to UnityMcpServer CopyDirectoryRecursive(embeddedRoot, destRoot); // Write/refresh version file try { File.WriteAllText(Path.Combine(destSrc, VersionFileName), embeddedVer ?? "unknown"); } catch { } McpLog.Info($"Installed/updated server to {destRoot} (version {embeddedVer})."); } // Cleanup legacy installs that are missing version or older than embedded foreach (var legacyRoot in GetLegacyRootsForDetection()) { try { string legacySrc = Path.Combine(legacyRoot, "src"); if (!File.Exists(Path.Combine(legacySrc, "server.py"))) continue; string legacyVer = ReadVersionFile(Path.Combine(legacySrc, VersionFileName)); bool legacyOlder = string.IsNullOrEmpty(legacyVer) || (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(legacyVer, embeddedVer) < 0); if (legacyOlder) { TryKillUvForPath(legacySrc); try { Directory.Delete(legacyRoot, recursive: true); McpLog.Info($"Removed legacy server at '{legacyRoot}'."); } catch (Exception ex) { McpLog.Warn($"Failed to remove legacy server at '{legacyRoot}': {ex.Message}"); } } } catch { } } // Clear overrides that might point at legacy locations try { EditorPrefs.DeleteKey("MCPForUnity.ServerSrc"); EditorPrefs.DeleteKey("MCPForUnity.PythonDirOverride"); } catch { } return; } catch (Exception ex) { // If a usable server is already present (installed or embedded), don't fail hard—just warn. bool hasInstalled = false; try { hasInstalled = File.Exists(Path.Combine(GetServerPath(), "server.py")); } catch { } if (hasInstalled || TryGetEmbeddedServerSource(out _)) { McpLog.Warn($"Using existing server; skipped install. Details: {ex.Message}"); return; } McpLog.Error($"Failed to ensure server installation: {ex.Message}"); } } public static string GetServerPath() { return Path.Combine(GetSaveLocation(), ServerFolder, "src"); } /// <summary> /// Gets the platform-specific save location for the server. /// </summary> private static string GetSaveLocation() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { // Use per-user LocalApplicationData for canonical install location var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, "AppData", "Local"); return Path.Combine(localAppData, RootFolder); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { var xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); if (string.IsNullOrEmpty(xdg)) { xdg = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, ".local", "share"); } return Path.Combine(xdg, RootFolder); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { // On macOS, use LocalApplicationData (~/Library/Application Support) var localAppSupport = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); // Unity/Mono may map LocalApplicationData to ~/.local/share on macOS; normalize to Application Support bool looksLikeXdg = !string.IsNullOrEmpty(localAppSupport) && localAppSupport.Replace('\\', '/').Contains("/.local/share"); if (string.IsNullOrEmpty(localAppSupport) || looksLikeXdg) { // Fallback: construct from $HOME var home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty; localAppSupport = Path.Combine(home, "Library", "Application Support"); } TryCreateMacSymlinkForAppSupport(); return Path.Combine(localAppSupport, RootFolder); } throw new Exception("Unsupported operating system."); } /// <summary> /// On macOS, create a no-spaces symlink ~/Library/AppSupport -> ~/Library/Application Support /// to mitigate arg parsing and quoting issues in some MCP clients. /// Safe to call repeatedly. /// </summary> private static void TryCreateMacSymlinkForAppSupport() { try { if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return; string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty; if (string.IsNullOrEmpty(home)) return; string canonical = Path.Combine(home, "Library", "Application Support"); string symlink = Path.Combine(home, "Library", "AppSupport"); // If symlink exists already, nothing to do if (Directory.Exists(symlink) || File.Exists(symlink)) return; // Create symlink only if canonical exists if (!Directory.Exists(canonical)) return; // Use 'ln -s' to create a directory symlink (macOS) var psi = new System.Diagnostics.ProcessStartInfo { FileName = "/bin/ln", Arguments = $"-s \"{canonical}\" \"{symlink}\"", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; using var p = System.Diagnostics.Process.Start(psi); p?.WaitForExit(2000); } catch { /* best-effort */ } } private static bool IsDirectoryWritable(string path) { try { File.Create(Path.Combine(path, "test.txt")).Dispose(); File.Delete(Path.Combine(path, "test.txt")); return true; } catch { return false; } } /// <summary> /// Checks if the server is installed at the specified location. /// </summary> private static bool IsServerInstalled(string location) { return Directory.Exists(location) && File.Exists(Path.Combine(location, ServerFolder, "src", "server.py")); } /// <summary> /// Detects legacy installs or older versions and logs findings (no deletion yet). /// </summary> private static void DetectAndLogLegacyInstallStates(string canonicalRoot) { try { string canonicalSrc = Path.Combine(canonicalRoot, "src"); // Normalize canonical root for comparisons string normCanonicalRoot = NormalizePathSafe(canonicalRoot); string embeddedSrc = null; TryGetEmbeddedServerSource(out embeddedSrc); string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc ?? string.Empty, VersionFileName)); string installedVer = ReadVersionFile(Path.Combine(canonicalSrc, VersionFileName)); // Legacy paths (macOS/Linux .config; Windows roaming as example) foreach (var legacyRoot in GetLegacyRootsForDetection()) { // Skip logging for the canonical root itself if (PathsEqualSafe(legacyRoot, normCanonicalRoot)) continue; string legacySrc = Path.Combine(legacyRoot, "src"); bool hasServer = File.Exists(Path.Combine(legacySrc, "server.py")); string legacyVer = ReadVersionFile(Path.Combine(legacySrc, VersionFileName)); if (hasServer) { // Case 1: No version file if (string.IsNullOrEmpty(legacyVer)) { McpLog.Info("Detected legacy install without version file at: " + legacyRoot, always: false); } // Case 2: Lives in legacy path McpLog.Info("Detected legacy install path: " + legacyRoot, always: false); // Case 3: Has version but appears older than embedded if (!string.IsNullOrEmpty(embeddedVer) && !string.IsNullOrEmpty(legacyVer) && CompareSemverSafe(legacyVer, embeddedVer) < 0) { McpLog.Info($"Legacy install version {legacyVer} is older than embedded {embeddedVer}", always: false); } } } // Also log if canonical is missing version (treated as older) if (Directory.Exists(canonicalRoot)) { if (string.IsNullOrEmpty(installedVer)) { McpLog.Info("Canonical install missing version file (treat as older). Path: " + canonicalRoot, always: false); } else if (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(installedVer, embeddedVer) < 0) { McpLog.Info($"Canonical install version {installedVer} is older than embedded {embeddedVer}", always: false); } } } catch (Exception ex) { McpLog.Warn("Detect legacy/version state failed: " + ex.Message); } } private static string NormalizePathSafe(string path) { try { return string.IsNullOrEmpty(path) ? path : Path.GetFullPath(path.Trim()); } catch { return path; } } private static bool PathsEqualSafe(string a, string b) { if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false; string na = NormalizePathSafe(a); string nb = NormalizePathSafe(b); try { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { return string.Equals(na, nb, StringComparison.OrdinalIgnoreCase); } return string.Equals(na, nb, StringComparison.Ordinal); } catch { return false; } } private static IEnumerable<string> GetLegacyRootsForDetection() { var roots = new System.Collections.Generic.List<string>(); string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; // macOS/Linux legacy roots.Add(Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer")); roots.Add(Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer")); // Windows roaming example try { string roaming = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty; if (!string.IsNullOrEmpty(roaming)) roots.Add(Path.Combine(roaming, "UnityMCP", "UnityMcpServer")); // Windows legacy: early installers/dev scripts used %LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer // Detect this location so we can clean up older copies during install/update. string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty; if (!string.IsNullOrEmpty(localAppData)) roots.Add(Path.Combine(localAppData, "Programs", "UnityMCP", "UnityMcpServer")); } catch { } return roots; } private static void TryKillUvForPath(string serverSrcPath) { try { if (string.IsNullOrEmpty(serverSrcPath)) return; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return; var psi = new System.Diagnostics.ProcessStartInfo { FileName = "/usr/bin/pgrep", Arguments = $"-f \"uv .*--directory {serverSrcPath}\"", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; using var p = System.Diagnostics.Process.Start(psi); if (p == null) return; string outp = p.StandardOutput.ReadToEnd(); p.WaitForExit(1500); if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp)) { foreach (var line in outp.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)) { if (int.TryParse(line.Trim(), out int pid)) { try { System.Diagnostics.Process.GetProcessById(pid).Kill(); } catch { } } } } } catch { } } private static string ReadVersionFile(string path) { try { if (string.IsNullOrEmpty(path) || !File.Exists(path)) return null; string v = File.ReadAllText(path).Trim(); return string.IsNullOrEmpty(v) ? null : v; } catch { return null; } } private static int CompareSemverSafe(string a, string b) { try { if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return 0; var ap = a.Split('.'); var bp = b.Split('.'); for (int i = 0; i < Math.Max(ap.Length, bp.Length); i++) { int ai = (i < ap.Length && int.TryParse(ap[i], out var t1)) ? t1 : 0; int bi = (i < bp.Length && int.TryParse(bp[i], out var t2)) ? t2 : 0; if (ai != bi) return ai.CompareTo(bi); } return 0; } catch { return 0; } } /// <summary> /// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package /// or common development locations. /// </summary> private static bool TryGetEmbeddedServerSource(out string srcPath) { return ServerPathResolver.TryFindEmbeddedServerSource(out srcPath); } private static readonly string[] _skipDirs = { ".venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".git" }; private static void CopyDirectoryRecursive(string sourceDir, string destinationDir) { Directory.CreateDirectory(destinationDir); foreach (string filePath in Directory.GetFiles(sourceDir)) { string fileName = Path.GetFileName(filePath); string destFile = Path.Combine(destinationDir, fileName); File.Copy(filePath, destFile, overwrite: true); } foreach (string dirPath in Directory.GetDirectories(sourceDir)) { string dirName = Path.GetFileName(dirPath); foreach (var skip in _skipDirs) { if (dirName.Equals(skip, StringComparison.OrdinalIgnoreCase)) goto NextDir; } try { if ((File.GetAttributes(dirPath) & FileAttributes.ReparsePoint) != 0) continue; } catch { } string destSubDir = Path.Combine(destinationDir, dirName); CopyDirectoryRecursive(dirPath, destSubDir); NextDir:; } } public static bool RebuildMcpServer() { try { // Find embedded source if (!TryGetEmbeddedServerSource(out string embeddedSrc)) { Debug.LogError("RebuildMcpServer: Could not find embedded server source."); return false; } string saveLocation = GetSaveLocation(); string destRoot = Path.Combine(saveLocation, ServerFolder); string destSrc = Path.Combine(destRoot, "src"); // Kill any running uv processes for this server TryKillUvForPath(destSrc); // Delete the entire installed server directory if (Directory.Exists(destRoot)) { try { Directory.Delete(destRoot, recursive: true); Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Deleted existing server at {destRoot}"); } catch (Exception ex) { Debug.LogError($"Failed to delete existing server: {ex.Message}"); return false; } } // Re-copy from embedded source string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc; Directory.CreateDirectory(destRoot); CopyDirectoryRecursive(embeddedRoot, destRoot); // Write version file string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc, VersionFileName)) ?? "unknown"; try { File.WriteAllText(Path.Combine(destSrc, VersionFileName), embeddedVer); } catch (Exception ex) { Debug.LogWarning($"Failed to write version file: {ex.Message}"); } Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Server rebuilt successfully at {destRoot} (version {embeddedVer})"); return true; } catch (Exception ex) { Debug.LogError($"RebuildMcpServer failed: {ex.Message}"); return false; } } internal static string FindUvPath() { // Allow user override via EditorPrefs try { string overridePath = EditorPrefs.GetString("MCPForUnity.UvPath", string.Empty); if (!string.IsNullOrEmpty(overridePath) && File.Exists(overridePath)) { if (ValidateUvBinary(overridePath)) return overridePath; } } catch { } string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; // Platform-specific candidate lists string[] candidates; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty; string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) ?? string.Empty; string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty; // Fast path: resolve from PATH first try { var wherePsi = new System.Diagnostics.ProcessStartInfo { FileName = "where", Arguments = "uv.exe", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; using var wp = System.Diagnostics.Process.Start(wherePsi); string output = wp.StandardOutput.ReadToEnd().Trim(); wp.WaitForExit(1500); if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output)) { foreach (var line in output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) { string path = line.Trim(); if (File.Exists(path) && ValidateUvBinary(path)) return path; } } } catch { } // Windows Store (PythonSoftwareFoundation) install location probe // Example: %LOCALAPPDATA%\Packages\PythonSoftwareFoundation.Python.3.13_*\LocalCache\local-packages\Python313\Scripts\uv.exe try { string pkgsRoot = Path.Combine(localAppData, "Packages"); if (Directory.Exists(pkgsRoot)) { var pythonPkgs = Directory.GetDirectories(pkgsRoot, "PythonSoftwareFoundation.Python.*", SearchOption.TopDirectoryOnly) .OrderByDescending(p => p, StringComparer.OrdinalIgnoreCase); foreach (var pkg in pythonPkgs) { string localCache = Path.Combine(pkg, "LocalCache", "local-packages"); if (!Directory.Exists(localCache)) continue; var pyRoots = Directory.GetDirectories(localCache, "Python*", SearchOption.TopDirectoryOnly) .OrderByDescending(d => d, StringComparer.OrdinalIgnoreCase); foreach (var pyRoot in pyRoots) { string uvExe = Path.Combine(pyRoot, "Scripts", "uv.exe"); if (File.Exists(uvExe) && ValidateUvBinary(uvExe)) return uvExe; } } } } catch { } candidates = new[] { // Preferred: WinGet Links shims (stable entrypoints) // Per-user shim (LOCALAPPDATA) → machine-wide shim (Program Files\WinGet\Links) Path.Combine(localAppData, "Microsoft", "WinGet", "Links", "uv.exe"), Path.Combine(programFiles, "WinGet", "Links", "uv.exe"), // Common per-user installs Path.Combine(localAppData, @"Programs\Python\Python313\Scripts\uv.exe"), Path.Combine(localAppData, @"Programs\Python\Python312\Scripts\uv.exe"), Path.Combine(localAppData, @"Programs\Python\Python311\Scripts\uv.exe"), Path.Combine(localAppData, @"Programs\Python\Python310\Scripts\uv.exe"), Path.Combine(appData, @"Python\Python313\Scripts\uv.exe"), Path.Combine(appData, @"Python\Python312\Scripts\uv.exe"), Path.Combine(appData, @"Python\Python311\Scripts\uv.exe"), Path.Combine(appData, @"Python\Python310\Scripts\uv.exe"), // Program Files style installs (if a native installer was used) Path.Combine(programFiles, @"uv\uv.exe"), // Try simple name resolution later via PATH "uv.exe", "uv" }; } else { candidates = new[] { "/opt/homebrew/bin/uv", "/usr/local/bin/uv", "/usr/bin/uv", "/opt/local/bin/uv", Path.Combine(home, ".local", "bin", "uv"), "/opt/homebrew/opt/uv/bin/uv", // Framework Python installs "/Library/Frameworks/Python.framework/Versions/3.13/bin/uv", "/Library/Frameworks/Python.framework/Versions/3.12/bin/uv", // Fallback to PATH resolution by name "uv" }; } foreach (string c in candidates) { try { if (File.Exists(c) && ValidateUvBinary(c)) return c; } catch { /* ignore */ } } // Use platform-appropriate which/where to resolve from PATH (non-Windows handled here; Windows tried earlier) try { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { var whichPsi = new System.Diagnostics.ProcessStartInfo { FileName = "/usr/bin/which", Arguments = "uv", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; try { // Prepend common user-local and package manager locations so 'which' can see them in Unity's GUI env string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; string prepend = string.Join(":", new[] { System.IO.Path.Combine(homeDir, ".local", "bin"), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin" }); string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; whichPsi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath) ? prepend : (prepend + ":" + currentPath); } catch { } using var wp = System.Diagnostics.Process.Start(whichPsi); string output = wp.StandardOutput.ReadToEnd().Trim(); wp.WaitForExit(3000); if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output)) { if (ValidateUvBinary(output)) return output; } } } catch { } // Manual PATH scan try { string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; string[] parts = pathEnv.Split(Path.PathSeparator); foreach (string part in parts) { try { // Check both uv and uv.exe string candidateUv = Path.Combine(part, "uv"); string candidateUvExe = Path.Combine(part, "uv.exe"); if (File.Exists(candidateUv) && ValidateUvBinary(candidateUv)) return candidateUv; if (File.Exists(candidateUvExe) && ValidateUvBinary(candidateUvExe)) return candidateUvExe; } catch { } } } catch { } return null; } private static bool ValidateUvBinary(string uvPath) { try { var psi = new System.Diagnostics.ProcessStartInfo { FileName = uvPath, Arguments = "--version", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; using var p = System.Diagnostics.Process.Start(psi); if (!p.WaitForExit(5000)) { try { p.Kill(); } catch { } return false; } if (p.ExitCode == 0) { string output = p.StandardOutput.ReadToEnd().Trim(); return output.StartsWith("uv "); } } catch { } return false; } } } ``` -------------------------------------------------------------------------------- /TestProjects/UnityMCPTests/Assets/Scripts/LongUnityScriptClaudeTest.cs: -------------------------------------------------------------------------------- ```csharp using UnityEngine; using System.Collections.Generic; // Standalone, dependency-free long script for Claude NL/T editing tests. // Intentionally verbose to simulate a complex gameplay script without external packages. public class LongUnityScriptClaudeTest : MonoBehaviour { [Header("Core References")] public Transform reachOrigin; public Animator animator; [Header("State")] private Transform currentTarget; private Transform previousTarget; private float lastTargetFoundTime; [Header("Held Objects")] private readonly List<Transform> heldObjects = new List<Transform>(); // Accumulators used by padding methods to avoid complete no-ops private int padAccumulator = 0; private Vector3 padVector = Vector3.zero; [Header("Tuning")] public float maxReachDistance = 2f; public float maxHorizontalDistance = 1.0f; public float maxVerticalDistance = 1.0f; // Public accessors used by NL tests public bool HasTarget() { return currentTarget != null; } public Transform GetCurrentTarget() => currentTarget; // Simple selection logic (self-contained) private Transform FindBestTarget() { if (reachOrigin == null) return null; // Dummy: prefer previously seen target within distance if (currentTarget != null && Vector3.Distance(reachOrigin.position, currentTarget.position) <= maxReachDistance) return currentTarget; return null; } private void HandleTargetSwitch(Transform next) { if (next == currentTarget) return; previousTarget = currentTarget; currentTarget = next; lastTargetFoundTime = Time.time; } private void LateUpdate() { // Keep file long with harmless per-frame work if (currentTarget == null && previousTarget != null) { // decay previous reference over time if (Time.time - lastTargetFoundTime > 0.5f) previousTarget = null; } } // NL tests sometimes add comments above Update() as an anchor private void Update() { if (reachOrigin == null) return; var best = FindBestTarget(); if (best != null) HandleTargetSwitch(best); } // Dummy reach/hold API (no external deps) public void OnObjectHeld(Transform t) { if (t == null) return; if (!heldObjects.Contains(t)) heldObjects.Add(t); animator?.SetInteger("objectsHeld", heldObjects.Count); } public void OnObjectPlaced() { if (heldObjects.Count == 0) return; heldObjects.RemoveAt(heldObjects.Count - 1); animator?.SetInteger("objectsHeld", heldObjects.Count); } // More padding: repetitive blocks with slight variations #region Padding Blocks private Vector3 AccumulateBlend(Transform t) { if (t == null || reachOrigin == null) return Vector3.zero; Vector3 local = reachOrigin.InverseTransformPoint(t.position); float bx = Mathf.Clamp(local.x / Mathf.Max(0.001f, maxHorizontalDistance), -1f, 1f); float by = Mathf.Clamp(local.y / Mathf.Max(0.001f, maxVerticalDistance), -1f, 1f); return new Vector3(bx, by, 0f); } private void ApplyBlend(Vector3 blend) { if (animator == null) return; animator.SetFloat("reachX", blend.x); animator.SetFloat("reachY", blend.y); } public void TickBlendOnce() { var b = AccumulateBlend(currentTarget); ApplyBlend(b); } // A long series of small no-op methods to bulk up the file without adding deps private void Step001() { } private void Step002() { } private void Step003() { } private void Step004() { } private void Step005() { } private void Step006() { } private void Step007() { } private void Step008() { } private void Step009() { } private void Step010() { } private void Step011() { } private void Step012() { } private void Step013() { } private void Step014() { } private void Step015() { } private void Step016() { } private void Step017() { } private void Step018() { } private void Step019() { } private void Step020() { } private void Step021() { } private void Step022() { } private void Step023() { } private void Step024() { } private void Step025() { } private void Step026() { } private void Step027() { } private void Step028() { } private void Step029() { } private void Step030() { } private void Step031() { } private void Step032() { } private void Step033() { } private void Step034() { } private void Step035() { } private void Step036() { } private void Step037() { } private void Step038() { } private void Step039() { } private void Step040() { } private void Step041() { } private void Step042() { } private void Step043() { } private void Step044() { } private void Step045() { } private void Step046() { } private void Step047() { } private void Step048() { } private void Step049() { } private void Step050() { } #endregion #region MassivePadding private void Pad0051() { } private void Pad0052() { } private void Pad0053() { } private void Pad0054() { } private void Pad0055() { } private void Pad0056() { } private void Pad0057() { } private void Pad0058() { } private void Pad0059() { } private void Pad0060() { } private void Pad0061() { } private void Pad0062() { } private void Pad0063() { } private void Pad0064() { } private void Pad0065() { } private void Pad0066() { } private void Pad0067() { } private void Pad0068() { } private void Pad0069() { } private void Pad0070() { } private void Pad0071() { } private void Pad0072() { } private void Pad0073() { } private void Pad0074() { } private void Pad0075() { } private void Pad0076() { } private void Pad0077() { } private void Pad0078() { } private void Pad0079() { } private void Pad0080() { } private void Pad0081() { } private void Pad0082() { } private void Pad0083() { } private void Pad0084() { } private void Pad0085() { } private void Pad0086() { } private void Pad0087() { } private void Pad0088() { } private void Pad0089() { } private void Pad0090() { } private void Pad0091() { } private void Pad0092() { } private void Pad0093() { } private void Pad0094() { } private void Pad0095() { } private void Pad0096() { } private void Pad0097() { } private void Pad0098() { } private void Pad0099() { } private void Pad0100() { // lightweight math to give this padding method some substance padAccumulator = (padAccumulator * 1664525 + 1013904223 + 100) & 0x7fffffff; float t = (padAccumulator % 1000) * 0.001f; padVector.x = Mathf.Lerp(padVector.x, t, 0.1f); padVector.y = Mathf.Lerp(padVector.y, 1f - t, 0.1f); padVector.z = 0f; } private void Pad0101() { } private void Pad0102() { } private void Pad0103() { } private void Pad0104() { } private void Pad0105() { } private void Pad0106() { } private void Pad0107() { } private void Pad0108() { } private void Pad0109() { } private void Pad0110() { } private void Pad0111() { } private void Pad0112() { } private void Pad0113() { } private void Pad0114() { } private void Pad0115() { } private void Pad0116() { } private void Pad0117() { } private void Pad0118() { } private void Pad0119() { } private void Pad0120() { } private void Pad0121() { } private void Pad0122() { } private void Pad0123() { } private void Pad0124() { } private void Pad0125() { } private void Pad0126() { } private void Pad0127() { } private void Pad0128() { } private void Pad0129() { } private void Pad0130() { } private void Pad0131() { } private void Pad0132() { } private void Pad0133() { } private void Pad0134() { } private void Pad0135() { } private void Pad0136() { } private void Pad0137() { } private void Pad0138() { } private void Pad0139() { } private void Pad0140() { } private void Pad0141() { } private void Pad0142() { } private void Pad0143() { } private void Pad0144() { } private void Pad0145() { } private void Pad0146() { } private void Pad0147() { } private void Pad0148() { } private void Pad0149() { } private void Pad0150() { // lightweight math to give this padding method some substance padAccumulator = (padAccumulator * 1664525 + 1013904223 + 150) & 0x7fffffff; float t = (padAccumulator % 1000) * 0.001f; padVector.x = Mathf.Lerp(padVector.x, t, 0.1f); padVector.y = Mathf.Lerp(padVector.y, 1f - t, 0.1f); padVector.z = 0f; } private void Pad0151() { } private void Pad0152() { } private void Pad0153() { } private void Pad0154() { } private void Pad0155() { } private void Pad0156() { } private void Pad0157() { } private void Pad0158() { } private void Pad0159() { } private void Pad0160() { } private void Pad0161() { } private void Pad0162() { } private void Pad0163() { } private void Pad0164() { } private void Pad0165() { } private void Pad0166() { } private void Pad0167() { } private void Pad0168() { } private void Pad0169() { } private void Pad0170() { } private void Pad0171() { } private void Pad0172() { } private void Pad0173() { } private void Pad0174() { } private void Pad0175() { } private void Pad0176() { } private void Pad0177() { } private void Pad0178() { } private void Pad0179() { } private void Pad0180() { } private void Pad0181() { } private void Pad0182() { } private void Pad0183() { } private void Pad0184() { } private void Pad0185() { } private void Pad0186() { } private void Pad0187() { } private void Pad0188() { } private void Pad0189() { } private void Pad0190() { } private void Pad0191() { } private void Pad0192() { } private void Pad0193() { } private void Pad0194() { } private void Pad0195() { } private void Pad0196() { } private void Pad0197() { } private void Pad0198() { } private void Pad0199() { } private void Pad0200() { // lightweight math to give this padding method some substance padAccumulator = (padAccumulator * 1664525 + 1013904223 + 200) & 0x7fffffff; float t = (padAccumulator % 1000) * 0.001f; padVector.x = Mathf.Lerp(padVector.x, t, 0.1f); padVector.y = Mathf.Lerp(padVector.y, 1f - t, 0.1f); padVector.z = 0f; } private void Pad0201() { } private void Pad0202() { } private void Pad0203() { } private void Pad0204() { } private void Pad0205() { } private void Pad0206() { } private void Pad0207() { } private void Pad0208() { } private void Pad0209() { } private void Pad0210() { } private void Pad0211() { } private void Pad0212() { } private void Pad0213() { } private void Pad0214() { } private void Pad0215() { } private void Pad0216() { } private void Pad0217() { } private void Pad0218() { } private void Pad0219() { } private void Pad0220() { } private void Pad0221() { } private void Pad0222() { } private void Pad0223() { } private void Pad0224() { } private void Pad0225() { } private void Pad0226() { } private void Pad0227() { } private void Pad0228() { } private void Pad0229() { } private void Pad0230() { } private void Pad0231() { } private void Pad0232() { } private void Pad0233() { } private void Pad0234() { } private void Pad0235() { } private void Pad0236() { } private void Pad0237() { } private void Pad0238() { } private void Pad0239() { } private void Pad0240() { } private void Pad0241() { } private void Pad0242() { } private void Pad0243() { } private void Pad0244() { } private void Pad0245() { } private void Pad0246() { } private void Pad0247() { } private void Pad0248() { } private void Pad0249() { } private void Pad0250() { // lightweight math to give this padding method some substance padAccumulator = (padAccumulator * 1664525 + 1013904223 + 250) & 0x7fffffff; float t = (padAccumulator % 1000) * 0.001f; padVector.x = Mathf.Lerp(padVector.x, t, 0.1f); padVector.y = Mathf.Lerp(padVector.y, 1f - t, 0.1f); padVector.z = 0f; } private void Pad0251() { } private void Pad0252() { } private void Pad0253() { } private void Pad0254() { } private void Pad0255() { } private void Pad0256() { } private void Pad0257() { } private void Pad0258() { } private void Pad0259() { } private void Pad0260() { } private void Pad0261() { } private void Pad0262() { } private void Pad0263() { } private void Pad0264() { } private void Pad0265() { } private void Pad0266() { } private void Pad0267() { } private void Pad0268() { } private void Pad0269() { } private void Pad0270() { } private void Pad0271() { } private void Pad0272() { } private void Pad0273() { } private void Pad0274() { } private void Pad0275() { } private void Pad0276() { } private void Pad0277() { } private void Pad0278() { } private void Pad0279() { } private void Pad0280() { } private void Pad0281() { } private void Pad0282() { } private void Pad0283() { } private void Pad0284() { } private void Pad0285() { } private void Pad0286() { } private void Pad0287() { } private void Pad0288() { } private void Pad0289() { } private void Pad0290() { } private void Pad0291() { } private void Pad0292() { } private void Pad0293() { } private void Pad0294() { } private void Pad0295() { } private void Pad0296() { } private void Pad0297() { } private void Pad0298() { } private void Pad0299() { } private void Pad0300() { // lightweight math to give this padding method some substance padAccumulator = (padAccumulator * 1664525 + 1013904223 + 300) & 0x7fffffff; float t = (padAccumulator % 1000) * 0.001f; padVector.x = Mathf.Lerp(padVector.x, t, 0.1f); padVector.y = Mathf.Lerp(padVector.y, 1f - t, 0.1f); padVector.z = 0f; } private void Pad0301() { } private void Pad0302() { } private void Pad0303() { } private void Pad0304() { } private void Pad0305() { } private void Pad0306() { } private void Pad0307() { } private void Pad0308() { } private void Pad0309() { } private void Pad0310() { } private void Pad0311() { } private void Pad0312() { } private void Pad0313() { } private void Pad0314() { } private void Pad0315() { } private void Pad0316() { } private void Pad0317() { } private void Pad0318() { } private void Pad0319() { } private void Pad0320() { } private void Pad0321() { } private void Pad0322() { } private void Pad0323() { } private void Pad0324() { } private void Pad0325() { } private void Pad0326() { } private void Pad0327() { } private void Pad0328() { } private void Pad0329() { } private void Pad0330() { } private void Pad0331() { } private void Pad0332() { } private void Pad0333() { } private void Pad0334() { } private void Pad0335() { } private void Pad0336() { } private void Pad0337() { } private void Pad0338() { } private void Pad0339() { } private void Pad0340() { } private void Pad0341() { } private void Pad0342() { } private void Pad0343() { } private void Pad0344() { } private void Pad0345() { } private void Pad0346() { } private void Pad0347() { } private void Pad0348() { } private void Pad0349() { } private void Pad0350() { // lightweight math to give this padding method some substance padAccumulator = (padAccumulator * 1664525 + 1013904223 + 350) & 0x7fffffff; float t = (padAccumulator % 1000) * 0.001f; padVector.x = Mathf.Lerp(padVector.x, t, 0.1f); padVector.y = Mathf.Lerp(padVector.y, 1f - t, 0.1f); padVector.z = 0f; } private void Pad0351() { } private void Pad0352() { } private void Pad0353() { } private void Pad0354() { } private void Pad0355() { } private void Pad0356() { } private void Pad0357() { } private void Pad0358() { } private void Pad0359() { } private void Pad0360() { } private void Pad0361() { } private void Pad0362() { } private void Pad0363() { } private void Pad0364() { } private void Pad0365() { } private void Pad0366() { } private void Pad0367() { } private void Pad0368() { } private void Pad0369() { } private void Pad0370() { } private void Pad0371() { } private void Pad0372() { } private void Pad0373() { } private void Pad0374() { } private void Pad0375() { } private void Pad0376() { } private void Pad0377() { } private void Pad0378() { } private void Pad0379() { } private void Pad0380() { } private void Pad0381() { } private void Pad0382() { } private void Pad0383() { } private void Pad0384() { } private void Pad0385() { } private void Pad0386() { } private void Pad0387() { } private void Pad0388() { } private void Pad0389() { } private void Pad0390() { } private void Pad0391() { } private void Pad0392() { } private void Pad0393() { } private void Pad0394() { } private void Pad0395() { } private void Pad0396() { } private void Pad0397() { } private void Pad0398() { } private void Pad0399() { } private void Pad0400() { // lightweight math to give this padding method some substance padAccumulator = (padAccumulator * 1664525 + 1013904223 + 400) & 0x7fffffff; float t = (padAccumulator % 1000) * 0.001f; padVector.x = Mathf.Lerp(padVector.x, t, 0.1f); padVector.y = Mathf.Lerp(padVector.y, 1f - t, 0.1f); padVector.z = 0f; } private void Pad0401() { } private void Pad0402() { } private void Pad0403() { } private void Pad0404() { } private void Pad0405() { } private void Pad0406() { } private void Pad0407() { } private void Pad0408() { } private void Pad0409() { } private void Pad0410() { } private void Pad0411() { } private void Pad0412() { } private void Pad0413() { } private void Pad0414() { } private void Pad0415() { } private void Pad0416() { } private void Pad0417() { } private void Pad0418() { } private void Pad0419() { } private void Pad0420() { } private void Pad0421() { } private void Pad0422() { } private void Pad0423() { } private void Pad0424() { } private void Pad0425() { } private void Pad0426() { } private void Pad0427() { } private void Pad0428() { } private void Pad0429() { } private void Pad0430() { } private void Pad0431() { } private void Pad0432() { } private void Pad0433() { } private void Pad0434() { } private void Pad0435() { } private void Pad0436() { } private void Pad0437() { } private void Pad0438() { } private void Pad0439() { } private void Pad0440() { } private void Pad0441() { } private void Pad0442() { } private void Pad0443() { } private void Pad0444() { } private void Pad0445() { } private void Pad0446() { } private void Pad0447() { } private void Pad0448() { } private void Pad0449() { } private void Pad0450() { // lightweight math to give this padding method some substance padAccumulator = (padAccumulator * 1664525 + 1013904223 + 450) & 0x7fffffff; float t = (padAccumulator % 1000) * 0.001f; padVector.x = Mathf.Lerp(padVector.x, t, 0.1f); padVector.y = Mathf.Lerp(padVector.y, 1f - t, 0.1f); padVector.z = 0f; } private void Pad0451() { } private void Pad0452() { } private void Pad0453() { } private void Pad0454() { } private void Pad0455() { } private void Pad0456() { } private void Pad0457() { } private void Pad0458() { } private void Pad0459() { } private void Pad0460() { } private void Pad0461() { } private void Pad0462() { } private void Pad0463() { } private void Pad0464() { } private void Pad0465() { } private void Pad0466() { } private void Pad0467() { } private void Pad0468() { } private void Pad0469() { } private void Pad0470() { } private void Pad0471() { } private void Pad0472() { } private void Pad0473() { } private void Pad0474() { } private void Pad0475() { } private void Pad0476() { } private void Pad0477() { } private void Pad0478() { } private void Pad0479() { } private void Pad0480() { } private void Pad0481() { } private void Pad0482() { } private void Pad0483() { } private void Pad0484() { } private void Pad0485() { } private void Pad0486() { } private void Pad0487() { } private void Pad0488() { } private void Pad0489() { } private void Pad0490() { } private void Pad0491() { } private void Pad0492() { } private void Pad0493() { } private void Pad0494() { } private void Pad0495() { } private void Pad0496() { } private void Pad0497() { } private void Pad0498() { } private void Pad0499() { } private void Pad0500() { // lightweight math to give this padding method some substance padAccumulator = (padAccumulator * 1664525 + 1013904223 + 500) & 0x7fffffff; float t = (padAccumulator % 1000) * 0.001f; padVector.x = Mathf.Lerp(padVector.x, t, 0.1f); padVector.y = Mathf.Lerp(padVector.y, 1f - t, 0.1f); padVector.z = 0f; } private void Pad0501() { } private void Pad0502() { } private void Pad0503() { } private void Pad0504() { } private void Pad0505() { } private void Pad0506() { } private void Pad0507() { } private void Pad0508() { } private void Pad0509() { } private void Pad0510() { } private void Pad0511() { } private void Pad0512() { } private void Pad0513() { } private void Pad0514() { } private void Pad0515() { } private void Pad0516() { } private void Pad0517() { } private void Pad0518() { } private void Pad0519() { } private void Pad0520() { } private void Pad0521() { } private void Pad0522() { } private void Pad0523() { } private void Pad0524() { } private void Pad0525() { } private void Pad0526() { } private void Pad0527() { } private void Pad0528() { } private void Pad0529() { } private void Pad0530() { } private void Pad0531() { } private void Pad0532() { } private void Pad0533() { } private void Pad0534() { } private void Pad0535() { } private void Pad0536() { } private void Pad0537() { } private void Pad0538() { } private void Pad0539() { } private void Pad0540() { } private void Pad0541() { } private void Pad0542() { } private void Pad0543() { } private void Pad0544() { } private void Pad0545() { } private void Pad0546() { } private void Pad0547() { } private void Pad0548() { } private void Pad0549() { } private void Pad0550() { // lightweight math to give this padding method some substance padAccumulator = (padAccumulator * 1664525 + 1013904223 + 550) & 0x7fffffff; float t = (padAccumulator % 1000) * 0.001f; padVector.x = Mathf.Lerp(padVector.x, t, 0.1f); padVector.y = Mathf.Lerp(padVector.y, 1f - t, 0.1f); padVector.z = 0f; } private void Pad0551() { } private void Pad0552() { } private void Pad0553() { } private void Pad0554() { } private void Pad0555() { } private void Pad0556() { } private void Pad0557() { } private void Pad0558() { } private void Pad0559() { } private void Pad0560() { } private void Pad0561() { } private void Pad0562() { } private void Pad0563() { } private void Pad0564() { } private void Pad0565() { } private void Pad0566() { } private void Pad0567() { } private void Pad0568() { } private void Pad0569() { } private void Pad0570() { } private void Pad0571() { } private void Pad0572() { } private void Pad0573() { } private void Pad0574() { } private void Pad0575() { } private void Pad0576() { } private void Pad0577() { } private void Pad0578() { } private void Pad0579() { } private void Pad0580() { } private void Pad0581() { } private void Pad0582() { } private void Pad0583() { } private void Pad0584() { } private void Pad0585() { } private void Pad0586() { } private void Pad0587() { } private void Pad0588() { } private void Pad0589() { } private void Pad0590() { } private void Pad0591() { } private void Pad0592() { } private void Pad0593() { } private void Pad0594() { } private void Pad0595() { } private void Pad0596() { } private void Pad0597() { } private void Pad0598() { } private void Pad0599() { } private void Pad0600() { // lightweight math to give this padding method some substance padAccumulator = (padAccumulator * 1664525 + 1013904223 + 600) & 0x7fffffff; float t = (padAccumulator % 1000) * 0.001f; padVector.x = Mathf.Lerp(padVector.x, t, 0.1f); padVector.y = Mathf.Lerp(padVector.y, 1f - t, 0.1f); padVector.z = 0f; } private void Pad0601() { } private void Pad0602() { } private void Pad0603() { } private void Pad0604() { } private void Pad0605() { } private void Pad0606() { } private void Pad0607() { } private void Pad0608() { } private void Pad0609() { } private void Pad0610() { } private void Pad0611() { } private void Pad0612() { } private void Pad0613() { } private void Pad0614() { } private void Pad0615() { } private void Pad0616() { } private void Pad0617() { } private void Pad0618() { } private void Pad0619() { } private void Pad0620() { } private void Pad0621() { } private void Pad0622() { } private void Pad0623() { } private void Pad0624() { } private void Pad0625() { } private void Pad0626() { } private void Pad0627() { } private void Pad0628() { } private void Pad0629() { } private void Pad0630() { } private void Pad0631() { } private void Pad0632() { } private void Pad0633() { } private void Pad0634() { } private void Pad0635() { } private void Pad0636() { } private void Pad0637() { } private void Pad0638() { } private void Pad0639() { } private void Pad0640() { } private void Pad0641() { } private void Pad0642() { } private void Pad0643() { } private void Pad0644() { } private void Pad0645() { } private void Pad0646() { } private void Pad0647() { } private void Pad0648() { } private void Pad0649() { } private void Pad0650() { // lightweight math to give this padding method some substance padAccumulator = (padAccumulator * 1664525 + 1013904223 + 650) & 0x7fffffff; float t = (padAccumulator % 1000) * 0.001f; padVector.x = Mathf.Lerp(padVector.x, t, 0.1f); padVector.y = Mathf.Lerp(padVector.y, 1f - t, 0.1f); padVector.z = 0f; } #endregion } ``` -------------------------------------------------------------------------------- /MCPForUnity/Editor/Windows/MCPForUnityEditorWindowNew.cs: -------------------------------------------------------------------------------- ```csharp using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; using UnityEditor; using UnityEditor.UIElements; // For Unity 2021 compatibility using UnityEngine; using UnityEngine.UIElements; using MCPForUnity.Editor.Data; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Models; using MCPForUnity.Editor.Services; namespace MCPForUnity.Editor.Windows { public class MCPForUnityEditorWindowNew : EditorWindow { // Protocol enum for future HTTP support private enum ConnectionProtocol { Stdio, // HTTPStreaming // Future } // Settings UI Elements private Label versionLabel; private Toggle debugLogsToggle; private EnumField validationLevelField; private Label validationDescription; private Foldout advancedSettingsFoldout; private TextField mcpServerPathOverride; private TextField uvPathOverride; private Button browsePythonButton; private Button clearPythonButton; private Button browseUvButton; private Button clearUvButton; private VisualElement mcpServerPathStatus; private VisualElement uvPathStatus; // Connection UI Elements private EnumField protocolDropdown; private TextField unityPortField; private TextField serverPortField; private VisualElement statusIndicator; private Label connectionStatusLabel; private Button connectionToggleButton; private VisualElement healthIndicator; private Label healthStatusLabel; private Button testConnectionButton; private VisualElement serverStatusBanner; private Label serverStatusMessage; private Button downloadServerButton; private Button rebuildServerButton; // Client UI Elements private DropdownField clientDropdown; private Button configureAllButton; private VisualElement clientStatusIndicator; private Label clientStatusLabel; private Button configureButton; private VisualElement claudeCliPathRow; private TextField claudeCliPath; private Button browseClaudeButton; private Foldout manualConfigFoldout; private TextField configPathField; private Button copyPathButton; private Button openFileButton; private TextField configJsonField; private Button copyJsonButton; private Label installationStepsLabel; // Data private readonly McpClients mcpClients = new(); private int selectedClientIndex = 0; private ValidationLevel currentValidationLevel = ValidationLevel.Standard; // Validation levels matching the existing enum private enum ValidationLevel { Basic, Standard, Comprehensive, Strict } public static void ShowWindow() { var window = GetWindow<MCPForUnityEditorWindowNew>("MCP For Unity"); window.minSize = new Vector2(500, 600); } public void CreateGUI() { // Determine base path (Package Manager vs Asset Store install) string basePath = AssetPathUtility.GetMcpPackageRootPath(); // Load UXML var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>( $"{basePath}/Editor/Windows/MCPForUnityEditorWindowNew.uxml" ); if (visualTree == null) { McpLog.Error($"Failed to load UXML at: {basePath}/Editor/Windows/MCPForUnityEditorWindowNew.uxml"); return; } visualTree.CloneTree(rootVisualElement); // Load USS var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>( $"{basePath}/Editor/Windows/MCPForUnityEditorWindowNew.uss" ); if (styleSheet != null) { rootVisualElement.styleSheets.Add(styleSheet); } // Cache UI elements CacheUIElements(); // Initialize UI InitializeUI(); // Register callbacks RegisterCallbacks(); // Initial update UpdateConnectionStatus(); UpdateServerStatusBanner(); UpdateClientStatus(); UpdatePathOverrides(); // Technically not required to connect, but if we don't do this, the UI will be blank UpdateManualConfiguration(); UpdateClaudeCliPathVisibility(); } private void OnEnable() { EditorApplication.update += OnEditorUpdate; } private void OnDisable() { EditorApplication.update -= OnEditorUpdate; } private void OnFocus() { // Only refresh data if UI is built if (rootVisualElement == null || rootVisualElement.childCount == 0) return; RefreshAllData(); } private void OnEditorUpdate() { // Only update UI if it's built if (rootVisualElement == null || rootVisualElement.childCount == 0) return; UpdateConnectionStatus(); } private void RefreshAllData() { // Update connection status UpdateConnectionStatus(); // Auto-verify bridge health if connected if (MCPServiceLocator.Bridge.IsRunning) { VerifyBridgeConnection(); } // Update path overrides UpdatePathOverrides(); // Refresh selected client (may have been configured externally) if (selectedClientIndex >= 0 && selectedClientIndex < mcpClients.clients.Count) { var client = mcpClients.clients[selectedClientIndex]; MCPServiceLocator.Client.CheckClientStatus(client); UpdateClientStatus(); UpdateManualConfiguration(); UpdateClaudeCliPathVisibility(); } } private void CacheUIElements() { // Settings versionLabel = rootVisualElement.Q<Label>("version-label"); debugLogsToggle = rootVisualElement.Q<Toggle>("debug-logs-toggle"); validationLevelField = rootVisualElement.Q<EnumField>("validation-level"); validationDescription = rootVisualElement.Q<Label>("validation-description"); advancedSettingsFoldout = rootVisualElement.Q<Foldout>("advanced-settings-foldout"); mcpServerPathOverride = rootVisualElement.Q<TextField>("python-path-override"); uvPathOverride = rootVisualElement.Q<TextField>("uv-path-override"); browsePythonButton = rootVisualElement.Q<Button>("browse-python-button"); clearPythonButton = rootVisualElement.Q<Button>("clear-python-button"); browseUvButton = rootVisualElement.Q<Button>("browse-uv-button"); clearUvButton = rootVisualElement.Q<Button>("clear-uv-button"); mcpServerPathStatus = rootVisualElement.Q<VisualElement>("mcp-server-path-status"); uvPathStatus = rootVisualElement.Q<VisualElement>("uv-path-status"); // Connection protocolDropdown = rootVisualElement.Q<EnumField>("protocol-dropdown"); unityPortField = rootVisualElement.Q<TextField>("unity-port"); serverPortField = rootVisualElement.Q<TextField>("server-port"); statusIndicator = rootVisualElement.Q<VisualElement>("status-indicator"); connectionStatusLabel = rootVisualElement.Q<Label>("connection-status"); connectionToggleButton = rootVisualElement.Q<Button>("connection-toggle"); healthIndicator = rootVisualElement.Q<VisualElement>("health-indicator"); healthStatusLabel = rootVisualElement.Q<Label>("health-status"); testConnectionButton = rootVisualElement.Q<Button>("test-connection-button"); serverStatusBanner = rootVisualElement.Q<VisualElement>("server-status-banner"); serverStatusMessage = rootVisualElement.Q<Label>("server-status-message"); downloadServerButton = rootVisualElement.Q<Button>("download-server-button"); rebuildServerButton = rootVisualElement.Q<Button>("rebuild-server-button"); // Client clientDropdown = rootVisualElement.Q<DropdownField>("client-dropdown"); configureAllButton = rootVisualElement.Q<Button>("configure-all-button"); clientStatusIndicator = rootVisualElement.Q<VisualElement>("client-status-indicator"); clientStatusLabel = rootVisualElement.Q<Label>("client-status"); configureButton = rootVisualElement.Q<Button>("configure-button"); claudeCliPathRow = rootVisualElement.Q<VisualElement>("claude-cli-path-row"); claudeCliPath = rootVisualElement.Q<TextField>("claude-cli-path"); browseClaudeButton = rootVisualElement.Q<Button>("browse-claude-button"); manualConfigFoldout = rootVisualElement.Q<Foldout>("manual-config-foldout"); configPathField = rootVisualElement.Q<TextField>("config-path"); copyPathButton = rootVisualElement.Q<Button>("copy-path-button"); openFileButton = rootVisualElement.Q<Button>("open-file-button"); configJsonField = rootVisualElement.Q<TextField>("config-json"); copyJsonButton = rootVisualElement.Q<Button>("copy-json-button"); installationStepsLabel = rootVisualElement.Q<Label>("installation-steps"); } private void InitializeUI() { // Settings Section UpdateVersionLabel(); debugLogsToggle.value = EditorPrefs.GetBool("MCPForUnity.DebugLogs", false); validationLevelField.Init(ValidationLevel.Standard); int savedLevel = EditorPrefs.GetInt("MCPForUnity.ValidationLevel", 1); currentValidationLevel = (ValidationLevel)Mathf.Clamp(savedLevel, 0, 3); validationLevelField.value = currentValidationLevel; UpdateValidationDescription(); // Advanced settings starts collapsed advancedSettingsFoldout.value = false; // Connection Section protocolDropdown.Init(ConnectionProtocol.Stdio); protocolDropdown.SetEnabled(false); // Disabled for now, only stdio supported unityPortField.value = MCPServiceLocator.Bridge.CurrentPort.ToString(); serverPortField.value = "6500"; // Client Configuration var clientNames = mcpClients.clients.Select(c => c.name).ToList(); clientDropdown.choices = clientNames; if (clientNames.Count > 0) { clientDropdown.index = 0; } // Manual config starts collapsed manualConfigFoldout.value = false; // Claude CLI path row hidden by default claudeCliPathRow.style.display = DisplayStyle.None; } private void RegisterCallbacks() { // Settings callbacks debugLogsToggle.RegisterValueChangedCallback(evt => { EditorPrefs.SetBool("MCPForUnity.DebugLogs", evt.newValue); }); validationLevelField.RegisterValueChangedCallback(evt => { currentValidationLevel = (ValidationLevel)evt.newValue; EditorPrefs.SetInt("MCPForUnity.ValidationLevel", (int)currentValidationLevel); UpdateValidationDescription(); }); // Advanced settings callbacks browsePythonButton.clicked += OnBrowsePythonClicked; clearPythonButton.clicked += OnClearPythonClicked; browseUvButton.clicked += OnBrowseUvClicked; clearUvButton.clicked += OnClearUvClicked; // Connection callbacks connectionToggleButton.clicked += OnConnectionToggleClicked; testConnectionButton.clicked += OnTestConnectionClicked; downloadServerButton.clicked += OnDownloadServerClicked; rebuildServerButton.clicked += OnRebuildServerClicked; // Client callbacks clientDropdown.RegisterValueChangedCallback(evt => { selectedClientIndex = clientDropdown.index; UpdateClientStatus(); UpdateManualConfiguration(); UpdateClaudeCliPathVisibility(); }); configureAllButton.clicked += OnConfigureAllClientsClicked; configureButton.clicked += OnConfigureClicked; browseClaudeButton.clicked += OnBrowseClaudeClicked; copyPathButton.clicked += OnCopyPathClicked; openFileButton.clicked += OnOpenFileClicked; copyJsonButton.clicked += OnCopyJsonClicked; } private void UpdateValidationDescription() { validationDescription.text = GetValidationLevelDescription((int)currentValidationLevel); } private string GetValidationLevelDescription(int index) { return index switch { 0 => "Only basic syntax checks (braces, quotes, comments)", 1 => "Syntax checks + Unity best practices and warnings", 2 => "All checks + semantic analysis and performance warnings", 3 => "Full semantic validation with namespace/type resolution (requires Roslyn)", _ => "Standard validation" }; } private void UpdateConnectionStatus() { var bridgeService = MCPServiceLocator.Bridge; bool isRunning = bridgeService.IsRunning; if (isRunning) { connectionStatusLabel.text = "Connected"; statusIndicator.RemoveFromClassList("disconnected"); statusIndicator.AddToClassList("connected"); connectionToggleButton.text = "Stop"; } else { connectionStatusLabel.text = "Disconnected"; statusIndicator.RemoveFromClassList("connected"); statusIndicator.AddToClassList("disconnected"); connectionToggleButton.text = "Start"; // Reset health status when disconnected healthStatusLabel.text = "Unknown"; healthIndicator.RemoveFromClassList("healthy"); healthIndicator.RemoveFromClassList("warning"); healthIndicator.AddToClassList("unknown"); } // Update ports unityPortField.value = bridgeService.CurrentPort.ToString(); } private void UpdateClientStatus() { if (selectedClientIndex < 0 || selectedClientIndex >= mcpClients.clients.Count) return; var client = mcpClients.clients[selectedClientIndex]; MCPServiceLocator.Client.CheckClientStatus(client); clientStatusLabel.text = client.GetStatusDisplayString(); // Reset inline color style (clear error state from OnConfigureClicked) clientStatusLabel.style.color = StyleKeyword.Null; // Update status indicator color clientStatusIndicator.RemoveFromClassList("configured"); clientStatusIndicator.RemoveFromClassList("not-configured"); clientStatusIndicator.RemoveFromClassList("warning"); switch (client.status) { case McpStatus.Configured: case McpStatus.Running: case McpStatus.Connected: clientStatusIndicator.AddToClassList("configured"); break; case McpStatus.IncorrectPath: case McpStatus.CommunicationError: case McpStatus.NoResponse: clientStatusIndicator.AddToClassList("warning"); break; default: clientStatusIndicator.AddToClassList("not-configured"); break; } // Update configure button text for Claude Code if (client.mcpType == McpTypes.ClaudeCode) { bool isConfigured = client.status == McpStatus.Configured; configureButton.text = isConfigured ? "Unregister" : "Register"; } else { configureButton.text = "Configure"; } } private void UpdateManualConfiguration() { if (selectedClientIndex < 0 || selectedClientIndex >= mcpClients.clients.Count) return; var client = mcpClients.clients[selectedClientIndex]; // Get config path string configPath = MCPServiceLocator.Client.GetConfigPath(client); configPathField.value = configPath; // Get config JSON string configJson = MCPServiceLocator.Client.GenerateConfigJson(client); configJsonField.value = configJson; // Get installation steps string steps = MCPServiceLocator.Client.GetInstallationSteps(client); installationStepsLabel.text = steps; } private void UpdateClaudeCliPathVisibility() { if (selectedClientIndex < 0 || selectedClientIndex >= mcpClients.clients.Count) return; var client = mcpClients.clients[selectedClientIndex]; // Show Claude CLI path only for Claude Code client if (client.mcpType == McpTypes.ClaudeCode) { string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath(); if (string.IsNullOrEmpty(claudePath)) { // Show path selector if not found claudeCliPathRow.style.display = DisplayStyle.Flex; claudeCliPath.value = "Not found - click Browse to select"; } else { // Show detected path claudeCliPathRow.style.display = DisplayStyle.Flex; claudeCliPath.value = claudePath; } } else { claudeCliPathRow.style.display = DisplayStyle.None; } } private void UpdatePathOverrides() { var pathService = MCPServiceLocator.Paths; // MCP Server Path string mcpServerPath = pathService.GetMcpServerPath(); if (pathService.HasMcpServerOverride) { mcpServerPathOverride.value = mcpServerPath ?? "(override set but invalid)"; } else { mcpServerPathOverride.value = mcpServerPath ?? "(auto-detected)"; } // Update status indicator mcpServerPathStatus.RemoveFromClassList("valid"); mcpServerPathStatus.RemoveFromClassList("invalid"); if (!string.IsNullOrEmpty(mcpServerPath) && File.Exists(Path.Combine(mcpServerPath, "server.py"))) { mcpServerPathStatus.AddToClassList("valid"); } else { mcpServerPathStatus.AddToClassList("invalid"); } // UV Path string uvPath = pathService.GetUvPath(); if (pathService.HasUvPathOverride) { uvPathOverride.value = uvPath ?? "(override set but invalid)"; } else { uvPathOverride.value = uvPath ?? "(auto-detected)"; } // Update status indicator uvPathStatus.RemoveFromClassList("valid"); uvPathStatus.RemoveFromClassList("invalid"); if (!string.IsNullOrEmpty(uvPath) && File.Exists(uvPath)) { uvPathStatus.AddToClassList("valid"); } else { uvPathStatus.AddToClassList("invalid"); } } // Button callbacks private void OnConnectionToggleClicked() { var bridgeService = MCPServiceLocator.Bridge; if (bridgeService.IsRunning) { bridgeService.Stop(); } else { bridgeService.Start(); // Verify connection after starting (Option C: verify on connect) EditorApplication.delayCall += () => { if (bridgeService.IsRunning) { VerifyBridgeConnection(); } }; } UpdateConnectionStatus(); } private void OnTestConnectionClicked() { VerifyBridgeConnection(); } private void VerifyBridgeConnection() { var bridgeService = MCPServiceLocator.Bridge; if (!bridgeService.IsRunning) { healthStatusLabel.text = "Disconnected"; healthIndicator.RemoveFromClassList("healthy"); healthIndicator.RemoveFromClassList("warning"); healthIndicator.AddToClassList("unknown"); McpLog.Warn("Cannot verify connection: Bridge is not running"); return; } var result = bridgeService.Verify(bridgeService.CurrentPort); healthIndicator.RemoveFromClassList("healthy"); healthIndicator.RemoveFromClassList("warning"); healthIndicator.RemoveFromClassList("unknown"); if (result.Success && result.PingSucceeded) { healthStatusLabel.text = "Healthy"; healthIndicator.AddToClassList("healthy"); McpLog.Info("Bridge verification successful"); } else if (result.HandshakeValid) { healthStatusLabel.text = "Ping Failed"; healthIndicator.AddToClassList("warning"); McpLog.Warn($"Bridge verification warning: {result.Message}"); } else { healthStatusLabel.text = "Unhealthy"; healthIndicator.AddToClassList("warning"); McpLog.Error($"Bridge verification failed: {result.Message}"); } } private void OnDownloadServerClicked() { if (ServerInstaller.DownloadAndInstallServer()) { UpdateServerStatusBanner(); UpdatePathOverrides(); EditorUtility.DisplayDialog( "Download Complete", "Server installed successfully! Start your connection and configure your MCP clients to begin.", "OK" ); } } private void OnRebuildServerClicked() { try { bool success = ServerInstaller.RebuildMcpServer(); if (success) { EditorUtility.DisplayDialog("MCP For Unity", "Server rebuilt successfully.", "OK"); UpdateServerStatusBanner(); UpdatePathOverrides(); } else { EditorUtility.DisplayDialog("MCP For Unity", "Rebuild failed. Please check Console for details.", "OK"); } } catch (Exception ex) { McpLog.Error($"Failed to rebuild server: {ex.Message}"); EditorUtility.DisplayDialog("MCP For Unity", $"Rebuild failed: {ex.Message}", "OK"); } } private void UpdateServerStatusBanner() { bool hasEmbedded = ServerInstaller.HasEmbeddedServer(); string installedVer = ServerInstaller.GetInstalledServerVersion(); string packageVer = AssetPathUtility.GetPackageVersion(); // Show/hide download vs rebuild buttons if (hasEmbedded) { downloadServerButton.style.display = DisplayStyle.None; rebuildServerButton.style.display = DisplayStyle.Flex; } else { downloadServerButton.style.display = DisplayStyle.Flex; rebuildServerButton.style.display = DisplayStyle.None; } // Update banner if (!hasEmbedded && string.IsNullOrEmpty(installedVer)) { serverStatusMessage.text = "\u26A0 Server not installed. Click 'Download & Install Server' to get started."; serverStatusBanner.style.display = DisplayStyle.Flex; } else if (!hasEmbedded && !string.IsNullOrEmpty(installedVer) && installedVer != packageVer) { serverStatusMessage.text = $"\u26A0 Server update available (v{installedVer} \u2192 v{packageVer}). Update recommended."; serverStatusBanner.style.display = DisplayStyle.Flex; } else { serverStatusBanner.style.display = DisplayStyle.None; } } private void OnConfigureAllClientsClicked() { try { var summary = MCPServiceLocator.Client.ConfigureAllDetectedClients(); // Build detailed message string message = summary.GetSummaryMessage() + "\n\n"; foreach (var msg in summary.Messages) { message += msg + "\n"; } EditorUtility.DisplayDialog("Configure All Clients", message, "OK"); // Refresh current client status if (selectedClientIndex >= 0 && selectedClientIndex < mcpClients.clients.Count) { UpdateClientStatus(); UpdateManualConfiguration(); } } catch (Exception ex) { EditorUtility.DisplayDialog("Configuration Failed", ex.Message, "OK"); } } private void OnConfigureClicked() { if (selectedClientIndex < 0 || selectedClientIndex >= mcpClients.clients.Count) return; var client = mcpClients.clients[selectedClientIndex]; try { if (client.mcpType == McpTypes.ClaudeCode) { bool isConfigured = client.status == McpStatus.Configured; if (isConfigured) { MCPServiceLocator.Client.UnregisterClaudeCode(); } else { MCPServiceLocator.Client.RegisterClaudeCode(); } } else { MCPServiceLocator.Client.ConfigureClient(client); } UpdateClientStatus(); UpdateManualConfiguration(); } catch (Exception ex) { clientStatusLabel.text = "Error"; clientStatusLabel.style.color = Color.red; McpLog.Error($"Configuration failed: {ex.Message}"); EditorUtility.DisplayDialog("Configuration Failed", ex.Message, "OK"); } } private void OnBrowsePythonClicked() { string picked = EditorUtility.OpenFolderPanel("Select MCP Server Directory", Application.dataPath, ""); if (!string.IsNullOrEmpty(picked)) { try { MCPServiceLocator.Paths.SetMcpServerOverride(picked); UpdatePathOverrides(); McpLog.Info($"MCP server path override set to: {picked}"); } catch (Exception ex) { EditorUtility.DisplayDialog("Invalid Path", ex.Message, "OK"); } } } private void OnClearPythonClicked() { MCPServiceLocator.Paths.ClearMcpServerOverride(); UpdatePathOverrides(); McpLog.Info("MCP server path override cleared"); } private void OnBrowseUvClicked() { string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/opt/homebrew/bin" : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); string picked = EditorUtility.OpenFilePanel("Select UV Executable", suggested, ""); if (!string.IsNullOrEmpty(picked)) { try { MCPServiceLocator.Paths.SetUvPathOverride(picked); UpdatePathOverrides(); McpLog.Info($"UV path override set to: {picked}"); } catch (Exception ex) { EditorUtility.DisplayDialog("Invalid Path", ex.Message, "OK"); } } } private void OnClearUvClicked() { MCPServiceLocator.Paths.ClearUvPathOverride(); UpdatePathOverrides(); McpLog.Info("UV path override cleared"); } private void OnBrowseClaudeClicked() { string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/opt/homebrew/bin" : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); string picked = EditorUtility.OpenFilePanel("Select Claude CLI", suggested, ""); if (!string.IsNullOrEmpty(picked)) { try { MCPServiceLocator.Paths.SetClaudeCliPathOverride(picked); UpdateClaudeCliPathVisibility(); UpdateClientStatus(); McpLog.Info($"Claude CLI path override set to: {picked}"); } catch (Exception ex) { EditorUtility.DisplayDialog("Invalid Path", ex.Message, "OK"); } } } private void OnCopyPathClicked() { EditorGUIUtility.systemCopyBuffer = configPathField.value; McpLog.Info("Config path copied to clipboard"); } private void OnOpenFileClicked() { string path = configPathField.value; try { if (!File.Exists(path)) { EditorUtility.DisplayDialog("Open File", "The configuration file path does not exist.", "OK"); return; } Process.Start(new ProcessStartInfo { FileName = path, UseShellExecute = true }); } catch (Exception ex) { McpLog.Error($"Failed to open file: {ex.Message}"); } } private void OnCopyJsonClicked() { EditorGUIUtility.systemCopyBuffer = configJsonField.value; McpLog.Info("Configuration copied to clipboard"); } private void UpdateVersionLabel() { string currentVersion = AssetPathUtility.GetPackageVersion(); versionLabel.text = $"v{currentVersion}"; // Check for updates using the service var updateCheck = MCPServiceLocator.Updates.CheckForUpdate(currentVersion); if (updateCheck.UpdateAvailable && !string.IsNullOrEmpty(updateCheck.LatestVersion)) { // Update available - enhance the label versionLabel.text = $"\u2191 v{currentVersion} (Update available: v{updateCheck.LatestVersion})"; versionLabel.style.color = new Color(1f, 0.7f, 0f); // Orange versionLabel.tooltip = $"Version {updateCheck.LatestVersion} is available. Update via Package Manager.\n\nGit URL: https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity"; } else { versionLabel.style.color = StyleKeyword.Null; // Default color versionLabel.tooltip = $"Current version: {currentVersion}"; } } } } ```