This is page 7 of 13. Use http://codebase.md/justinpbarnett/unity-mcp?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 │ │ │ │ ├── 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 -------------------------------------------------------------------------------- /UnityMcpBridge/Editor/Tools/ManageEditor.cs: -------------------------------------------------------------------------------- ```csharp using System; using System.Collections.Generic; using System.Linq; using System.IO; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEditorInternal; // Required for tag management using UnityEditor.SceneManagement; using UnityEngine; using MCPForUnity.Editor.Helpers; namespace MCPForUnity.Editor.Tools { /// <summary> /// Handles operations related to controlling and querying the Unity Editor state, /// including managing Tags and Layers. /// </summary> [McpForUnityTool("manage_editor")] public static class ManageEditor { // Constant for starting user layer index private const int FirstUserLayerIndex = 8; // Constant for total layer count private const int TotalLayerCount = 32; /// <summary> /// Main handler for editor management actions. /// </summary> public static object HandleCommand(JObject @params) { string action = @params["action"]?.ToString().ToLower(); // Parameters for specific actions string tagName = @params["tagName"]?.ToString(); string layerName = @params["layerName"]?.ToString(); bool waitForCompletion = @params["waitForCompletion"]?.ToObject<bool>() ?? false; // Example - not used everywhere if (string.IsNullOrEmpty(action)) { return Response.Error("Action parameter is required."); } // Route action switch (action) { // Play Mode Control case "play": try { if (!EditorApplication.isPlaying) { EditorApplication.isPlaying = true; return Response.Success("Entered play mode."); } return Response.Success("Already in play mode."); } catch (Exception e) { return Response.Error($"Error entering play mode: {e.Message}"); } case "pause": try { if (EditorApplication.isPlaying) { EditorApplication.isPaused = !EditorApplication.isPaused; return Response.Success( EditorApplication.isPaused ? "Game paused." : "Game resumed." ); } return Response.Error("Cannot pause/resume: Not in play mode."); } catch (Exception e) { return Response.Error($"Error pausing/resuming game: {e.Message}"); } case "stop": try { if (EditorApplication.isPlaying) { EditorApplication.isPlaying = false; return Response.Success("Exited play mode."); } return Response.Success("Already stopped (not in play mode)."); } catch (Exception e) { return Response.Error($"Error stopping play mode: {e.Message}"); } // Editor State/Info case "get_state": return GetEditorState(); case "get_project_root": return GetProjectRoot(); case "get_windows": return GetEditorWindows(); case "get_active_tool": return GetActiveTool(); case "get_selection": return GetSelection(); case "get_prefab_stage": return GetPrefabStageInfo(); case "set_active_tool": string toolName = @params["toolName"]?.ToString(); if (string.IsNullOrEmpty(toolName)) return Response.Error("'toolName' parameter required for set_active_tool."); return SetActiveTool(toolName); // Tag Management case "add_tag": if (string.IsNullOrEmpty(tagName)) return Response.Error("'tagName' parameter required for add_tag."); return AddTag(tagName); case "remove_tag": if (string.IsNullOrEmpty(tagName)) return Response.Error("'tagName' parameter required for remove_tag."); return RemoveTag(tagName); case "get_tags": return GetTags(); // Helper to list current tags // Layer Management case "add_layer": if (string.IsNullOrEmpty(layerName)) return Response.Error("'layerName' parameter required for add_layer."); return AddLayer(layerName); case "remove_layer": if (string.IsNullOrEmpty(layerName)) return Response.Error("'layerName' parameter required for remove_layer."); return RemoveLayer(layerName); case "get_layers": return GetLayers(); // Helper to list current layers // --- Settings (Example) --- // case "set_resolution": // int? width = @params["width"]?.ToObject<int?>(); // int? height = @params["height"]?.ToObject<int?>(); // if (!width.HasValue || !height.HasValue) return Response.Error("'width' and 'height' parameters required."); // return SetGameViewResolution(width.Value, height.Value); // case "set_quality": // // Handle string name or int index // return SetQualityLevel(@params["qualityLevel"]); default: return Response.Error( $"Unknown action: '{action}'. Supported actions include play, pause, stop, get_state, get_project_root, get_windows, get_active_tool, get_selection, get_prefab_stage, set_active_tool, add_tag, remove_tag, get_tags, add_layer, remove_layer, get_layers." ); } } // --- Editor State/Info Methods --- private static object GetEditorState() { try { var state = new { isPlaying = EditorApplication.isPlaying, isPaused = EditorApplication.isPaused, isCompiling = EditorApplication.isCompiling, isUpdating = EditorApplication.isUpdating, applicationPath = EditorApplication.applicationPath, applicationContentsPath = EditorApplication.applicationContentsPath, timeSinceStartup = EditorApplication.timeSinceStartup, }; return Response.Success("Retrieved editor state.", state); } catch (Exception e) { return Response.Error($"Error getting editor state: {e.Message}"); } } private static object GetProjectRoot() { try { // Application.dataPath points to <Project>/Assets string assetsPath = Application.dataPath.Replace('\\', '/'); string projectRoot = Directory.GetParent(assetsPath)?.FullName.Replace('\\', '/'); if (string.IsNullOrEmpty(projectRoot)) { return Response.Error("Could not determine project root from Application.dataPath"); } return Response.Success("Project root resolved.", new { projectRoot }); } catch (Exception e) { return Response.Error($"Error getting project root: {e.Message}"); } } private static object GetEditorWindows() { try { // Get all types deriving from EditorWindow var windowTypes = AppDomain .CurrentDomain.GetAssemblies() .SelectMany(assembly => assembly.GetTypes()) .Where(type => type.IsSubclassOf(typeof(EditorWindow))) .ToList(); var openWindows = new List<object>(); // Find currently open instances // Resources.FindObjectsOfTypeAll seems more reliable than GetWindow for finding *all* open windows EditorWindow[] allWindows = Resources.FindObjectsOfTypeAll<EditorWindow>(); foreach (EditorWindow window in allWindows) { if (window == null) continue; // Skip potentially destroyed windows try { openWindows.Add( new { title = window.titleContent.text, typeName = window.GetType().FullName, isFocused = EditorWindow.focusedWindow == window, position = new { x = window.position.x, y = window.position.y, width = window.position.width, height = window.position.height, }, instanceID = window.GetInstanceID(), } ); } catch (Exception ex) { Debug.LogWarning( $"Could not get info for window {window.GetType().Name}: {ex.Message}" ); } } return Response.Success("Retrieved list of open editor windows.", openWindows); } catch (Exception e) { return Response.Error($"Error getting editor windows: {e.Message}"); } } private static object GetPrefabStageInfo() { try { PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage(); if (stage == null) { return Response.Success ("No prefab stage is currently open.", new { isOpen = false }); } return Response.Success( "Prefab stage info retrieved.", new { isOpen = true, assetPath = stage.assetPath, prefabRootName = stage.prefabContentsRoot != null ? stage.prefabContentsRoot.name : null, mode = stage.mode.ToString(), isDirty = stage.scene.isDirty } ); } catch (Exception e) { return Response.Error($"Error getting prefab stage info: {e.Message}"); } } private static object GetActiveTool() { try { Tool currentTool = UnityEditor.Tools.current; string toolName = currentTool.ToString(); // Enum to string bool customToolActive = UnityEditor.Tools.current == Tool.Custom; // Check if a custom tool is active string activeToolName = customToolActive ? EditorTools.GetActiveToolName() : toolName; // Get custom name if needed var toolInfo = new { activeTool = activeToolName, isCustom = customToolActive, pivotMode = UnityEditor.Tools.pivotMode.ToString(), pivotRotation = UnityEditor.Tools.pivotRotation.ToString(), handleRotation = UnityEditor.Tools.handleRotation.eulerAngles, // Euler for simplicity handlePosition = UnityEditor.Tools.handlePosition, }; return Response.Success("Retrieved active tool information.", toolInfo); } catch (Exception e) { return Response.Error($"Error getting active tool: {e.Message}"); } } private static object SetActiveTool(string toolName) { try { Tool targetTool; if (Enum.TryParse<Tool>(toolName, true, out targetTool)) // Case-insensitive parse { // Check if it's a valid built-in tool if (targetTool != Tool.None && targetTool <= Tool.Custom) // Tool.Custom is the last standard tool { UnityEditor.Tools.current = targetTool; return Response.Success($"Set active tool to '{targetTool}'."); } else { return Response.Error( $"Cannot directly set tool to '{toolName}'. It might be None, Custom, or invalid." ); } } else { // Potentially try activating a custom tool by name here if needed // This often requires specific editor scripting knowledge for that tool. return Response.Error( $"Could not parse '{toolName}' as a standard Unity Tool (View, Move, Rotate, Scale, Rect, Transform, Custom)." ); } } catch (Exception e) { return Response.Error($"Error setting active tool: {e.Message}"); } } private static object GetSelection() { try { var selectionInfo = new { activeObject = Selection.activeObject?.name, activeGameObject = Selection.activeGameObject?.name, activeTransform = Selection.activeTransform?.name, activeInstanceID = Selection.activeInstanceID, count = Selection.count, objects = Selection .objects.Select(obj => new { name = obj?.name, type = obj?.GetType().FullName, instanceID = obj?.GetInstanceID(), }) .ToList(), gameObjects = Selection .gameObjects.Select(go => new { name = go?.name, instanceID = go?.GetInstanceID(), }) .ToList(), assetGUIDs = Selection.assetGUIDs, // GUIDs for selected assets in Project view }; return Response.Success("Retrieved current selection details.", selectionInfo); } catch (Exception e) { return Response.Error($"Error getting selection: {e.Message}"); } } // --- Tag Management Methods --- private static object AddTag(string tagName) { if (string.IsNullOrWhiteSpace(tagName)) return Response.Error("Tag name cannot be empty or whitespace."); // Check if tag already exists if (InternalEditorUtility.tags.Contains(tagName)) { return Response.Error($"Tag '{tagName}' already exists."); } try { // Add the tag using the internal utility InternalEditorUtility.AddTag(tagName); // Force save assets to ensure the change persists in the TagManager asset AssetDatabase.SaveAssets(); return Response.Success($"Tag '{tagName}' added successfully."); } catch (Exception e) { return Response.Error($"Failed to add tag '{tagName}': {e.Message}"); } } private static object RemoveTag(string tagName) { if (string.IsNullOrWhiteSpace(tagName)) return Response.Error("Tag name cannot be empty or whitespace."); if (tagName.Equals("Untagged", StringComparison.OrdinalIgnoreCase)) return Response.Error("Cannot remove the built-in 'Untagged' tag."); // Check if tag exists before attempting removal if (!InternalEditorUtility.tags.Contains(tagName)) { return Response.Error($"Tag '{tagName}' does not exist."); } try { // Remove the tag using the internal utility InternalEditorUtility.RemoveTag(tagName); // Force save assets AssetDatabase.SaveAssets(); return Response.Success($"Tag '{tagName}' removed successfully."); } catch (Exception e) { // Catch potential issues if the tag is somehow in use or removal fails return Response.Error($"Failed to remove tag '{tagName}': {e.Message}"); } } private static object GetTags() { try { string[] tags = InternalEditorUtility.tags; return Response.Success("Retrieved current tags.", tags); } catch (Exception e) { return Response.Error($"Failed to retrieve tags: {e.Message}"); } } // --- Layer Management Methods --- private static object AddLayer(string layerName) { if (string.IsNullOrWhiteSpace(layerName)) return Response.Error("Layer name cannot be empty or whitespace."); // Access the TagManager asset SerializedObject tagManager = GetTagManager(); if (tagManager == null) return Response.Error("Could not access TagManager asset."); SerializedProperty layersProp = tagManager.FindProperty("layers"); if (layersProp == null || !layersProp.isArray) return Response.Error("Could not find 'layers' property in TagManager."); // Check if layer name already exists (case-insensitive check recommended) for (int i = 0; i < TotalLayerCount; i++) { SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i); if ( layerSP != null && layerName.Equals(layerSP.stringValue, StringComparison.OrdinalIgnoreCase) ) { return Response.Error($"Layer '{layerName}' already exists at index {i}."); } } // Find the first empty user layer slot (indices 8 to 31) int firstEmptyUserLayer = -1; for (int i = FirstUserLayerIndex; i < TotalLayerCount; i++) { SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i); if (layerSP != null && string.IsNullOrEmpty(layerSP.stringValue)) { firstEmptyUserLayer = i; break; } } if (firstEmptyUserLayer == -1) { return Response.Error("No empty User Layer slots available (8-31 are full)."); } // Assign the name to the found slot try { SerializedProperty targetLayerSP = layersProp.GetArrayElementAtIndex( firstEmptyUserLayer ); targetLayerSP.stringValue = layerName; // Apply the changes to the TagManager asset tagManager.ApplyModifiedProperties(); // Save assets to make sure it's written to disk AssetDatabase.SaveAssets(); return Response.Success( $"Layer '{layerName}' added successfully to slot {firstEmptyUserLayer}." ); } catch (Exception e) { return Response.Error($"Failed to add layer '{layerName}': {e.Message}"); } } private static object RemoveLayer(string layerName) { if (string.IsNullOrWhiteSpace(layerName)) return Response.Error("Layer name cannot be empty or whitespace."); // Access the TagManager asset SerializedObject tagManager = GetTagManager(); if (tagManager == null) return Response.Error("Could not access TagManager asset."); SerializedProperty layersProp = tagManager.FindProperty("layers"); if (layersProp == null || !layersProp.isArray) return Response.Error("Could not find 'layers' property in TagManager."); // Find the layer by name (must be user layer) int layerIndexToRemove = -1; for (int i = FirstUserLayerIndex; i < TotalLayerCount; i++) // Start from user layers { SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i); // Case-insensitive comparison is safer if ( layerSP != null && layerName.Equals(layerSP.stringValue, StringComparison.OrdinalIgnoreCase) ) { layerIndexToRemove = i; break; } } if (layerIndexToRemove == -1) { return Response.Error($"User layer '{layerName}' not found."); } // Clear the name for that index try { SerializedProperty targetLayerSP = layersProp.GetArrayElementAtIndex( layerIndexToRemove ); targetLayerSP.stringValue = string.Empty; // Set to empty string to remove // Apply the changes tagManager.ApplyModifiedProperties(); // Save assets AssetDatabase.SaveAssets(); return Response.Success( $"Layer '{layerName}' (slot {layerIndexToRemove}) removed successfully." ); } catch (Exception e) { return Response.Error($"Failed to remove layer '{layerName}': {e.Message}"); } } private static object GetLayers() { try { var layers = new Dictionary<int, string>(); for (int i = 0; i < TotalLayerCount; i++) { string layerName = LayerMask.LayerToName(i); if (!string.IsNullOrEmpty(layerName)) // Only include layers that have names { layers.Add(i, layerName); } } return Response.Success("Retrieved current named layers.", layers); } catch (Exception e) { return Response.Error($"Failed to retrieve layers: {e.Message}"); } } // --- Helper Methods --- /// <summary> /// Gets the SerializedObject for the TagManager asset. /// </summary> private static SerializedObject GetTagManager() { try { // Load the TagManager asset from the ProjectSettings folder UnityEngine.Object[] tagManagerAssets = AssetDatabase.LoadAllAssetsAtPath( "ProjectSettings/TagManager.asset" ); if (tagManagerAssets == null || tagManagerAssets.Length == 0) { Debug.LogError("[ManageEditor] TagManager.asset not found in ProjectSettings."); return null; } // The first object in the asset file should be the TagManager return new SerializedObject(tagManagerAssets[0]); } catch (Exception e) { Debug.LogError($"[ManageEditor] Error accessing TagManager.asset: {e.Message}"); return null; } } // --- Example Implementations for Settings --- /* private static object SetGameViewResolution(int width, int height) { ... } private static object SetQualityLevel(JToken qualityLevelToken) { ... } */ } // Helper class to get custom tool names (remains the same) internal static class EditorTools { public static string GetActiveToolName() { // This is a placeholder. Real implementation depends on how custom tools // are registered and tracked in the specific Unity project setup. // It might involve checking static variables, calling methods on specific tool managers, etc. if (UnityEditor.Tools.current == Tool.Custom) { // Example: Check a known custom tool manager // if (MyCustomToolManager.IsActive) return MyCustomToolManager.ActiveToolName; return "Unknown Custom Tool"; } return UnityEditor.Tools.current.ToString(); } } } ``` -------------------------------------------------------------------------------- /MCPForUnity/Editor/Tools/ManageEditor.cs: -------------------------------------------------------------------------------- ```csharp using System; using System.Collections.Generic; using System.Linq; using System.IO; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEditorInternal; // Required for tag management using UnityEditor.SceneManagement; using UnityEngine; using MCPForUnity.Editor.Helpers; namespace MCPForUnity.Editor.Tools { /// <summary> /// Handles operations related to controlling and querying the Unity Editor state, /// including managing Tags and Layers. /// </summary> [McpForUnityTool("manage_editor")] public static class ManageEditor { // Constant for starting user layer index private const int FirstUserLayerIndex = 8; // Constant for total layer count private const int TotalLayerCount = 32; /// <summary> /// Main handler for editor management actions. /// </summary> public static object HandleCommand(JObject @params) { string action = @params["action"]?.ToString().ToLower(); // Parameters for specific actions string tagName = @params["tagName"]?.ToString(); string layerName = @params["layerName"]?.ToString(); bool waitForCompletion = @params["waitForCompletion"]?.ToObject<bool>() ?? false; // Example - not used everywhere if (string.IsNullOrEmpty(action)) { return Response.Error("Action parameter is required."); } // Route action switch (action) { // Play Mode Control case "play": try { if (!EditorApplication.isPlaying) { EditorApplication.isPlaying = true; return Response.Success("Entered play mode."); } return Response.Success("Already in play mode."); } catch (Exception e) { return Response.Error($"Error entering play mode: {e.Message}"); } case "pause": try { if (EditorApplication.isPlaying) { EditorApplication.isPaused = !EditorApplication.isPaused; return Response.Success( EditorApplication.isPaused ? "Game paused." : "Game resumed." ); } return Response.Error("Cannot pause/resume: Not in play mode."); } catch (Exception e) { return Response.Error($"Error pausing/resuming game: {e.Message}"); } case "stop": try { if (EditorApplication.isPlaying) { EditorApplication.isPlaying = false; return Response.Success("Exited play mode."); } return Response.Success("Already stopped (not in play mode)."); } catch (Exception e) { return Response.Error($"Error stopping play mode: {e.Message}"); } // Editor State/Info case "get_state": return GetEditorState(); case "get_project_root": return GetProjectRoot(); case "get_windows": return GetEditorWindows(); case "get_active_tool": return GetActiveTool(); case "get_selection": return GetSelection(); case "get_prefab_stage": return GetPrefabStageInfo(); case "set_active_tool": string toolName = @params["toolName"]?.ToString(); if (string.IsNullOrEmpty(toolName)) return Response.Error("'toolName' parameter required for set_active_tool."); return SetActiveTool(toolName); // Tag Management case "add_tag": if (string.IsNullOrEmpty(tagName)) return Response.Error("'tagName' parameter required for add_tag."); return AddTag(tagName); case "remove_tag": if (string.IsNullOrEmpty(tagName)) return Response.Error("'tagName' parameter required for remove_tag."); return RemoveTag(tagName); case "get_tags": return GetTags(); // Helper to list current tags // Layer Management case "add_layer": if (string.IsNullOrEmpty(layerName)) return Response.Error("'layerName' parameter required for add_layer."); return AddLayer(layerName); case "remove_layer": if (string.IsNullOrEmpty(layerName)) return Response.Error("'layerName' parameter required for remove_layer."); return RemoveLayer(layerName); case "get_layers": return GetLayers(); // Helper to list current layers // --- Settings (Example) --- // case "set_resolution": // int? width = @params["width"]?.ToObject<int?>(); // int? height = @params["height"]?.ToObject<int?>(); // if (!width.HasValue || !height.HasValue) return Response.Error("'width' and 'height' parameters required."); // return SetGameViewResolution(width.Value, height.Value); // case "set_quality": // // Handle string name or int index // return SetQualityLevel(@params["qualityLevel"]); default: return Response.Error( $"Unknown action: '{action}'. Supported actions include play, pause, stop, get_state, get_project_root, get_windows, get_active_tool, get_selection, get_prefab_stage, set_active_tool, add_tag, remove_tag, get_tags, add_layer, remove_layer, get_layers." ); } } // --- Editor State/Info Methods --- private static object GetEditorState() { try { var state = new { isPlaying = EditorApplication.isPlaying, isPaused = EditorApplication.isPaused, isCompiling = EditorApplication.isCompiling, isUpdating = EditorApplication.isUpdating, applicationPath = EditorApplication.applicationPath, applicationContentsPath = EditorApplication.applicationContentsPath, timeSinceStartup = EditorApplication.timeSinceStartup, }; return Response.Success("Retrieved editor state.", state); } catch (Exception e) { return Response.Error($"Error getting editor state: {e.Message}"); } } private static object GetProjectRoot() { try { // Application.dataPath points to <Project>/Assets string assetsPath = Application.dataPath.Replace('\\', '/'); string projectRoot = Directory.GetParent(assetsPath)?.FullName.Replace('\\', '/'); if (string.IsNullOrEmpty(projectRoot)) { return Response.Error("Could not determine project root from Application.dataPath"); } return Response.Success("Project root resolved.", new { projectRoot }); } catch (Exception e) { return Response.Error($"Error getting project root: {e.Message}"); } } private static object GetEditorWindows() { try { // Get all types deriving from EditorWindow var windowTypes = AppDomain .CurrentDomain.GetAssemblies() .SelectMany(assembly => assembly.GetTypes()) .Where(type => type.IsSubclassOf(typeof(EditorWindow))) .ToList(); var openWindows = new List<object>(); // Find currently open instances // Resources.FindObjectsOfTypeAll seems more reliable than GetWindow for finding *all* open windows EditorWindow[] allWindows = UnityEngine.Resources.FindObjectsOfTypeAll<EditorWindow>(); foreach (EditorWindow window in allWindows) { if (window == null) continue; // Skip potentially destroyed windows try { openWindows.Add( new { title = window.titleContent.text, typeName = window.GetType().FullName, isFocused = EditorWindow.focusedWindow == window, position = new { x = window.position.x, y = window.position.y, width = window.position.width, height = window.position.height, }, instanceID = window.GetInstanceID(), } ); } catch (Exception ex) { Debug.LogWarning( $"Could not get info for window {window.GetType().Name}: {ex.Message}" ); } } return Response.Success("Retrieved list of open editor windows.", openWindows); } catch (Exception e) { return Response.Error($"Error getting editor windows: {e.Message}"); } } private static object GetPrefabStageInfo() { try { PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage(); if (stage == null) { return Response.Success ("No prefab stage is currently open.", new { isOpen = false }); } return Response.Success( "Prefab stage info retrieved.", new { isOpen = true, assetPath = stage.assetPath, prefabRootName = stage.prefabContentsRoot != null ? stage.prefabContentsRoot.name : null, mode = stage.mode.ToString(), isDirty = stage.scene.isDirty } ); } catch (Exception e) { return Response.Error($"Error getting prefab stage info: {e.Message}"); } } private static object GetActiveTool() { try { Tool currentTool = UnityEditor.Tools.current; string toolName = currentTool.ToString(); // Enum to string bool customToolActive = UnityEditor.Tools.current == Tool.Custom; // Check if a custom tool is active string activeToolName = customToolActive ? EditorTools.GetActiveToolName() : toolName; // Get custom name if needed var toolInfo = new { activeTool = activeToolName, isCustom = customToolActive, pivotMode = UnityEditor.Tools.pivotMode.ToString(), pivotRotation = UnityEditor.Tools.pivotRotation.ToString(), handleRotation = UnityEditor.Tools.handleRotation.eulerAngles, // Euler for simplicity handlePosition = UnityEditor.Tools.handlePosition, }; return Response.Success("Retrieved active tool information.", toolInfo); } catch (Exception e) { return Response.Error($"Error getting active tool: {e.Message}"); } } private static object SetActiveTool(string toolName) { try { Tool targetTool; if (Enum.TryParse<Tool>(toolName, true, out targetTool)) // Case-insensitive parse { // Check if it's a valid built-in tool if (targetTool != Tool.None && targetTool <= Tool.Custom) // Tool.Custom is the last standard tool { UnityEditor.Tools.current = targetTool; return Response.Success($"Set active tool to '{targetTool}'."); } else { return Response.Error( $"Cannot directly set tool to '{toolName}'. It might be None, Custom, or invalid." ); } } else { // Potentially try activating a custom tool by name here if needed // This often requires specific editor scripting knowledge for that tool. return Response.Error( $"Could not parse '{toolName}' as a standard Unity Tool (View, Move, Rotate, Scale, Rect, Transform, Custom)." ); } } catch (Exception e) { return Response.Error($"Error setting active tool: {e.Message}"); } } private static object GetSelection() { try { var selectionInfo = new { activeObject = Selection.activeObject?.name, activeGameObject = Selection.activeGameObject?.name, activeTransform = Selection.activeTransform?.name, activeInstanceID = Selection.activeInstanceID, count = Selection.count, objects = Selection .objects.Select(obj => new { name = obj?.name, type = obj?.GetType().FullName, instanceID = obj?.GetInstanceID(), }) .ToList(), gameObjects = Selection .gameObjects.Select(go => new { name = go?.name, instanceID = go?.GetInstanceID(), }) .ToList(), assetGUIDs = Selection.assetGUIDs, // GUIDs for selected assets in Project view }; return Response.Success("Retrieved current selection details.", selectionInfo); } catch (Exception e) { return Response.Error($"Error getting selection: {e.Message}"); } } // --- Tag Management Methods --- private static object AddTag(string tagName) { if (string.IsNullOrWhiteSpace(tagName)) return Response.Error("Tag name cannot be empty or whitespace."); // Check if tag already exists if (InternalEditorUtility.tags.Contains(tagName)) { return Response.Error($"Tag '{tagName}' already exists."); } try { // Add the tag using the internal utility InternalEditorUtility.AddTag(tagName); // Force save assets to ensure the change persists in the TagManager asset AssetDatabase.SaveAssets(); return Response.Success($"Tag '{tagName}' added successfully."); } catch (Exception e) { return Response.Error($"Failed to add tag '{tagName}': {e.Message}"); } } private static object RemoveTag(string tagName) { if (string.IsNullOrWhiteSpace(tagName)) return Response.Error("Tag name cannot be empty or whitespace."); if (tagName.Equals("Untagged", StringComparison.OrdinalIgnoreCase)) return Response.Error("Cannot remove the built-in 'Untagged' tag."); // Check if tag exists before attempting removal if (!InternalEditorUtility.tags.Contains(tagName)) { return Response.Error($"Tag '{tagName}' does not exist."); } try { // Remove the tag using the internal utility InternalEditorUtility.RemoveTag(tagName); // Force save assets AssetDatabase.SaveAssets(); return Response.Success($"Tag '{tagName}' removed successfully."); } catch (Exception e) { // Catch potential issues if the tag is somehow in use or removal fails return Response.Error($"Failed to remove tag '{tagName}': {e.Message}"); } } private static object GetTags() { try { string[] tags = InternalEditorUtility.tags; return Response.Success("Retrieved current tags.", tags); } catch (Exception e) { return Response.Error($"Failed to retrieve tags: {e.Message}"); } } // --- Layer Management Methods --- private static object AddLayer(string layerName) { if (string.IsNullOrWhiteSpace(layerName)) return Response.Error("Layer name cannot be empty or whitespace."); // Access the TagManager asset SerializedObject tagManager = GetTagManager(); if (tagManager == null) return Response.Error("Could not access TagManager asset."); SerializedProperty layersProp = tagManager.FindProperty("layers"); if (layersProp == null || !layersProp.isArray) return Response.Error("Could not find 'layers' property in TagManager."); // Check if layer name already exists (case-insensitive check recommended) for (int i = 0; i < TotalLayerCount; i++) { SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i); if ( layerSP != null && layerName.Equals(layerSP.stringValue, StringComparison.OrdinalIgnoreCase) ) { return Response.Error($"Layer '{layerName}' already exists at index {i}."); } } // Find the first empty user layer slot (indices 8 to 31) int firstEmptyUserLayer = -1; for (int i = FirstUserLayerIndex; i < TotalLayerCount; i++) { SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i); if (layerSP != null && string.IsNullOrEmpty(layerSP.stringValue)) { firstEmptyUserLayer = i; break; } } if (firstEmptyUserLayer == -1) { return Response.Error("No empty User Layer slots available (8-31 are full)."); } // Assign the name to the found slot try { SerializedProperty targetLayerSP = layersProp.GetArrayElementAtIndex( firstEmptyUserLayer ); targetLayerSP.stringValue = layerName; // Apply the changes to the TagManager asset tagManager.ApplyModifiedProperties(); // Save assets to make sure it's written to disk AssetDatabase.SaveAssets(); return Response.Success( $"Layer '{layerName}' added successfully to slot {firstEmptyUserLayer}." ); } catch (Exception e) { return Response.Error($"Failed to add layer '{layerName}': {e.Message}"); } } private static object RemoveLayer(string layerName) { if (string.IsNullOrWhiteSpace(layerName)) return Response.Error("Layer name cannot be empty or whitespace."); // Access the TagManager asset SerializedObject tagManager = GetTagManager(); if (tagManager == null) return Response.Error("Could not access TagManager asset."); SerializedProperty layersProp = tagManager.FindProperty("layers"); if (layersProp == null || !layersProp.isArray) return Response.Error("Could not find 'layers' property in TagManager."); // Find the layer by name (must be user layer) int layerIndexToRemove = -1; for (int i = FirstUserLayerIndex; i < TotalLayerCount; i++) // Start from user layers { SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i); // Case-insensitive comparison is safer if ( layerSP != null && layerName.Equals(layerSP.stringValue, StringComparison.OrdinalIgnoreCase) ) { layerIndexToRemove = i; break; } } if (layerIndexToRemove == -1) { return Response.Error($"User layer '{layerName}' not found."); } // Clear the name for that index try { SerializedProperty targetLayerSP = layersProp.GetArrayElementAtIndex( layerIndexToRemove ); targetLayerSP.stringValue = string.Empty; // Set to empty string to remove // Apply the changes tagManager.ApplyModifiedProperties(); // Save assets AssetDatabase.SaveAssets(); return Response.Success( $"Layer '{layerName}' (slot {layerIndexToRemove}) removed successfully." ); } catch (Exception e) { return Response.Error($"Failed to remove layer '{layerName}': {e.Message}"); } } private static object GetLayers() { try { var layers = new Dictionary<int, string>(); for (int i = 0; i < TotalLayerCount; i++) { string layerName = LayerMask.LayerToName(i); if (!string.IsNullOrEmpty(layerName)) // Only include layers that have names { layers.Add(i, layerName); } } return Response.Success("Retrieved current named layers.", layers); } catch (Exception e) { return Response.Error($"Failed to retrieve layers: {e.Message}"); } } // --- Helper Methods --- /// <summary> /// Gets the SerializedObject for the TagManager asset. /// </summary> private static SerializedObject GetTagManager() { try { // Load the TagManager asset from the ProjectSettings folder UnityEngine.Object[] tagManagerAssets = AssetDatabase.LoadAllAssetsAtPath( "ProjectSettings/TagManager.asset" ); if (tagManagerAssets == null || tagManagerAssets.Length == 0) { Debug.LogError("[ManageEditor] TagManager.asset not found in ProjectSettings."); return null; } // The first object in the asset file should be the TagManager return new SerializedObject(tagManagerAssets[0]); } catch (Exception e) { Debug.LogError($"[ManageEditor] Error accessing TagManager.asset: {e.Message}"); return null; } } // --- Example Implementations for Settings --- /* private static object SetGameViewResolution(int width, int height) { ... } private static object SetQualityLevel(JToken qualityLevelToken) { ... } */ } // Helper class to get custom tool names (remains the same) internal static class EditorTools { public static string GetActiveToolName() { // This is a placeholder. Real implementation depends on how custom tools // are registered and tracked in the specific Unity project setup. // It might involve checking static variables, calling methods on specific tool managers, etc. if (UnityEditor.Tools.current == Tool.Custom) { // Example: Check a known custom tool manager // if (MyCustomToolManager.IsActive) return MyCustomToolManager.ActiveToolName; return "Unknown Custom Tool"; } return UnityEditor.Tools.current.ToString(); } } } ``` -------------------------------------------------------------------------------- /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); } } } } ``` -------------------------------------------------------------------------------- /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}"; } } } } ```