#
tokens: 37659/50000 5/264 files (page 9/19)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 9 of 19. Use http://codebase.md/justinpbarnett/unity-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .claude
│   ├── prompts
│   │   ├── nl-unity-suite-nl.md
│   │   └── nl-unity-suite-t.md
│   └── settings.json
├── .github
│   ├── scripts
│   │   └── mark_skipped.py
│   └── workflows
│       ├── bump-version.yml
│       ├── claude-nl-suite.yml
│       ├── github-repo-stats.yml
│       └── unity-tests.yml
├── .gitignore
├── deploy-dev.bat
├── docs
│   ├── CURSOR_HELP.md
│   ├── CUSTOM_TOOLS.md
│   ├── README-DEV-zh.md
│   ├── README-DEV.md
│   ├── screenshots
│   │   ├── v5_01_uninstall.png
│   │   ├── v5_02_install.png
│   │   ├── v5_03_open_mcp_window.png
│   │   ├── v5_04_rebuild_mcp_server.png
│   │   ├── v5_05_rebuild_success.png
│   │   ├── v6_2_create_python_tools_asset.png
│   │   ├── v6_2_python_tools_asset.png
│   │   ├── v6_new_ui_asset_store_version.png
│   │   ├── v6_new_ui_dark.png
│   │   └── v6_new_ui_light.png
│   ├── TELEMETRY.md
│   ├── v5_MIGRATION.md
│   └── v6_NEW_UI_CHANGES.md
├── LICENSE
├── logo.png
├── mcp_source.py
├── MCPForUnity
│   ├── Editor
│   │   ├── AssemblyInfo.cs
│   │   ├── AssemblyInfo.cs.meta
│   │   ├── Data
│   │   │   ├── DefaultServerConfig.cs
│   │   │   ├── DefaultServerConfig.cs.meta
│   │   │   ├── McpClients.cs
│   │   │   ├── McpClients.cs.meta
│   │   │   ├── PythonToolsAsset.cs
│   │   │   └── PythonToolsAsset.cs.meta
│   │   ├── Data.meta
│   │   ├── Dependencies
│   │   │   ├── DependencyManager.cs
│   │   │   ├── DependencyManager.cs.meta
│   │   │   ├── Models
│   │   │   │   ├── DependencyCheckResult.cs
│   │   │   │   ├── DependencyCheckResult.cs.meta
│   │   │   │   ├── DependencyStatus.cs
│   │   │   │   └── DependencyStatus.cs.meta
│   │   │   ├── Models.meta
│   │   │   ├── PlatformDetectors
│   │   │   │   ├── IPlatformDetector.cs
│   │   │   │   ├── IPlatformDetector.cs.meta
│   │   │   │   ├── LinuxPlatformDetector.cs
│   │   │   │   ├── LinuxPlatformDetector.cs.meta
│   │   │   │   ├── MacOSPlatformDetector.cs
│   │   │   │   ├── MacOSPlatformDetector.cs.meta
│   │   │   │   ├── PlatformDetectorBase.cs
│   │   │   │   ├── PlatformDetectorBase.cs.meta
│   │   │   │   ├── WindowsPlatformDetector.cs
│   │   │   │   └── WindowsPlatformDetector.cs.meta
│   │   │   └── PlatformDetectors.meta
│   │   ├── Dependencies.meta
│   │   ├── External
│   │   │   ├── Tommy.cs
│   │   │   └── Tommy.cs.meta
│   │   ├── External.meta
│   │   ├── Helpers
│   │   │   ├── AssetPathUtility.cs
│   │   │   ├── AssetPathUtility.cs.meta
│   │   │   ├── CodexConfigHelper.cs
│   │   │   ├── CodexConfigHelper.cs.meta
│   │   │   ├── ConfigJsonBuilder.cs
│   │   │   ├── ConfigJsonBuilder.cs.meta
│   │   │   ├── ExecPath.cs
│   │   │   ├── ExecPath.cs.meta
│   │   │   ├── GameObjectSerializer.cs
│   │   │   ├── GameObjectSerializer.cs.meta
│   │   │   ├── McpConfigFileHelper.cs
│   │   │   ├── McpConfigFileHelper.cs.meta
│   │   │   ├── McpConfigurationHelper.cs
│   │   │   ├── McpConfigurationHelper.cs.meta
│   │   │   ├── McpLog.cs
│   │   │   ├── McpLog.cs.meta
│   │   │   ├── McpPathResolver.cs
│   │   │   ├── McpPathResolver.cs.meta
│   │   │   ├── PackageDetector.cs
│   │   │   ├── PackageDetector.cs.meta
│   │   │   ├── PackageInstaller.cs
│   │   │   ├── PackageInstaller.cs.meta
│   │   │   ├── PortManager.cs
│   │   │   ├── PortManager.cs.meta
│   │   │   ├── PythonToolSyncProcessor.cs
│   │   │   ├── PythonToolSyncProcessor.cs.meta
│   │   │   ├── Response.cs
│   │   │   ├── Response.cs.meta
│   │   │   ├── ServerInstaller.cs
│   │   │   ├── ServerInstaller.cs.meta
│   │   │   ├── ServerPathResolver.cs
│   │   │   ├── ServerPathResolver.cs.meta
│   │   │   ├── TelemetryHelper.cs
│   │   │   ├── TelemetryHelper.cs.meta
│   │   │   ├── Vector3Helper.cs
│   │   │   └── Vector3Helper.cs.meta
│   │   ├── Helpers.meta
│   │   ├── Importers
│   │   │   ├── PythonFileImporter.cs
│   │   │   └── PythonFileImporter.cs.meta
│   │   ├── Importers.meta
│   │   ├── MCPForUnity.Editor.asmdef
│   │   ├── MCPForUnity.Editor.asmdef.meta
│   │   ├── MCPForUnityBridge.cs
│   │   ├── MCPForUnityBridge.cs.meta
│   │   ├── Models
│   │   │   ├── Command.cs
│   │   │   ├── Command.cs.meta
│   │   │   ├── McpClient.cs
│   │   │   ├── McpClient.cs.meta
│   │   │   ├── McpConfig.cs
│   │   │   ├── McpConfig.cs.meta
│   │   │   ├── MCPConfigServer.cs
│   │   │   ├── MCPConfigServer.cs.meta
│   │   │   ├── MCPConfigServers.cs
│   │   │   ├── MCPConfigServers.cs.meta
│   │   │   ├── McpStatus.cs
│   │   │   ├── McpStatus.cs.meta
│   │   │   ├── McpTypes.cs
│   │   │   ├── McpTypes.cs.meta
│   │   │   ├── ServerConfig.cs
│   │   │   └── ServerConfig.cs.meta
│   │   ├── Models.meta
│   │   ├── Resources
│   │   │   ├── McpForUnityResourceAttribute.cs
│   │   │   ├── McpForUnityResourceAttribute.cs.meta
│   │   │   ├── MenuItems
│   │   │   │   ├── GetMenuItems.cs
│   │   │   │   └── GetMenuItems.cs.meta
│   │   │   ├── MenuItems.meta
│   │   │   ├── Tests
│   │   │   │   ├── GetTests.cs
│   │   │   │   └── GetTests.cs.meta
│   │   │   └── Tests.meta
│   │   ├── Resources.meta
│   │   ├── Services
│   │   │   ├── BridgeControlService.cs
│   │   │   ├── BridgeControlService.cs.meta
│   │   │   ├── ClientConfigurationService.cs
│   │   │   ├── ClientConfigurationService.cs.meta
│   │   │   ├── IBridgeControlService.cs
│   │   │   ├── IBridgeControlService.cs.meta
│   │   │   ├── IClientConfigurationService.cs
│   │   │   ├── IClientConfigurationService.cs.meta
│   │   │   ├── IPackageUpdateService.cs
│   │   │   ├── IPackageUpdateService.cs.meta
│   │   │   ├── IPathResolverService.cs
│   │   │   ├── IPathResolverService.cs.meta
│   │   │   ├── IPythonToolRegistryService.cs
│   │   │   ├── IPythonToolRegistryService.cs.meta
│   │   │   ├── ITestRunnerService.cs
│   │   │   ├── ITestRunnerService.cs.meta
│   │   │   ├── IToolSyncService.cs
│   │   │   ├── IToolSyncService.cs.meta
│   │   │   ├── MCPServiceLocator.cs
│   │   │   ├── MCPServiceLocator.cs.meta
│   │   │   ├── PackageUpdateService.cs
│   │   │   ├── PackageUpdateService.cs.meta
│   │   │   ├── PathResolverService.cs
│   │   │   ├── PathResolverService.cs.meta
│   │   │   ├── PythonToolRegistryService.cs
│   │   │   ├── PythonToolRegistryService.cs.meta
│   │   │   ├── TestRunnerService.cs
│   │   │   ├── TestRunnerService.cs.meta
│   │   │   ├── ToolSyncService.cs
│   │   │   └── ToolSyncService.cs.meta
│   │   ├── Services.meta
│   │   ├── Setup
│   │   │   ├── SetupWizard.cs
│   │   │   ├── SetupWizard.cs.meta
│   │   │   ├── SetupWizardWindow.cs
│   │   │   └── SetupWizardWindow.cs.meta
│   │   ├── Setup.meta
│   │   ├── Tools
│   │   │   ├── CommandRegistry.cs
│   │   │   ├── CommandRegistry.cs.meta
│   │   │   ├── ExecuteMenuItem.cs
│   │   │   ├── ExecuteMenuItem.cs.meta
│   │   │   ├── ManageAsset.cs
│   │   │   ├── ManageAsset.cs.meta
│   │   │   ├── ManageEditor.cs
│   │   │   ├── ManageEditor.cs.meta
│   │   │   ├── ManageGameObject.cs
│   │   │   ├── ManageGameObject.cs.meta
│   │   │   ├── ManageScene.cs
│   │   │   ├── ManageScene.cs.meta
│   │   │   ├── ManageScript.cs
│   │   │   ├── ManageScript.cs.meta
│   │   │   ├── ManageShader.cs
│   │   │   ├── ManageShader.cs.meta
│   │   │   ├── McpForUnityToolAttribute.cs
│   │   │   ├── McpForUnityToolAttribute.cs.meta
│   │   │   ├── Prefabs
│   │   │   │   ├── ManagePrefabs.cs
│   │   │   │   └── ManagePrefabs.cs.meta
│   │   │   ├── Prefabs.meta
│   │   │   ├── ReadConsole.cs
│   │   │   ├── ReadConsole.cs.meta
│   │   │   ├── RunTests.cs
│   │   │   └── RunTests.cs.meta
│   │   ├── Tools.meta
│   │   ├── Windows
│   │   │   ├── ManualConfigEditorWindow.cs
│   │   │   ├── ManualConfigEditorWindow.cs.meta
│   │   │   ├── MCPForUnityEditorWindow.cs
│   │   │   ├── MCPForUnityEditorWindow.cs.meta
│   │   │   ├── MCPForUnityEditorWindowNew.cs
│   │   │   ├── MCPForUnityEditorWindowNew.cs.meta
│   │   │   ├── MCPForUnityEditorWindowNew.uss
│   │   │   ├── MCPForUnityEditorWindowNew.uss.meta
│   │   │   ├── MCPForUnityEditorWindowNew.uxml
│   │   │   ├── MCPForUnityEditorWindowNew.uxml.meta
│   │   │   ├── VSCodeManualSetupWindow.cs
│   │   │   └── VSCodeManualSetupWindow.cs.meta
│   │   └── Windows.meta
│   ├── Editor.meta
│   ├── package.json
│   ├── package.json.meta
│   ├── README.md
│   ├── README.md.meta
│   ├── Runtime
│   │   ├── MCPForUnity.Runtime.asmdef
│   │   ├── MCPForUnity.Runtime.asmdef.meta
│   │   ├── Serialization
│   │   │   ├── UnityTypeConverters.cs
│   │   │   └── UnityTypeConverters.cs.meta
│   │   └── Serialization.meta
│   ├── Runtime.meta
│   └── UnityMcpServer~
│       └── src
│           ├── __init__.py
│           ├── config.py
│           ├── Dockerfile
│           ├── models.py
│           ├── module_discovery.py
│           ├── port_discovery.py
│           ├── pyproject.toml
│           ├── pyrightconfig.json
│           ├── registry
│           │   ├── __init__.py
│           │   ├── resource_registry.py
│           │   └── tool_registry.py
│           ├── reload_sentinel.py
│           ├── resources
│           │   ├── __init__.py
│           │   ├── menu_items.py
│           │   └── tests.py
│           ├── server_version.txt
│           ├── server.py
│           ├── telemetry_decorator.py
│           ├── telemetry.py
│           ├── test_telemetry.py
│           ├── tools
│           │   ├── __init__.py
│           │   ├── execute_menu_item.py
│           │   ├── manage_asset.py
│           │   ├── manage_editor.py
│           │   ├── manage_gameobject.py
│           │   ├── manage_prefabs.py
│           │   ├── manage_scene.py
│           │   ├── manage_script.py
│           │   ├── manage_shader.py
│           │   ├── read_console.py
│           │   ├── resource_tools.py
│           │   ├── run_tests.py
│           │   └── script_apply_edits.py
│           ├── unity_connection.py
│           └── uv.lock
├── prune_tool_results.py
├── README-zh.md
├── README.md
├── restore-dev.bat
├── scripts
│   └── validate-nlt-coverage.sh
├── test_unity_socket_framing.py
├── TestProjects
│   └── UnityMCPTests
│       ├── .gitignore
│       ├── Assets
│       │   ├── Editor.meta
│       │   ├── Scenes
│       │   │   ├── SampleScene.unity
│       │   │   └── SampleScene.unity.meta
│       │   ├── Scenes.meta
│       │   ├── Scripts
│       │   │   ├── Hello.cs
│       │   │   ├── Hello.cs.meta
│       │   │   ├── LongUnityScriptClaudeTest.cs
│       │   │   ├── LongUnityScriptClaudeTest.cs.meta
│       │   │   ├── TestAsmdef
│       │   │   │   ├── CustomComponent.cs
│       │   │   │   ├── CustomComponent.cs.meta
│       │   │   │   ├── TestAsmdef.asmdef
│       │   │   │   └── TestAsmdef.asmdef.meta
│       │   │   └── TestAsmdef.meta
│       │   ├── Scripts.meta
│       │   ├── Tests
│       │   │   ├── EditMode
│       │   │   │   ├── Data
│       │   │   │   │   ├── PythonToolsAssetTests.cs
│       │   │   │   │   └── PythonToolsAssetTests.cs.meta
│       │   │   │   ├── Data.meta
│       │   │   │   ├── Helpers
│       │   │   │   │   ├── CodexConfigHelperTests.cs
│       │   │   │   │   ├── CodexConfigHelperTests.cs.meta
│       │   │   │   │   ├── WriteToConfigTests.cs
│       │   │   │   │   └── WriteToConfigTests.cs.meta
│       │   │   │   ├── Helpers.meta
│       │   │   │   ├── MCPForUnityTests.Editor.asmdef
│       │   │   │   ├── MCPForUnityTests.Editor.asmdef.meta
│       │   │   │   ├── Resources
│       │   │   │   │   ├── GetMenuItemsTests.cs
│       │   │   │   │   └── GetMenuItemsTests.cs.meta
│       │   │   │   ├── Resources.meta
│       │   │   │   ├── Services
│       │   │   │   │   ├── PackageUpdateServiceTests.cs
│       │   │   │   │   ├── PackageUpdateServiceTests.cs.meta
│       │   │   │   │   ├── PythonToolRegistryServiceTests.cs
│       │   │   │   │   ├── PythonToolRegistryServiceTests.cs.meta
│       │   │   │   │   ├── ToolSyncServiceTests.cs
│       │   │   │   │   └── ToolSyncServiceTests.cs.meta
│       │   │   │   ├── Services.meta
│       │   │   │   ├── Tools
│       │   │   │   │   ├── AIPropertyMatchingTests.cs
│       │   │   │   │   ├── AIPropertyMatchingTests.cs.meta
│       │   │   │   │   ├── CommandRegistryTests.cs
│       │   │   │   │   ├── CommandRegistryTests.cs.meta
│       │   │   │   │   ├── ComponentResolverTests.cs
│       │   │   │   │   ├── ComponentResolverTests.cs.meta
│       │   │   │   │   ├── ExecuteMenuItemTests.cs
│       │   │   │   │   ├── ExecuteMenuItemTests.cs.meta
│       │   │   │   │   ├── ManageGameObjectTests.cs
│       │   │   │   │   ├── ManageGameObjectTests.cs.meta
│       │   │   │   │   ├── ManagePrefabsTests.cs
│       │   │   │   │   ├── ManagePrefabsTests.cs.meta
│       │   │   │   │   ├── ManageScriptValidationTests.cs
│       │   │   │   │   ├── ManageScriptValidationTests.cs.meta
│       │   │   │   │   └── MaterialMeshInstantiationTests.cs
│       │   │   │   ├── Tools.meta
│       │   │   │   ├── Windows
│       │   │   │   │   ├── ManualConfigJsonBuilderTests.cs
│       │   │   │   │   └── ManualConfigJsonBuilderTests.cs.meta
│       │   │   │   └── Windows.meta
│       │   │   └── EditMode.meta
│       │   └── Tests.meta
│       ├── Packages
│       │   └── manifest.json
│       └── ProjectSettings
│           ├── Packages
│           │   └── com.unity.testtools.codecoverage
│           │       └── Settings.json
│           └── ProjectVersion.txt
├── tests
│   ├── conftest.py
│   ├── test_edit_normalization_and_noop.py
│   ├── test_edit_strict_and_warnings.py
│   ├── test_find_in_file_minimal.py
│   ├── test_get_sha.py
│   ├── test_improved_anchor_matching.py
│   ├── test_logging_stdout.py
│   ├── test_manage_script_uri.py
│   ├── test_read_console_truncate.py
│   ├── test_read_resource_minimal.py
│   ├── test_resources_api.py
│   ├── test_script_editing.py
│   ├── test_script_tools.py
│   ├── test_telemetry_endpoint_validation.py
│   ├── test_telemetry_queue_worker.py
│   ├── test_telemetry_subaction.py
│   ├── test_transport_framing.py
│   └── test_validate_script_summary.py
├── tools
│   └── stress_mcp.py
└── UnityMcpBridge
    ├── Editor
    │   ├── AssemblyInfo.cs
    │   ├── AssemblyInfo.cs.meta
    │   ├── Data
    │   │   ├── DefaultServerConfig.cs
    │   │   ├── DefaultServerConfig.cs.meta
    │   │   ├── McpClients.cs
    │   │   └── McpClients.cs.meta
    │   ├── Data.meta
    │   ├── Dependencies
    │   │   ├── DependencyManager.cs
    │   │   ├── DependencyManager.cs.meta
    │   │   ├── Models
    │   │   │   ├── DependencyCheckResult.cs
    │   │   │   ├── DependencyCheckResult.cs.meta
    │   │   │   ├── DependencyStatus.cs
    │   │   │   └── DependencyStatus.cs.meta
    │   │   ├── Models.meta
    │   │   ├── PlatformDetectors
    │   │   │   ├── IPlatformDetector.cs
    │   │   │   ├── IPlatformDetector.cs.meta
    │   │   │   ├── LinuxPlatformDetector.cs
    │   │   │   ├── LinuxPlatformDetector.cs.meta
    │   │   │   ├── MacOSPlatformDetector.cs
    │   │   │   ├── MacOSPlatformDetector.cs.meta
    │   │   │   ├── PlatformDetectorBase.cs
    │   │   │   ├── PlatformDetectorBase.cs.meta
    │   │   │   ├── WindowsPlatformDetector.cs
    │   │   │   └── WindowsPlatformDetector.cs.meta
    │   │   └── PlatformDetectors.meta
    │   ├── Dependencies.meta
    │   ├── External
    │   │   ├── Tommy.cs
    │   │   └── Tommy.cs.meta
    │   ├── External.meta
    │   ├── Helpers
    │   │   ├── AssetPathUtility.cs
    │   │   ├── AssetPathUtility.cs.meta
    │   │   ├── CodexConfigHelper.cs
    │   │   ├── CodexConfigHelper.cs.meta
    │   │   ├── ConfigJsonBuilder.cs
    │   │   ├── ConfigJsonBuilder.cs.meta
    │   │   ├── ExecPath.cs
    │   │   ├── ExecPath.cs.meta
    │   │   ├── GameObjectSerializer.cs
    │   │   ├── GameObjectSerializer.cs.meta
    │   │   ├── McpConfigFileHelper.cs
    │   │   ├── McpConfigFileHelper.cs.meta
    │   │   ├── McpConfigurationHelper.cs
    │   │   ├── McpConfigurationHelper.cs.meta
    │   │   ├── McpLog.cs
    │   │   ├── McpLog.cs.meta
    │   │   ├── McpPathResolver.cs
    │   │   ├── McpPathResolver.cs.meta
    │   │   ├── PackageDetector.cs
    │   │   ├── PackageDetector.cs.meta
    │   │   ├── PackageInstaller.cs
    │   │   ├── PackageInstaller.cs.meta
    │   │   ├── PortManager.cs
    │   │   ├── PortManager.cs.meta
    │   │   ├── Response.cs
    │   │   ├── Response.cs.meta
    │   │   ├── ServerInstaller.cs
    │   │   ├── ServerInstaller.cs.meta
    │   │   ├── ServerPathResolver.cs
    │   │   ├── ServerPathResolver.cs.meta
    │   │   ├── TelemetryHelper.cs
    │   │   ├── TelemetryHelper.cs.meta
    │   │   ├── Vector3Helper.cs
    │   │   └── Vector3Helper.cs.meta
    │   ├── Helpers.meta
    │   ├── MCPForUnity.Editor.asmdef
    │   ├── MCPForUnity.Editor.asmdef.meta
    │   ├── MCPForUnityBridge.cs
    │   ├── MCPForUnityBridge.cs.meta
    │   ├── Models
    │   │   ├── Command.cs
    │   │   ├── Command.cs.meta
    │   │   ├── McpClient.cs
    │   │   ├── McpClient.cs.meta
    │   │   ├── McpConfig.cs
    │   │   ├── McpConfig.cs.meta
    │   │   ├── MCPConfigServer.cs
    │   │   ├── MCPConfigServer.cs.meta
    │   │   ├── MCPConfigServers.cs
    │   │   ├── MCPConfigServers.cs.meta
    │   │   ├── McpStatus.cs
    │   │   ├── McpStatus.cs.meta
    │   │   ├── McpTypes.cs
    │   │   ├── McpTypes.cs.meta
    │   │   ├── ServerConfig.cs
    │   │   └── ServerConfig.cs.meta
    │   ├── Models.meta
    │   ├── Setup
    │   │   ├── SetupWizard.cs
    │   │   ├── SetupWizard.cs.meta
    │   │   ├── SetupWizardWindow.cs
    │   │   └── SetupWizardWindow.cs.meta
    │   ├── Setup.meta
    │   ├── Tools
    │   │   ├── CommandRegistry.cs
    │   │   ├── CommandRegistry.cs.meta
    │   │   ├── ManageAsset.cs
    │   │   ├── ManageAsset.cs.meta
    │   │   ├── ManageEditor.cs
    │   │   ├── ManageEditor.cs.meta
    │   │   ├── ManageGameObject.cs
    │   │   ├── ManageGameObject.cs.meta
    │   │   ├── ManageScene.cs
    │   │   ├── ManageScene.cs.meta
    │   │   ├── ManageScript.cs
    │   │   ├── ManageScript.cs.meta
    │   │   ├── ManageShader.cs
    │   │   ├── ManageShader.cs.meta
    │   │   ├── McpForUnityToolAttribute.cs
    │   │   ├── McpForUnityToolAttribute.cs.meta
    │   │   ├── MenuItems
    │   │   │   ├── ManageMenuItem.cs
    │   │   │   ├── ManageMenuItem.cs.meta
    │   │   │   ├── MenuItemExecutor.cs
    │   │   │   ├── MenuItemExecutor.cs.meta
    │   │   │   ├── MenuItemsReader.cs
    │   │   │   └── MenuItemsReader.cs.meta
    │   │   ├── MenuItems.meta
    │   │   ├── Prefabs
    │   │   │   ├── ManagePrefabs.cs
    │   │   │   └── ManagePrefabs.cs.meta
    │   │   ├── Prefabs.meta
    │   │   ├── ReadConsole.cs
    │   │   └── ReadConsole.cs.meta
    │   ├── Tools.meta
    │   ├── Windows
    │   │   ├── ManualConfigEditorWindow.cs
    │   │   ├── ManualConfigEditorWindow.cs.meta
    │   │   ├── MCPForUnityEditorWindow.cs
    │   │   ├── MCPForUnityEditorWindow.cs.meta
    │   │   ├── VSCodeManualSetupWindow.cs
    │   │   └── VSCodeManualSetupWindow.cs.meta
    │   └── Windows.meta
    ├── Editor.meta
    ├── package.json
    ├── package.json.meta
    ├── README.md
    ├── README.md.meta
    ├── Runtime
    │   ├── MCPForUnity.Runtime.asmdef
    │   ├── MCPForUnity.Runtime.asmdef.meta
    │   ├── Serialization
    │   │   ├── UnityTypeConverters.cs
    │   │   └── UnityTypeConverters.cs.meta
    │   └── Serialization.meta
    ├── Runtime.meta
    └── UnityMcpServer~
        └── src
            ├── __init__.py
            ├── config.py
            ├── Dockerfile
            ├── port_discovery.py
            ├── pyproject.toml
            ├── pyrightconfig.json
            ├── registry
            │   ├── __init__.py
            │   └── tool_registry.py
            ├── reload_sentinel.py
            ├── server_version.txt
            ├── server.py
            ├── telemetry_decorator.py
            ├── telemetry.py
            ├── test_telemetry.py
            ├── tools
            │   ├── __init__.py
            │   ├── manage_asset.py
            │   ├── manage_editor.py
            │   ├── manage_gameobject.py
            │   ├── manage_menu_item.py
            │   ├── manage_prefabs.py
            │   ├── manage_scene.py
            │   ├── manage_script.py
            │   ├── manage_shader.py
            │   ├── read_console.py
            │   ├── resource_tools.py
            │   └── script_apply_edits.py
            ├── unity_connection.py
            └── uv.lock
```

# Files

--------------------------------------------------------------------------------
/MCPForUnity/Editor/Setup/SetupWizardWindow.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Linq;
  3 | using MCPForUnity.Editor.Data;
  4 | using MCPForUnity.Editor.Dependencies;
  5 | using MCPForUnity.Editor.Dependencies.Models;
  6 | using MCPForUnity.Editor.Helpers;
  7 | using MCPForUnity.Editor.Models;
  8 | using UnityEditor;
  9 | using UnityEngine;
 10 | 
 11 | namespace MCPForUnity.Editor.Setup
 12 | {
 13 |     /// <summary>
 14 |     /// Setup wizard window for guiding users through dependency installation
 15 |     /// </summary>
 16 |     public class SetupWizardWindow : EditorWindow
 17 |     {
 18 |         private DependencyCheckResult _dependencyResult;
 19 |         private Vector2 _scrollPosition;
 20 |         private int _currentStep = 0;
 21 |         private McpClients _mcpClients;
 22 |         private int _selectedClientIndex = 0;
 23 | 
 24 |         private readonly string[] _stepTitles = {
 25 |             "Setup",
 26 |             "Configure",
 27 |             "Complete"
 28 |         };
 29 | 
 30 |         public static void ShowWindow(DependencyCheckResult dependencyResult = null)
 31 |         {
 32 |             var window = GetWindow<SetupWizardWindow>("MCP for Unity Setup");
 33 |             window.minSize = new Vector2(500, 400);
 34 |             window.maxSize = new Vector2(800, 600);
 35 |             window._dependencyResult = dependencyResult ?? DependencyManager.CheckAllDependencies();
 36 |             window.Show();
 37 |         }
 38 | 
 39 |         private void OnEnable()
 40 |         {
 41 |             if (_dependencyResult == null)
 42 |             {
 43 |                 _dependencyResult = DependencyManager.CheckAllDependencies();
 44 |             }
 45 | 
 46 |             _mcpClients = new McpClients();
 47 | 
 48 |             // Check client configurations on startup
 49 |             foreach (var client in _mcpClients.clients)
 50 |             {
 51 |                 CheckClientConfiguration(client);
 52 |             }
 53 |         }
 54 | 
 55 |         private void OnGUI()
 56 |         {
 57 |             DrawHeader();
 58 |             DrawProgressBar();
 59 | 
 60 |             _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
 61 | 
 62 |             switch (_currentStep)
 63 |             {
 64 |                 case 0: DrawSetupStep(); break;
 65 |                 case 1: DrawConfigureStep(); break;
 66 |                 case 2: DrawCompleteStep(); break;
 67 |             }
 68 | 
 69 |             EditorGUILayout.EndScrollView();
 70 | 
 71 |             DrawFooter();
 72 |         }
 73 | 
 74 |         private void DrawHeader()
 75 |         {
 76 |             EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
 77 |             GUILayout.Label("MCP for Unity Setup Wizard", EditorStyles.boldLabel);
 78 |             GUILayout.FlexibleSpace();
 79 |             GUILayout.Label($"Step {_currentStep + 1} of {_stepTitles.Length}");
 80 |             EditorGUILayout.EndHorizontal();
 81 | 
 82 |             EditorGUILayout.Space();
 83 | 
 84 |             // Step title
 85 |             var titleStyle = new GUIStyle(EditorStyles.largeLabel)
 86 |             {
 87 |                 fontSize = 16,
 88 |                 fontStyle = FontStyle.Bold
 89 |             };
 90 |             EditorGUILayout.LabelField(_stepTitles[_currentStep], titleStyle);
 91 |             EditorGUILayout.Space();
 92 |         }
 93 | 
 94 |         private void DrawProgressBar()
 95 |         {
 96 |             var rect = EditorGUILayout.GetControlRect(false, 4);
 97 |             var progress = (_currentStep + 1) / (float)_stepTitles.Length;
 98 |             EditorGUI.ProgressBar(rect, progress, "");
 99 |             EditorGUILayout.Space();
100 |         }
101 | 
102 |         private void DrawSetupStep()
103 |         {
104 |             // Welcome section
105 |             DrawSectionTitle("MCP for Unity Setup");
106 | 
107 |             EditorGUILayout.LabelField(
108 |                 "This wizard will help you set up MCP for Unity to connect AI assistants with your Unity Editor.",
109 |                 EditorStyles.wordWrappedLabel
110 |             );
111 |             EditorGUILayout.Space();
112 | 
113 |             // Dependency check section
114 |             EditorGUILayout.BeginHorizontal();
115 |             DrawSectionTitle("System Check", 14);
116 |             GUILayout.FlexibleSpace();
117 |             if (GUILayout.Button("Refresh", GUILayout.Width(60), GUILayout.Height(20)))
118 |             {
119 |                 _dependencyResult = DependencyManager.CheckAllDependencies();
120 |             }
121 |             EditorGUILayout.EndHorizontal();
122 | 
123 |             // Show simplified dependency status
124 |             foreach (var dep in _dependencyResult.Dependencies)
125 |             {
126 |                 DrawSimpleDependencyStatus(dep);
127 |             }
128 | 
129 |             // Overall status and installation guidance
130 |             EditorGUILayout.Space();
131 |             if (!_dependencyResult.IsSystemReady)
132 |             {
133 |                 // Only show critical warnings when dependencies are actually missing
134 |                 EditorGUILayout.HelpBox(
135 |                     "⚠️ Missing Dependencies: MCP for Unity requires Python 3.10+ and UV package manager to function properly.",
136 |                     MessageType.Warning
137 |                 );
138 | 
139 |                 EditorGUILayout.Space();
140 |                 EditorGUILayout.BeginVertical(EditorStyles.helpBox);
141 |                 DrawErrorStatus("Installation Required");
142 | 
143 |                 var recommendations = DependencyManager.GetInstallationRecommendations();
144 |                 EditorGUILayout.LabelField(recommendations, EditorStyles.wordWrappedLabel);
145 | 
146 |                 EditorGUILayout.Space();
147 |                 if (GUILayout.Button("Open Installation Links", GUILayout.Height(25)))
148 |                 {
149 |                     OpenInstallationUrls();
150 |                 }
151 |                 EditorGUILayout.EndVertical();
152 |             }
153 |             else
154 |             {
155 |                 DrawSuccessStatus("System Ready");
156 |                 EditorGUILayout.LabelField("All requirements are met. You can proceed to configure your AI clients.", EditorStyles.wordWrappedLabel);
157 |             }
158 |         }
159 | 
160 | 
161 | 
162 |         private void DrawCompleteStep()
163 |         {
164 |             DrawSectionTitle("Setup Complete");
165 | 
166 |             // Refresh dependency check with caching to avoid heavy operations on every repaint
167 |             if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2)
168 |             {
169 |                 _dependencyResult = DependencyManager.CheckAllDependencies();
170 |             }
171 | 
172 |             if (_dependencyResult.IsSystemReady)
173 |             {
174 |                 DrawSuccessStatus("MCP for Unity Ready!");
175 | 
176 |                 EditorGUILayout.HelpBox(
177 |                     "🎉 MCP for Unity is now set up and ready to use!\n\n" +
178 |                     "• Dependencies verified\n" +
179 |                     "• MCP server ready\n" +
180 |                     "• Client configuration accessible",
181 |                     MessageType.Info
182 |                 );
183 | 
184 |                 EditorGUILayout.Space();
185 |                 EditorGUILayout.BeginHorizontal();
186 |                 if (GUILayout.Button("Documentation", GUILayout.Height(30)))
187 |                 {
188 |                     Application.OpenURL("https://github.com/CoplayDev/unity-mcp");
189 |                 }
190 |                 if (GUILayout.Button("Client Settings", GUILayout.Height(30)))
191 |                 {
192 |                     Windows.MCPForUnityEditorWindow.ShowWindow();
193 |                 }
194 |                 EditorGUILayout.EndHorizontal();
195 |             }
196 |             else
197 |             {
198 |                 DrawErrorStatus("Setup Incomplete - Package Non-Functional");
199 | 
200 |                 EditorGUILayout.HelpBox(
201 |                     "🚨 MCP for Unity CANNOT work - dependencies still missing!\n\n" +
202 |                     "Install ALL required dependencies before the package will function.",
203 |                     MessageType.Error
204 |                 );
205 | 
206 |                 var missingDeps = _dependencyResult.GetMissingRequired();
207 |                 if (missingDeps.Count > 0)
208 |                 {
209 |                     EditorGUILayout.Space();
210 |                     EditorGUILayout.LabelField("Still Missing:", EditorStyles.boldLabel);
211 |                     foreach (var dep in missingDeps)
212 |                     {
213 |                         EditorGUILayout.LabelField($"✗ {dep.Name}", EditorStyles.label);
214 |                     }
215 |                 }
216 | 
217 |                 EditorGUILayout.Space();
218 |                 if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30)))
219 |                 {
220 |                     _currentStep = 0;
221 |                 }
222 |             }
223 |         }
224 | 
225 |         // Helper methods for consistent UI components
226 |         private void DrawSectionTitle(string title, int fontSize = 16)
227 |         {
228 |             var titleStyle = new GUIStyle(EditorStyles.boldLabel)
229 |             {
230 |                 fontSize = fontSize,
231 |                 fontStyle = FontStyle.Bold
232 |             };
233 |             EditorGUILayout.LabelField(title, titleStyle);
234 |             EditorGUILayout.Space();
235 |         }
236 | 
237 |         private void DrawSuccessStatus(string message)
238 |         {
239 |             var originalColor = GUI.color;
240 |             GUI.color = Color.green;
241 |             EditorGUILayout.LabelField($"✓ {message}", EditorStyles.boldLabel);
242 |             GUI.color = originalColor;
243 |             EditorGUILayout.Space();
244 |         }
245 | 
246 |         private void DrawErrorStatus(string message)
247 |         {
248 |             var originalColor = GUI.color;
249 |             GUI.color = Color.red;
250 |             EditorGUILayout.LabelField($"✗ {message}", EditorStyles.boldLabel);
251 |             GUI.color = originalColor;
252 |             EditorGUILayout.Space();
253 |         }
254 | 
255 |         private void DrawSimpleDependencyStatus(DependencyStatus dep)
256 |         {
257 |             EditorGUILayout.BeginHorizontal();
258 | 
259 |             var statusIcon = dep.IsAvailable ? "✓" : "✗";
260 |             var statusColor = dep.IsAvailable ? Color.green : Color.red;
261 | 
262 |             var originalColor = GUI.color;
263 |             GUI.color = statusColor;
264 |             GUILayout.Label(statusIcon, GUILayout.Width(20));
265 |             EditorGUILayout.LabelField(dep.Name, EditorStyles.boldLabel);
266 |             GUI.color = originalColor;
267 | 
268 |             if (!dep.IsAvailable && !string.IsNullOrEmpty(dep.ErrorMessage))
269 |             {
270 |                 EditorGUILayout.LabelField($"({dep.ErrorMessage})", EditorStyles.miniLabel);
271 |             }
272 | 
273 |             EditorGUILayout.EndHorizontal();
274 |         }
275 | 
276 |         private void DrawConfigureStep()
277 |         {
278 |             DrawSectionTitle("AI Client Configuration");
279 | 
280 |             // Check dependencies first (with caching to avoid heavy operations on every repaint)
281 |             if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2)
282 |             {
283 |                 _dependencyResult = DependencyManager.CheckAllDependencies();
284 |             }
285 |             if (!_dependencyResult.IsSystemReady)
286 |             {
287 |                 DrawErrorStatus("Cannot Configure - System Requirements Not Met");
288 | 
289 |                 EditorGUILayout.HelpBox(
290 |                     "Client configuration requires system dependencies to be installed first. Please complete setup before proceeding.",
291 |                     MessageType.Warning
292 |                 );
293 | 
294 |                 if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30)))
295 |                 {
296 |                     _currentStep = 0;
297 |                 }
298 |                 return;
299 |             }
300 | 
301 |             EditorGUILayout.LabelField(
302 |                 "Configure your AI assistants to work with Unity. Select a client below to set it up:",
303 |                 EditorStyles.wordWrappedLabel
304 |             );
305 |             EditorGUILayout.Space();
306 | 
307 |             // Client selection and configuration
308 |             if (_mcpClients.clients.Count > 0)
309 |             {
310 |                 // Client selector dropdown
311 |                 string[] clientNames = _mcpClients.clients.Select(c => c.name).ToArray();
312 |                 EditorGUI.BeginChangeCheck();
313 |                 _selectedClientIndex = EditorGUILayout.Popup("Select AI Client:", _selectedClientIndex, clientNames);
314 |                 if (EditorGUI.EndChangeCheck())
315 |                 {
316 |                     _selectedClientIndex = Mathf.Clamp(_selectedClientIndex, 0, _mcpClients.clients.Count - 1);
317 |                     // Refresh client status when selection changes
318 |                     CheckClientConfiguration(_mcpClients.clients[_selectedClientIndex]);
319 |                 }
320 | 
321 |                 EditorGUILayout.Space();
322 | 
323 |                 var selectedClient = _mcpClients.clients[_selectedClientIndex];
324 |                 DrawClientConfigurationInWizard(selectedClient);
325 | 
326 |                 EditorGUILayout.Space();
327 | 
328 |                 // Batch configuration option
329 |                 EditorGUILayout.BeginVertical(EditorStyles.helpBox);
330 |                 EditorGUILayout.LabelField("Quick Setup", EditorStyles.boldLabel);
331 |                 EditorGUILayout.LabelField(
332 |                     "Automatically configure all detected AI clients at once:",
333 |                     EditorStyles.wordWrappedLabel
334 |                 );
335 |                 EditorGUILayout.Space();
336 | 
337 |                 if (GUILayout.Button("Configure All Detected Clients", GUILayout.Height(30)))
338 |                 {
339 |                     ConfigureAllClientsInWizard();
340 |                 }
341 |                 EditorGUILayout.EndVertical();
342 |             }
343 |             else
344 |             {
345 |                 EditorGUILayout.HelpBox("No AI clients detected. Make sure you have Claude Code, Cursor, or VSCode installed.", MessageType.Info);
346 |             }
347 | 
348 |             EditorGUILayout.Space();
349 |             EditorGUILayout.HelpBox(
350 |                 "💡 You might need to restart your AI client after configuring.",
351 |                 MessageType.Info
352 |             );
353 |         }
354 | 
355 |         private void DrawFooter()
356 |         {
357 |             EditorGUILayout.Space();
358 |             EditorGUILayout.BeginHorizontal();
359 | 
360 |             // Back button
361 |             GUI.enabled = _currentStep > 0;
362 |             if (GUILayout.Button("Back", GUILayout.Width(60)))
363 |             {
364 |                 _currentStep--;
365 |             }
366 | 
367 |             GUILayout.FlexibleSpace();
368 | 
369 |             // Skip button
370 |             if (GUILayout.Button("Skip", GUILayout.Width(60)))
371 |             {
372 |                 bool dismiss = EditorUtility.DisplayDialog(
373 |                     "Skip Setup",
374 |                     "⚠️ Skipping setup will leave MCP for Unity non-functional!\n\n" +
375 |                     "You can restart setup from: Window > MCP for Unity > Setup Wizard (Required)",
376 |                     "Skip Anyway",
377 |                     "Cancel"
378 |                 );
379 | 
380 |                 if (dismiss)
381 |                 {
382 |                     SetupWizard.MarkSetupDismissed();
383 |                     Close();
384 |                 }
385 |             }
386 | 
387 |             // Next/Done button
388 |             GUI.enabled = true;
389 |             string buttonText = _currentStep == _stepTitles.Length - 1 ? "Done" : "Next";
390 | 
391 |             if (GUILayout.Button(buttonText, GUILayout.Width(80)))
392 |             {
393 |                 if (_currentStep == _stepTitles.Length - 1)
394 |                 {
395 |                     SetupWizard.MarkSetupCompleted();
396 |                     Close();
397 |                 }
398 |                 else
399 |                 {
400 |                     _currentStep++;
401 |                 }
402 |             }
403 | 
404 |             GUI.enabled = true;
405 |             EditorGUILayout.EndHorizontal();
406 |         }
407 | 
408 |         private void DrawClientConfigurationInWizard(McpClient client)
409 |         {
410 |             EditorGUILayout.BeginVertical(EditorStyles.helpBox);
411 | 
412 |             EditorGUILayout.LabelField($"{client.name} Configuration", EditorStyles.boldLabel);
413 |             EditorGUILayout.Space();
414 | 
415 |             // Show current status
416 |             var statusColor = GetClientStatusColor(client);
417 |             var originalColor = GUI.color;
418 |             GUI.color = statusColor;
419 |             EditorGUILayout.LabelField($"Status: {client.configStatus}", EditorStyles.label);
420 |             GUI.color = originalColor;
421 | 
422 |             EditorGUILayout.Space();
423 | 
424 |             // Configuration buttons
425 |             EditorGUILayout.BeginHorizontal();
426 | 
427 |             if (client.mcpType == McpTypes.ClaudeCode)
428 |             {
429 |                 // Special handling for Claude Code
430 |                 bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude());
431 |                 if (claudeAvailable)
432 |                 {
433 |                     bool isConfigured = client.status == McpStatus.Configured;
434 |                     string buttonText = isConfigured ? "Unregister" : "Register";
435 |                     if (GUILayout.Button($"{buttonText} with Claude Code"))
436 |                     {
437 |                         if (isConfigured)
438 |                         {
439 |                             UnregisterFromClaudeCode(client);
440 |                         }
441 |                         else
442 |                         {
443 |                             RegisterWithClaudeCode(client);
444 |                         }
445 |                     }
446 |                 }
447 |                 else
448 |                 {
449 |                     EditorGUILayout.HelpBox("Claude Code not found. Please install Claude Code first.", MessageType.Warning);
450 |                     if (GUILayout.Button("Open Claude Code Website"))
451 |                     {
452 |                         Application.OpenURL("https://claude.ai/download");
453 |                     }
454 |                 }
455 |             }
456 |             else
457 |             {
458 |                 // Standard client configuration
459 |                 if (GUILayout.Button($"Configure {client.name}"))
460 |                 {
461 |                     ConfigureClientInWizard(client);
462 |                 }
463 | 
464 |                 if (GUILayout.Button("Manual Setup"))
465 |                 {
466 |                     ShowManualSetupInWizard(client);
467 |                 }
468 |             }
469 | 
470 |             EditorGUILayout.EndHorizontal();
471 |             EditorGUILayout.EndVertical();
472 |         }
473 | 
474 |         private Color GetClientStatusColor(McpClient client)
475 |         {
476 |             return client.status switch
477 |             {
478 |                 McpStatus.Configured => Color.green,
479 |                 McpStatus.Running => Color.green,
480 |                 McpStatus.Connected => Color.green,
481 |                 McpStatus.IncorrectPath => Color.yellow,
482 |                 McpStatus.CommunicationError => Color.yellow,
483 |                 McpStatus.NoResponse => Color.yellow,
484 |                 _ => Color.red
485 |             };
486 |         }
487 | 
488 |         private void ConfigureClientInWizard(McpClient client)
489 |         {
490 |             try
491 |             {
492 |                 string result = PerformClientConfiguration(client);
493 | 
494 |                 EditorUtility.DisplayDialog(
495 |                     $"{client.name} Configuration",
496 |                     result,
497 |                     "OK"
498 |                 );
499 | 
500 |                 // Refresh client status
501 |                 CheckClientConfiguration(client);
502 |                 Repaint();
503 |             }
504 |             catch (System.Exception ex)
505 |             {
506 |                 EditorUtility.DisplayDialog(
507 |                     "Configuration Error",
508 |                     $"Failed to configure {client.name}: {ex.Message}",
509 |                     "OK"
510 |                 );
511 |             }
512 |         }
513 | 
514 |         private void ConfigureAllClientsInWizard()
515 |         {
516 |             int successCount = 0;
517 |             int totalCount = _mcpClients.clients.Count;
518 | 
519 |             foreach (var client in _mcpClients.clients)
520 |             {
521 |                 try
522 |                 {
523 |                     if (client.mcpType == McpTypes.ClaudeCode)
524 |                     {
525 |                         if (!string.IsNullOrEmpty(ExecPath.ResolveClaude()) && client.status != McpStatus.Configured)
526 |                         {
527 |                             RegisterWithClaudeCode(client);
528 |                             successCount++;
529 |                         }
530 |                         else if (client.status == McpStatus.Configured)
531 |                         {
532 |                             successCount++; // Already configured
533 |                         }
534 |                     }
535 |                     else
536 |                     {
537 |                         string result = PerformClientConfiguration(client);
538 |                         if (result.Contains("success", System.StringComparison.OrdinalIgnoreCase))
539 |                         {
540 |                             successCount++;
541 |                         }
542 |                     }
543 | 
544 |                     CheckClientConfiguration(client);
545 |                 }
546 |                 catch (System.Exception ex)
547 |                 {
548 |                     McpLog.Error($"Failed to configure {client.name}: {ex.Message}");
549 |                 }
550 |             }
551 | 
552 |             EditorUtility.DisplayDialog(
553 |                 "Batch Configuration Complete",
554 |                 $"Successfully configured {successCount} out of {totalCount} clients.\n\n" +
555 |                 "Restart your AI clients for changes to take effect.",
556 |                 "OK"
557 |             );
558 | 
559 |             Repaint();
560 |         }
561 | 
562 |         private void RegisterWithClaudeCode(McpClient client)
563 |         {
564 |             try
565 |             {
566 |                 string pythonDir = McpPathResolver.FindPackagePythonDirectory();
567 |                 string claudePath = ExecPath.ResolveClaude();
568 |                 string uvPath = ExecPath.ResolveUv() ?? "uv";
569 | 
570 |                 string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py";
571 | 
572 |                 if (!ExecPath.TryRun(claudePath, args, null, out var stdout, out var stderr, 15000, McpPathResolver.GetPathPrepend()))
573 |                 {
574 |                     if ((stdout + stderr).Contains("already exists", System.StringComparison.OrdinalIgnoreCase))
575 |                     {
576 |                         CheckClientConfiguration(client);
577 |                         EditorUtility.DisplayDialog("Claude Code", "MCP for Unity is already registered with Claude Code.", "OK");
578 |                     }
579 |                     else
580 |                     {
581 |                         throw new System.Exception($"Registration failed: {stderr}");
582 |                     }
583 |                 }
584 |                 else
585 |                 {
586 |                     CheckClientConfiguration(client);
587 |                     EditorUtility.DisplayDialog("Claude Code", "Successfully registered MCP for Unity with Claude Code!", "OK");
588 |                 }
589 |             }
590 |             catch (System.Exception ex)
591 |             {
592 |                 EditorUtility.DisplayDialog("Registration Error", $"Failed to register with Claude Code: {ex.Message}", "OK");
593 |             }
594 |         }
595 | 
596 |         private void UnregisterFromClaudeCode(McpClient client)
597 |         {
598 |             try
599 |             {
600 |                 string claudePath = ExecPath.ResolveClaude();
601 |                 if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", null, out var stdout, out var stderr, 10000, McpPathResolver.GetPathPrepend()))
602 |                 {
603 |                     CheckClientConfiguration(client);
604 |                     EditorUtility.DisplayDialog("Claude Code", "Successfully unregistered MCP for Unity from Claude Code.", "OK");
605 |                 }
606 |                 else
607 |                 {
608 |                     throw new System.Exception($"Unregistration failed: {stderr}");
609 |                 }
610 |             }
611 |             catch (System.Exception ex)
612 |             {
613 |                 EditorUtility.DisplayDialog("Unregistration Error", $"Failed to unregister from Claude Code: {ex.Message}", "OK");
614 |             }
615 |         }
616 | 
617 |         private string PerformClientConfiguration(McpClient client)
618 |         {
619 |             // This mirrors the logic from MCPForUnityEditorWindow.ConfigureMcpClient
620 |             string configPath = McpConfigurationHelper.GetClientConfigPath(client);
621 |             string pythonDir = McpPathResolver.FindPackagePythonDirectory();
622 | 
623 |             if (string.IsNullOrEmpty(pythonDir))
624 |             {
625 |                 return "Manual configuration required - Python server directory not found.";
626 |             }
627 | 
628 |             McpConfigurationHelper.EnsureConfigDirectoryExists(configPath);
629 |             return McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client);
630 |         }
631 | 
632 |         private void ShowManualSetupInWizard(McpClient client)
633 |         {
634 |             string configPath = McpConfigurationHelper.GetClientConfigPath(client);
635 |             string pythonDir = McpPathResolver.FindPackagePythonDirectory();
636 |             string uvPath = ServerInstaller.FindUvPath();
637 | 
638 |             if (string.IsNullOrEmpty(uvPath))
639 |             {
640 |                 EditorUtility.DisplayDialog("Manual Setup", "UV package manager not found. Please install UV first.", "OK");
641 |                 return;
642 |             }
643 | 
644 |             // Build manual configuration using the sophisticated helper logic
645 |             string result = McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client);
646 |             string manualConfig;
647 | 
648 |             if (result == "Configured successfully")
649 |             {
650 |                 // Read back the configuration that was written
651 |                 try
652 |                 {
653 |                     manualConfig = System.IO.File.ReadAllText(configPath);
654 |                 }
655 |                 catch
656 |                 {
657 |                     manualConfig = "Configuration written successfully, but could not read back for display.";
658 |                 }
659 |             }
660 |             else
661 |             {
662 |                 manualConfig = $"Configuration failed: {result}";
663 |             }
664 | 
665 |             EditorUtility.DisplayDialog(
666 |                 $"Manual Setup - {client.name}",
667 |                 $"Configuration file location:\n{configPath}\n\n" +
668 |                 $"Configuration result:\n{manualConfig}",
669 |                 "OK"
670 |             );
671 |         }
672 | 
673 |         private void CheckClientConfiguration(McpClient client)
674 |         {
675 |             // Basic status check - could be enhanced to mirror MCPForUnityEditorWindow logic
676 |             try
677 |             {
678 |                 string configPath = McpConfigurationHelper.GetClientConfigPath(client);
679 |                 if (System.IO.File.Exists(configPath))
680 |                 {
681 |                     client.configStatus = "Configured";
682 |                     client.status = McpStatus.Configured;
683 |                 }
684 |                 else
685 |                 {
686 |                     client.configStatus = "Not Configured";
687 |                     client.status = McpStatus.NotConfigured;
688 |                 }
689 |             }
690 |             catch
691 |             {
692 |                 client.configStatus = "Error";
693 |                 client.status = McpStatus.Error;
694 |             }
695 |         }
696 | 
697 |         private void OpenInstallationUrls()
698 |         {
699 |             var (pythonUrl, uvUrl) = DependencyManager.GetInstallationUrls();
700 | 
701 |             bool openPython = EditorUtility.DisplayDialog(
702 |                 "Open Installation URLs",
703 |                 "Open Python installation page?",
704 |                 "Yes",
705 |                 "No"
706 |             );
707 | 
708 |             if (openPython)
709 |             {
710 |                 Application.OpenURL(pythonUrl);
711 |             }
712 | 
713 |             bool openUV = EditorUtility.DisplayDialog(
714 |                 "Open Installation URLs",
715 |                 "Open UV installation page?",
716 |                 "Yes",
717 |                 "No"
718 |             );
719 | 
720 |             if (openUV)
721 |             {
722 |                 Application.OpenURL(uvUrl);
723 |             }
724 |         }
725 |     }
726 | }
727 | 
```

--------------------------------------------------------------------------------
/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectTests.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.Collections;
  4 | using NUnit.Framework;
  5 | using UnityEngine;
  6 | using UnityEditor;
  7 | using UnityEngine.TestTools;
  8 | using Newtonsoft.Json.Linq;
  9 | using MCPForUnity.Editor.Tools;
 10 | 
 11 | namespace MCPForUnityTests.Editor.Tools
 12 | {
 13 |     public class ManageGameObjectTests
 14 |     {
 15 |         private GameObject testGameObject;
 16 | 
 17 |         [SetUp]
 18 |         public void SetUp()
 19 |         {
 20 |             // Create a test GameObject for each test
 21 |             testGameObject = new GameObject("TestObject");
 22 |         }
 23 | 
 24 |         [TearDown]
 25 |         public void TearDown()
 26 |         {
 27 |             // Clean up test GameObject
 28 |             if (testGameObject != null)
 29 |             {
 30 |                 UnityEngine.Object.DestroyImmediate(testGameObject);
 31 |             }
 32 |         }
 33 | 
 34 |         [Test]
 35 |         public void HandleCommand_ReturnsError_ForNullParams()
 36 |         {
 37 |             var result = ManageGameObject.HandleCommand(null);
 38 | 
 39 |             Assert.IsNotNull(result, "Should return a result object");
 40 |             // Note: Actual error checking would need access to Response structure
 41 |         }
 42 | 
 43 |         [Test]
 44 |         public void HandleCommand_ReturnsError_ForEmptyParams()
 45 |         {
 46 |             var emptyParams = new JObject();
 47 |             var result = ManageGameObject.HandleCommand(emptyParams);
 48 | 
 49 |             Assert.IsNotNull(result, "Should return a result object for empty params");
 50 |         }
 51 | 
 52 |         [Test]
 53 |         public void HandleCommand_ProcessesValidCreateAction()
 54 |         {
 55 |             var createParams = new JObject
 56 |             {
 57 |                 ["action"] = "create",
 58 |                 ["name"] = "TestCreateObject"
 59 |             };
 60 | 
 61 |             var result = ManageGameObject.HandleCommand(createParams);
 62 | 
 63 |             Assert.IsNotNull(result, "Should return a result for valid create action");
 64 | 
 65 |             // Clean up - find and destroy the created object
 66 |             var createdObject = GameObject.Find("TestCreateObject");
 67 |             if (createdObject != null)
 68 |             {
 69 |                 UnityEngine.Object.DestroyImmediate(createdObject);
 70 |             }
 71 |         }
 72 | 
 73 |         [Test]
 74 |         public void ComponentResolver_Integration_WorksWithRealComponents()
 75 |         {
 76 |             // Test that our ComponentResolver works with actual Unity components
 77 |             var transformResult = ComponentResolver.TryResolve("Transform", out Type transformType, out string error);
 78 | 
 79 |             Assert.IsTrue(transformResult, "Should resolve Transform component");
 80 |             Assert.AreEqual(typeof(Transform), transformType, "Should return correct Transform type");
 81 |             Assert.IsEmpty(error, "Should have no error for valid component");
 82 |         }
 83 | 
 84 |         [Test]
 85 |         public void ComponentResolver_Integration_WorksWithBuiltInComponents()
 86 |         {
 87 |             var components = new[]
 88 |             {
 89 |                 ("Rigidbody", typeof(Rigidbody)),
 90 |                 ("Collider", typeof(Collider)),
 91 |                 ("Renderer", typeof(Renderer)),
 92 |                 ("Camera", typeof(Camera)),
 93 |                 ("Light", typeof(Light))
 94 |             };
 95 | 
 96 |             foreach (var (componentName, expectedType) in components)
 97 |             {
 98 |                 var result = ComponentResolver.TryResolve(componentName, out Type actualType, out string error);
 99 | 
100 |                 // Some components might not resolve (abstract classes), but the method should handle gracefully
101 |                 if (result)
102 |                 {
103 |                     Assert.IsTrue(expectedType.IsAssignableFrom(actualType),
104 |                         $"{componentName} should resolve to assignable type");
105 |                 }
106 |                 else
107 |                 {
108 |                     Assert.IsNotEmpty(error, $"Should have error message for {componentName}");
109 |                 }
110 |             }
111 |         }
112 | 
113 |         [Test]
114 |         public void PropertyMatching_Integration_WorksWithRealGameObject()
115 |         {
116 |             // Add a Rigidbody to test real property matching
117 |             var rigidbody = testGameObject.AddComponent<Rigidbody>();
118 | 
119 |             var properties = ComponentResolver.GetAllComponentProperties(typeof(Rigidbody));
120 | 
121 |             Assert.IsNotEmpty(properties, "Rigidbody should have properties");
122 |             Assert.Contains("mass", properties, "Rigidbody should have mass property");
123 |             Assert.Contains("useGravity", properties, "Rigidbody should have useGravity property");
124 | 
125 |             // Test AI suggestions
126 |             var suggestions = ComponentResolver.GetAIPropertySuggestions("Use Gravity", properties);
127 |             Assert.Contains("useGravity", suggestions, "Should suggest useGravity for 'Use Gravity'");
128 |         }
129 | 
130 |         [Test]
131 |         public void PropertyMatching_HandlesMonoBehaviourProperties()
132 |         {
133 |             var properties = ComponentResolver.GetAllComponentProperties(typeof(MonoBehaviour));
134 | 
135 |             Assert.IsNotEmpty(properties, "MonoBehaviour should have properties");
136 |             Assert.Contains("enabled", properties, "MonoBehaviour should have enabled property");
137 |             Assert.Contains("name", properties, "MonoBehaviour should have name property");
138 |             Assert.Contains("tag", properties, "MonoBehaviour should have tag property");
139 |         }
140 | 
141 |         [Test]
142 |         public void PropertyMatching_HandlesCaseVariations()
143 |         {
144 |             var testProperties = new List<string> { "maxReachDistance", "playerHealth", "movementSpeed" };
145 | 
146 |             var testCases = new[]
147 |             {
148 |                 ("max reach distance", "maxReachDistance"),
149 |                 ("Max Reach Distance", "maxReachDistance"),
150 |                 ("MAX_REACH_DISTANCE", "maxReachDistance"),
151 |                 ("player health", "playerHealth"),
152 |                 ("movement speed", "movementSpeed")
153 |             };
154 | 
155 |             foreach (var (input, expected) in testCases)
156 |             {
157 |                 var suggestions = ComponentResolver.GetAIPropertySuggestions(input, testProperties);
158 |                 Assert.Contains(expected, suggestions, $"Should suggest {expected} for input '{input}'");
159 |             }
160 |         }
161 | 
162 |         [Test]
163 |         public void ErrorHandling_ReturnsHelpfulMessages()
164 |         {
165 |             // This test verifies that error messages are helpful and contain suggestions
166 |             var testProperties = new List<string> { "mass", "velocity", "drag", "useGravity" };
167 |             var suggestions = ComponentResolver.GetAIPropertySuggestions("weight", testProperties);
168 | 
169 |             // Even if no perfect match, should return valid list
170 |             Assert.IsNotNull(suggestions, "Should return valid suggestions list");
171 | 
172 |             // Test with completely invalid input
173 |             var badSuggestions = ComponentResolver.GetAIPropertySuggestions("xyz123invalid", testProperties);
174 |             Assert.IsNotNull(badSuggestions, "Should handle invalid input gracefully");
175 |         }
176 | 
177 |         [Test]
178 |         public void PerformanceTest_CachingWorks()
179 |         {
180 |             var properties = ComponentResolver.GetAllComponentProperties(typeof(Transform));
181 |             var input = "Test Property Name";
182 | 
183 |             // First call - populate cache
184 |             var startTime = System.DateTime.UtcNow;
185 |             var suggestions1 = ComponentResolver.GetAIPropertySuggestions(input, properties);
186 |             var firstCallTime = (System.DateTime.UtcNow - startTime).TotalMilliseconds;
187 | 
188 |             // Second call - should use cache
189 |             startTime = System.DateTime.UtcNow;
190 |             var suggestions2 = ComponentResolver.GetAIPropertySuggestions(input, properties);
191 |             var secondCallTime = (System.DateTime.UtcNow - startTime).TotalMilliseconds;
192 | 
193 |             Assert.AreEqual(suggestions1.Count, suggestions2.Count, "Cached results should be identical");
194 |             CollectionAssert.AreEqual(suggestions1, suggestions2, "Cached results should match exactly");
195 | 
196 |             // Second call should be faster (though this test might be flaky)
197 |             Assert.LessOrEqual(secondCallTime, firstCallTime * 2, "Cached call should not be significantly slower");
198 |         }
199 | 
200 |         [Test]
201 |         public void SetComponentProperties_CollectsAllFailuresAndAppliesValidOnes()
202 |         {
203 |             // Arrange - add Transform and Rigidbody components to test with
204 |             var transform = testGameObject.transform;
205 |             var rigidbody = testGameObject.AddComponent<Rigidbody>();
206 | 
207 |             // Create a params object with mixed valid and invalid properties
208 |             var setPropertiesParams = new JObject
209 |             {
210 |                 ["action"] = "modify",
211 |                 ["target"] = testGameObject.name,
212 |                 ["search_method"] = "by_name",
213 |                 ["componentProperties"] = new JObject
214 |                 {
215 |                     ["Transform"] = new JObject
216 |                     {
217 |                         ["localPosition"] = new JObject { ["x"] = 1.0f, ["y"] = 2.0f, ["z"] = 3.0f },  // Valid
218 |                         ["rotatoin"] = new JObject { ["x"] = 0.0f, ["y"] = 90.0f, ["z"] = 0.0f }, // Invalid (typo - should be rotation)
219 |                         ["localScale"] = new JObject { ["x"] = 2.0f, ["y"] = 2.0f, ["z"] = 2.0f }      // Valid
220 |                     },
221 |                     ["Rigidbody"] = new JObject
222 |                     {
223 |                         ["mass"] = 5.0f,            // Valid
224 |                         ["invalidProp"] = "test",   // Invalid - doesn't exist
225 |                         ["useGravity"] = true       // Valid
226 |                     }
227 |                 }
228 |             };
229 | 
230 |             // Store original values to verify changes  
231 |             var originalLocalPosition = transform.localPosition;
232 |             var originalLocalScale = transform.localScale;
233 |             var originalMass = rigidbody.mass;
234 |             var originalUseGravity = rigidbody.useGravity;
235 | 
236 |             Debug.Log($"BEFORE TEST - Mass: {rigidbody.mass}, UseGravity: {rigidbody.useGravity}");
237 | 
238 |             // Expect the warning logs from the invalid properties
239 |             LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex("Property 'rotatoin' not found"));
240 |             LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex("Property 'invalidProp' not found"));
241 | 
242 |             // Act
243 |             var result = ManageGameObject.HandleCommand(setPropertiesParams);
244 | 
245 |             Debug.Log($"AFTER TEST - Mass: {rigidbody.mass}, UseGravity: {rigidbody.useGravity}");
246 |             Debug.Log($"AFTER TEST - LocalPosition: {transform.localPosition}");
247 |             Debug.Log($"AFTER TEST - LocalScale: {transform.localScale}");
248 | 
249 |             // Assert - verify that valid properties were set despite invalid ones
250 |             Assert.AreEqual(new Vector3(1.0f, 2.0f, 3.0f), transform.localPosition,
251 |                 "Valid localPosition should be set even with other invalid properties");
252 |             Assert.AreEqual(new Vector3(2.0f, 2.0f, 2.0f), transform.localScale,
253 |                 "Valid localScale should be set even with other invalid properties");
254 |             Assert.AreEqual(5.0f, rigidbody.mass, 0.001f,
255 |                 "Valid mass should be set even with other invalid properties");
256 |             Assert.AreEqual(true, rigidbody.useGravity,
257 |                 "Valid useGravity should be set even with other invalid properties");
258 | 
259 |             // Verify the result indicates errors (since we had invalid properties)
260 |             Assert.IsNotNull(result, "Should return a result object");
261 | 
262 |             // The collect-and-continue behavior means we should get an error response 
263 |             // that contains info about the failed properties, but valid ones were still applied
264 |             // This proves the collect-and-continue behavior is working
265 | 
266 |             // Harden: verify structured error response with failures list contains both invalid fields
267 |             var successProp = result.GetType().GetProperty("success");
268 |             Assert.IsNotNull(successProp, "Result should expose 'success' property");
269 |             Assert.IsFalse((bool)successProp.GetValue(result), "Result.success should be false for partial failure");
270 | 
271 |             var dataProp = result.GetType().GetProperty("data");
272 |             Assert.IsNotNull(dataProp, "Result should include 'data' with errors");
273 |             var dataVal = dataProp.GetValue(result);
274 |             Assert.IsNotNull(dataVal, "Result.data should not be null");
275 |             var errorsProp = dataVal.GetType().GetProperty("errors");
276 |             Assert.IsNotNull(errorsProp, "Result.data should include 'errors' list");
277 |             var errorsEnum = errorsProp.GetValue(dataVal) as System.Collections.IEnumerable;
278 |             Assert.IsNotNull(errorsEnum, "errors should be enumerable");
279 | 
280 |             bool foundRotatoin = false;
281 |             bool foundInvalidProp = false;
282 |             foreach (var err in errorsEnum)
283 |             {
284 |                 string s = err?.ToString() ?? string.Empty;
285 |                 if (s.Contains("rotatoin")) foundRotatoin = true;
286 |                 if (s.Contains("invalidProp")) foundInvalidProp = true;
287 |             }
288 |             Assert.IsTrue(foundRotatoin, "errors should mention the misspelled 'rotatoin' property");
289 |             Assert.IsTrue(foundInvalidProp, "errors should mention the 'invalidProp' property");
290 |         }
291 | 
292 |         [Test]
293 |         public void SetComponentProperties_ContinuesAfterException()
294 |         {
295 |             // Arrange - create scenario that might cause exceptions
296 |             var rigidbody = testGameObject.AddComponent<Rigidbody>();
297 | 
298 |             // Set initial values that we'll change
299 |             rigidbody.mass = 1.0f;
300 |             rigidbody.useGravity = true;
301 | 
302 |             var setPropertiesParams = new JObject
303 |             {
304 |                 ["action"] = "modify",
305 |                 ["target"] = testGameObject.name,
306 |                 ["search_method"] = "by_name",
307 |                 ["componentProperties"] = new JObject
308 |                 {
309 |                     ["Rigidbody"] = new JObject
310 |                     {
311 |                         ["mass"] = 2.5f,                    // Valid - should be set
312 |                         ["velocity"] = "invalid_type",      // Invalid type - will cause exception  
313 |                         ["useGravity"] = false              // Valid - should still be set after exception
314 |                     }
315 |                 }
316 |             };
317 | 
318 |             // Expect the error logs from the invalid property
319 |             LogAssert.Expect(LogType.Error, new System.Text.RegularExpressions.Regex("Unexpected error converting token to UnityEngine.Vector3"));
320 |             LogAssert.Expect(LogType.Error, new System.Text.RegularExpressions.Regex("SetProperty.*Failed to set 'velocity'"));
321 |             LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex("Property 'velocity' not found"));
322 | 
323 |             // Act
324 |             var result = ManageGameObject.HandleCommand(setPropertiesParams);
325 | 
326 |             // Assert - verify that valid properties before AND after the exception were still set
327 |             Assert.AreEqual(2.5f, rigidbody.mass, 0.001f,
328 |                 "Mass should be set even if later property causes exception");
329 |             Assert.AreEqual(false, rigidbody.useGravity,
330 |                 "UseGravity should be set even if previous property caused exception");
331 | 
332 |             Assert.IsNotNull(result, "Should return a result even with exceptions");
333 | 
334 |             // The key test: processing continued after the exception and set useGravity
335 |             // This proves the collect-and-continue behavior works even with exceptions
336 | 
337 |             // Harden: verify structured error response contains velocity failure
338 |             var successProp2 = result.GetType().GetProperty("success");
339 |             Assert.IsNotNull(successProp2, "Result should expose 'success' property");
340 |             Assert.IsFalse((bool)successProp2.GetValue(result), "Result.success should be false when an exception occurs for a property");
341 | 
342 |             var dataProp2 = result.GetType().GetProperty("data");
343 |             Assert.IsNotNull(dataProp2, "Result should include 'data' with errors");
344 |             var dataVal2 = dataProp2.GetValue(result);
345 |             Assert.IsNotNull(dataVal2, "Result.data should not be null");
346 |             var errorsProp2 = dataVal2.GetType().GetProperty("errors");
347 |             Assert.IsNotNull(errorsProp2, "Result.data should include 'errors' list");
348 |             var errorsEnum2 = errorsProp2.GetValue(dataVal2) as System.Collections.IEnumerable;
349 |             Assert.IsNotNull(errorsEnum2, "errors should be enumerable");
350 | 
351 |             bool foundVelocityError = false;
352 |             foreach (var err in errorsEnum2)
353 |             {
354 |                 string s = err?.ToString() ?? string.Empty;
355 |                 if (s.Contains("velocity")) { foundVelocityError = true; break; }
356 |             }
357 |             Assert.IsTrue(foundVelocityError, "errors should include a message referencing 'velocity'");
358 |         }
359 | 
360 |         [Test]
361 |         public void GetComponentData_DoesNotInstantiateMaterialsInEditMode()
362 |         {
363 |             // Arrange - Create a GameObject with MeshRenderer and MeshFilter components
364 |             var testObject = new GameObject("MaterialMeshTestObject");
365 |             var meshRenderer = testObject.AddComponent<MeshRenderer>();
366 |             var meshFilter = testObject.AddComponent<MeshFilter>();
367 |             
368 |             // Create a simple material and mesh for testing
369 |             var testMaterial = new Material(Shader.Find("Standard"));
370 |             var tempCube = GameObject.CreatePrimitive(PrimitiveType.Cube);
371 |             var testMesh = tempCube.GetComponent<MeshFilter>().sharedMesh;
372 |             UnityEngine.Object.DestroyImmediate(tempCube);
373 |             
374 |             // Set the shared material and mesh (these should be used in edit mode)
375 |             meshRenderer.sharedMaterial = testMaterial;
376 |             meshFilter.sharedMesh = testMesh;
377 |             
378 |             // Act - Get component data which should trigger material/mesh property access
379 |             var prevIgnore = LogAssert.ignoreFailingMessages;
380 |             LogAssert.ignoreFailingMessages = true; // Avoid failing due to incidental editor logs during reflection
381 |             object result;
382 |             try
383 |             {
384 |                 result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshRenderer);
385 |             }
386 |             finally
387 |             {
388 |                 LogAssert.ignoreFailingMessages = prevIgnore;
389 |             }
390 |             
391 |             // Assert - Basic success and shape tolerance
392 |             Assert.IsNotNull(result, "GetComponentData should return a result");
393 |             if (result is Dictionary<string, object> dict &&
394 |                 dict.TryGetValue("properties", out var propsObj) &&
395 |                 propsObj is Dictionary<string, object> properties)
396 |             {
397 |                 Assert.IsTrue(properties.ContainsKey("material") || properties.ContainsKey("sharedMaterial") || properties.ContainsKey("materials") || properties.ContainsKey("sharedMaterials"),
398 |                     "Serialized data should include a material-related key when present.");
399 |             }
400 |             
401 |             // Clean up
402 |             UnityEngine.Object.DestroyImmediate(testMaterial);
403 |             UnityEngine.Object.DestroyImmediate(testObject);
404 |         }
405 | 
406 |         [Test]
407 |         public void GetComponentData_DoesNotInstantiateMeshesInEditMode()
408 |         {
409 |             // Arrange - Create a GameObject with MeshFilter component
410 |             var testObject = new GameObject("MeshTestObject");
411 |             var meshFilter = testObject.AddComponent<MeshFilter>();
412 |             
413 |             // Create a simple mesh for testing
414 |             var tempSphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
415 |             var testMesh = tempSphere.GetComponent<MeshFilter>().sharedMesh;
416 |             UnityEngine.Object.DestroyImmediate(tempSphere);
417 |             meshFilter.sharedMesh = testMesh;
418 |             
419 |             // Act - Get component data which should trigger mesh property access
420 |             var prevIgnore2 = LogAssert.ignoreFailingMessages;
421 |             LogAssert.ignoreFailingMessages = true;
422 |             object result;
423 |             try
424 |             {
425 |                 result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshFilter);
426 |             }
427 |             finally
428 |             {
429 |                 LogAssert.ignoreFailingMessages = prevIgnore2;
430 |             }
431 |             
432 |             // Assert - Basic success and shape tolerance
433 |             Assert.IsNotNull(result, "GetComponentData should return a result");
434 |             if (result is Dictionary<string, object> dict2 &&
435 |                 dict2.TryGetValue("properties", out var propsObj2) &&
436 |                 propsObj2 is Dictionary<string, object> properties2)
437 |             {
438 |                 Assert.IsTrue(properties2.ContainsKey("mesh") || properties2.ContainsKey("sharedMesh"),
439 |                     "Serialized data should include a mesh-related key when present.");
440 |             }
441 |             
442 |             // Clean up
443 |             UnityEngine.Object.DestroyImmediate(testObject);
444 |         }
445 | 
446 |         [Test]
447 |         public void GetComponentData_UsesSharedMaterialInEditMode()
448 |         {
449 |             // Arrange - Create a GameObject with MeshRenderer
450 |             var testObject = new GameObject("SharedMaterialTestObject");
451 |             var meshRenderer = testObject.AddComponent<MeshRenderer>();
452 |             
453 |             // Create a test material
454 |             var testMaterial = new Material(Shader.Find("Standard"));
455 |             testMaterial.name = "TestMaterial";
456 |             meshRenderer.sharedMaterial = testMaterial;
457 |             
458 |             // Act - Get component data in edit mode
459 |             var result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshRenderer);
460 |             
461 |             // Assert - Verify that the material property was accessed without instantiation
462 |             Assert.IsNotNull(result, "GetComponentData should return a result");
463 |             
464 |             // Check that result is a dictionary with properties key
465 |             if (result is Dictionary<string, object> resultDict && 
466 |                 resultDict.TryGetValue("properties", out var propertiesObj) &&
467 |                 propertiesObj is Dictionary<string, object> properties)
468 |             {
469 |                 Assert.IsTrue(properties.ContainsKey("material") || properties.ContainsKey("sharedMaterial"),
470 |                     "Serialized data should include 'material' or 'sharedMaterial' when present.");
471 |             }
472 |             
473 |             // Clean up
474 |             UnityEngine.Object.DestroyImmediate(testMaterial);
475 |             UnityEngine.Object.DestroyImmediate(testObject);
476 |         }
477 | 
478 |         [Test]
479 |         public void GetComponentData_UsesSharedMeshInEditMode()
480 |         {
481 |             // Arrange - Create a GameObject with MeshFilter
482 |             var testObject = new GameObject("SharedMeshTestObject");
483 |             var meshFilter = testObject.AddComponent<MeshFilter>();
484 |             
485 |             // Create a test mesh
486 |             var tempCylinder = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
487 |             var testMesh = tempCylinder.GetComponent<MeshFilter>().sharedMesh;
488 |             UnityEngine.Object.DestroyImmediate(tempCylinder);
489 |             testMesh.name = "TestMesh";
490 |             meshFilter.sharedMesh = testMesh;
491 |             
492 |             // Act - Get component data in edit mode
493 |             var result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshFilter);
494 |             
495 |             // Assert - Verify that the mesh property was accessed without instantiation
496 |             Assert.IsNotNull(result, "GetComponentData should return a result");
497 |             
498 |             // Check that result is a dictionary with properties key
499 |             if (result is Dictionary<string, object> resultDict && 
500 |                 resultDict.TryGetValue("properties", out var propertiesObj) &&
501 |                 propertiesObj is Dictionary<string, object> properties)
502 |             {
503 |                 Assert.IsTrue(properties.ContainsKey("mesh") || properties.ContainsKey("sharedMesh"),
504 |                     "Serialized data should include 'mesh' or 'sharedMesh' when present.");
505 |             }
506 |             
507 |             // Clean up
508 |             UnityEngine.Object.DestroyImmediate(testObject);
509 |         }
510 | 
511 |         [Test]
512 |         public void GetComponentData_HandlesNullMaterialsAndMeshes()
513 |         {
514 |             // Arrange - Create a GameObject with MeshRenderer and MeshFilter but no materials/meshes
515 |             var testObject = new GameObject("NullMaterialMeshTestObject");
516 |             var meshRenderer = testObject.AddComponent<MeshRenderer>();
517 |             var meshFilter = testObject.AddComponent<MeshFilter>();
518 |             
519 |             // Don't set any materials or meshes - they should be null
520 |             
521 |             // Act - Get component data
522 |             var rendererResult = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshRenderer);
523 |             var meshFilterResult = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshFilter);
524 |             
525 |             // Assert - Verify that the operations succeeded even with null materials/meshes
526 |             Assert.IsNotNull(rendererResult, "GetComponentData should handle null materials");
527 |             Assert.IsNotNull(meshFilterResult, "GetComponentData should handle null meshes");
528 |             
529 |             // Clean up
530 |             UnityEngine.Object.DestroyImmediate(testObject);
531 |         }
532 | 
533 |         [Test]
534 |         public void GetComponentData_WorksWithMultipleMaterials()
535 |         {
536 |             // Arrange - Create a GameObject with MeshRenderer that has multiple materials
537 |             var testObject = new GameObject("MultiMaterialTestObject");
538 |             var meshRenderer = testObject.AddComponent<MeshRenderer>();
539 |             
540 |             // Create multiple test materials
541 |             var material1 = new Material(Shader.Find("Standard"));
542 |             material1.name = "TestMaterial1";
543 |             var material2 = new Material(Shader.Find("Standard"));
544 |             material2.name = "TestMaterial2";
545 |             
546 |             meshRenderer.sharedMaterials = new Material[] { material1, material2 };
547 |             
548 |             // Act - Get component data
549 |             var result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshRenderer);
550 |             
551 |             // Assert - Verify that the operation succeeded with multiple materials
552 |             Assert.IsNotNull(result, "GetComponentData should handle multiple materials");
553 |             
554 |             // Clean up
555 |             UnityEngine.Object.DestroyImmediate(material1);
556 |             UnityEngine.Object.DestroyImmediate(material2);
557 |             UnityEngine.Object.DestroyImmediate(testObject);
558 |         }
559 |     }
560 | }
561 | 
```

--------------------------------------------------------------------------------
/MCPForUnity/Editor/Helpers/GameObjectSerializer.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.Linq;
  4 | using System.Reflection;
  5 | using Newtonsoft.Json;
  6 | using Newtonsoft.Json.Linq;
  7 | using UnityEditor;
  8 | using UnityEngine;
  9 | using MCPForUnity.Runtime.Serialization; // For Converters
 10 | 
 11 | namespace MCPForUnity.Editor.Helpers
 12 | {
 13 |     /// <summary>
 14 |     /// Handles serialization of GameObjects and Components for MCP responses.
 15 |     /// Includes reflection helpers and caching for performance.
 16 |     /// </summary> 
 17 |     public static class GameObjectSerializer
 18 |     {
 19 |         // --- Data Serialization ---
 20 | 
 21 |         /// <summary>
 22 |         /// Creates a serializable representation of a GameObject.
 23 |         /// </summary>
 24 |         public static object GetGameObjectData(GameObject go)
 25 |         {
 26 |             if (go == null)
 27 |                 return null;
 28 |             return new
 29 |             {
 30 |                 name = go.name,
 31 |                 instanceID = go.GetInstanceID(),
 32 |                 tag = go.tag,
 33 |                 layer = go.layer,
 34 |                 activeSelf = go.activeSelf,
 35 |                 activeInHierarchy = go.activeInHierarchy,
 36 |                 isStatic = go.isStatic,
 37 |                 scenePath = go.scene.path, // Identify which scene it belongs to
 38 |                 transform = new // Serialize transform components carefully to avoid JSON issues
 39 |                 {
 40 |                     // Serialize Vector3 components individually to prevent self-referencing loops.
 41 |                     // The default serializer can struggle with properties like Vector3.normalized.
 42 |                     position = new
 43 |                     {
 44 |                         x = go.transform.position.x,
 45 |                         y = go.transform.position.y,
 46 |                         z = go.transform.position.z,
 47 |                     },
 48 |                     localPosition = new
 49 |                     {
 50 |                         x = go.transform.localPosition.x,
 51 |                         y = go.transform.localPosition.y,
 52 |                         z = go.transform.localPosition.z,
 53 |                     },
 54 |                     rotation = new
 55 |                     {
 56 |                         x = go.transform.rotation.eulerAngles.x,
 57 |                         y = go.transform.rotation.eulerAngles.y,
 58 |                         z = go.transform.rotation.eulerAngles.z,
 59 |                     },
 60 |                     localRotation = new
 61 |                     {
 62 |                         x = go.transform.localRotation.eulerAngles.x,
 63 |                         y = go.transform.localRotation.eulerAngles.y,
 64 |                         z = go.transform.localRotation.eulerAngles.z,
 65 |                     },
 66 |                     scale = new
 67 |                     {
 68 |                         x = go.transform.localScale.x,
 69 |                         y = go.transform.localScale.y,
 70 |                         z = go.transform.localScale.z,
 71 |                     },
 72 |                     forward = new
 73 |                     {
 74 |                         x = go.transform.forward.x,
 75 |                         y = go.transform.forward.y,
 76 |                         z = go.transform.forward.z,
 77 |                     },
 78 |                     up = new
 79 |                     {
 80 |                         x = go.transform.up.x,
 81 |                         y = go.transform.up.y,
 82 |                         z = go.transform.up.z,
 83 |                     },
 84 |                     right = new
 85 |                     {
 86 |                         x = go.transform.right.x,
 87 |                         y = go.transform.right.y,
 88 |                         z = go.transform.right.z,
 89 |                     },
 90 |                 },
 91 |                 parentInstanceID = go.transform.parent?.gameObject.GetInstanceID() ?? 0, // 0 if no parent
 92 |                 // Optionally include components, but can be large
 93 |                 // components = go.GetComponents<Component>().Select(c => GetComponentData(c)).ToList()
 94 |                 // Or just component names:
 95 |                 componentNames = go.GetComponents<Component>()
 96 |                     .Select(c => c.GetType().FullName)
 97 |                     .ToList(),
 98 |             };
 99 |         }
100 | 
101 |         // --- Metadata Caching for Reflection ---
102 |         private class CachedMetadata
103 |         {
104 |             public readonly List<PropertyInfo> SerializableProperties;
105 |             public readonly List<FieldInfo> SerializableFields;
106 | 
107 |             public CachedMetadata(List<PropertyInfo> properties, List<FieldInfo> fields)
108 |             {
109 |                 SerializableProperties = properties;
110 |                 SerializableFields = fields;
111 |             }
112 |         }
113 |         // Key becomes Tuple<Type, bool>
114 |         private static readonly Dictionary<Tuple<Type, bool>, CachedMetadata> _metadataCache = new Dictionary<Tuple<Type, bool>, CachedMetadata>();
115 |         // --- End Metadata Caching ---
116 | 
117 |         /// <summary>
118 |         /// Creates a serializable representation of a Component, attempting to serialize
119 |         /// public properties and fields using reflection, with caching and control over non-public fields.
120 |         /// </summary>
121 |         // Add the flag parameter here
122 |         public static object GetComponentData(Component c, bool includeNonPublicSerializedFields = true)
123 |         {
124 |             // --- Add Early Logging --- 
125 |             // Debug.Log($"[GetComponentData] Starting for component: {c?.GetType()?.FullName ?? "null"} (ID: {c?.GetInstanceID() ?? 0})");
126 |             // --- End Early Logging ---
127 | 
128 |             if (c == null) return null;
129 |             Type componentType = c.GetType();
130 | 
131 |             // --- Special handling for Transform to avoid reflection crashes and problematic properties --- 
132 |             if (componentType == typeof(Transform))
133 |             {
134 |                 Transform tr = c as Transform;
135 |                 // Debug.Log($"[GetComponentData] Manually serializing Transform (ID: {tr.GetInstanceID()})");
136 |                 return new Dictionary<string, object>
137 |                 {
138 |                     { "typeName", componentType.FullName },
139 |                     { "instanceID", tr.GetInstanceID() },
140 |                     // Manually extract known-safe properties. Avoid Quaternion 'rotation' and 'lossyScale'.
141 |                     { "position", CreateTokenFromValue(tr.position, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
142 |                     { "localPosition", CreateTokenFromValue(tr.localPosition, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
143 |                     { "eulerAngles", CreateTokenFromValue(tr.eulerAngles, typeof(Vector3))?.ToObject<object>() ?? new JObject() }, // Use Euler angles
144 |                     { "localEulerAngles", CreateTokenFromValue(tr.localEulerAngles, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
145 |                     { "localScale", CreateTokenFromValue(tr.localScale, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
146 |                     { "right", CreateTokenFromValue(tr.right, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
147 |                     { "up", CreateTokenFromValue(tr.up, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
148 |                     { "forward", CreateTokenFromValue(tr.forward, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
149 |                     { "parentInstanceID", tr.parent?.gameObject.GetInstanceID() ?? 0 },
150 |                     { "rootInstanceID", tr.root?.gameObject.GetInstanceID() ?? 0 },
151 |                     { "childCount", tr.childCount },
152 |                     // Include standard Object/Component properties
153 |                     { "name", tr.name },
154 |                     { "tag", tr.tag },
155 |                     { "gameObjectInstanceID", tr.gameObject?.GetInstanceID() ?? 0 }
156 |                 };
157 |             }
158 |             // --- End Special handling for Transform --- 
159 | 
160 |             // --- Special handling for Camera to avoid matrix-related crashes ---
161 |             if (componentType == typeof(Camera))
162 |             {
163 |                 Camera cam = c as Camera;
164 |                 var cameraProperties = new Dictionary<string, object>();
165 | 
166 |                 // List of safe properties to serialize
167 |                 var safeProperties = new Dictionary<string, Func<object>>
168 |                 {
169 |                     { "nearClipPlane", () => cam.nearClipPlane },
170 |                     { "farClipPlane", () => cam.farClipPlane },
171 |                     { "fieldOfView", () => cam.fieldOfView },
172 |                     { "renderingPath", () => (int)cam.renderingPath },
173 |                     { "actualRenderingPath", () => (int)cam.actualRenderingPath },
174 |                     { "allowHDR", () => cam.allowHDR },
175 |                     { "allowMSAA", () => cam.allowMSAA },
176 |                     { "allowDynamicResolution", () => cam.allowDynamicResolution },
177 |                     { "forceIntoRenderTexture", () => cam.forceIntoRenderTexture },
178 |                     { "orthographicSize", () => cam.orthographicSize },
179 |                     { "orthographic", () => cam.orthographic },
180 |                     { "opaqueSortMode", () => (int)cam.opaqueSortMode },
181 |                     { "transparencySortMode", () => (int)cam.transparencySortMode },
182 |                     { "depth", () => cam.depth },
183 |                     { "aspect", () => cam.aspect },
184 |                     { "cullingMask", () => cam.cullingMask },
185 |                     { "eventMask", () => cam.eventMask },
186 |                     { "backgroundColor", () => cam.backgroundColor },
187 |                     { "clearFlags", () => (int)cam.clearFlags },
188 |                     { "stereoEnabled", () => cam.stereoEnabled },
189 |                     { "stereoSeparation", () => cam.stereoSeparation },
190 |                     { "stereoConvergence", () => cam.stereoConvergence },
191 |                     { "enabled", () => cam.enabled },
192 |                     { "name", () => cam.name },
193 |                     { "tag", () => cam.tag },
194 |                     { "gameObject", () => new { name = cam.gameObject.name, instanceID = cam.gameObject.GetInstanceID() } }
195 |                 };
196 | 
197 |                 foreach (var prop in safeProperties)
198 |                 {
199 |                     try
200 |                     {
201 |                         var value = prop.Value();
202 |                         if (value != null)
203 |                         {
204 |                             AddSerializableValue(cameraProperties, prop.Key, value.GetType(), value);
205 |                         }
206 |                     }
207 |                     catch (Exception)
208 |                     {
209 |                         // Silently skip any property that fails
210 |                         continue;
211 |                     }
212 |                 }
213 | 
214 |                 return new Dictionary<string, object>
215 |                 {
216 |                     { "typeName", componentType.FullName },
217 |                     { "instanceID", cam.GetInstanceID() },
218 |                     { "properties", cameraProperties }
219 |                 };
220 |             }
221 |             // --- End Special handling for Camera ---
222 | 
223 |             var data = new Dictionary<string, object>
224 |             {
225 |                 { "typeName", componentType.FullName },
226 |                 { "instanceID", c.GetInstanceID() }
227 |             };
228 | 
229 |             // --- Get Cached or Generate Metadata (using new cache key) ---
230 |             Tuple<Type, bool> cacheKey = new Tuple<Type, bool>(componentType, includeNonPublicSerializedFields);
231 |             if (!_metadataCache.TryGetValue(cacheKey, out CachedMetadata cachedData))
232 |             {
233 |                 var propertiesToCache = new List<PropertyInfo>();
234 |                 var fieldsToCache = new List<FieldInfo>();
235 | 
236 |                 // Traverse the hierarchy from the component type up to MonoBehaviour
237 |                 Type currentType = componentType;
238 |                 while (currentType != null && currentType != typeof(MonoBehaviour) && currentType != typeof(object))
239 |                 {
240 |                     // Get properties declared only at the current type level
241 |                     BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
242 |                     foreach (var propInfo in currentType.GetProperties(propFlags))
243 |                     {
244 |                         // Basic filtering (readable, not indexer, not transform which is handled elsewhere)
245 |                         if (!propInfo.CanRead || propInfo.GetIndexParameters().Length > 0 || propInfo.Name == "transform") continue;
246 |                         // Add if not already added (handles overrides - keep the most derived version)
247 |                         if (!propertiesToCache.Any(p => p.Name == propInfo.Name))
248 |                         {
249 |                             propertiesToCache.Add(propInfo);
250 |                         }
251 |                     }
252 | 
253 |                     // Get fields declared only at the current type level (both public and non-public)
254 |                     BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly;
255 |                     var declaredFields = currentType.GetFields(fieldFlags);
256 | 
257 |                     // Process the declared Fields for caching
258 |                 foreach (var fieldInfo in declaredFields)
259 |                     {
260 |                         if (fieldInfo.Name.EndsWith("k__BackingField")) continue; // Skip backing fields
261 | 
262 |                         // Add if not already added (handles hiding - keep the most derived version)
263 |                         if (fieldsToCache.Any(f => f.Name == fieldInfo.Name)) continue;
264 | 
265 |                     bool shouldInclude = false;
266 |                     if (includeNonPublicSerializedFields)
267 |                     {
268 |                         // If TRUE, include Public OR any NonPublic with [SerializeField] (private/protected/internal)
269 |                         var hasSerializeField = fieldInfo.IsDefined(typeof(SerializeField), inherit: true);
270 |                         shouldInclude = fieldInfo.IsPublic || (!fieldInfo.IsPublic && hasSerializeField);
271 |                     }
272 |                     else // includeNonPublicSerializedFields is FALSE
273 |                     {
274 |                         // If FALSE, include ONLY if it is explicitly Public.
275 |                         shouldInclude = fieldInfo.IsPublic;
276 |                     }
277 | 
278 |                         if (shouldInclude)
279 |                         {
280 |                             fieldsToCache.Add(fieldInfo);
281 |                         }
282 |                     }
283 | 
284 |                     // Move to the base type
285 |                     currentType = currentType.BaseType;
286 |                 }
287 |                 // --- End Hierarchy Traversal ---
288 | 
289 |                 cachedData = new CachedMetadata(propertiesToCache, fieldsToCache);
290 |                 _metadataCache[cacheKey] = cachedData; // Add to cache with combined key
291 |             }
292 |             // --- End Get Cached or Generate Metadata ---
293 | 
294 |             // --- Use cached metadata ---
295 |             var serializablePropertiesOutput = new Dictionary<string, object>();
296 | 
297 |             // --- Add Logging Before Property Loop ---
298 |             // Debug.Log($"[GetComponentData] Starting property loop for {componentType.Name}...");
299 |             // --- End Logging Before Property Loop ---
300 | 
301 |             // Use cached properties
302 |             foreach (var propInfo in cachedData.SerializableProperties)
303 |             {
304 |                 string propName = propInfo.Name;
305 | 
306 |                 // --- Skip known obsolete/problematic Component shortcut properties ---
307 |                 bool skipProperty = false;
308 |                 if (propName == "rigidbody" || propName == "rigidbody2D" || propName == "camera" ||
309 |                     propName == "light" || propName == "animation" || propName == "constantForce" ||
310 |                     propName == "renderer" || propName == "audio" || propName == "networkView" ||
311 |                     propName == "collider" || propName == "collider2D" || propName == "hingeJoint" ||
312 |                     propName == "particleSystem" ||
313 |                     // Also skip potentially problematic Matrix properties prone to cycles/errors
314 |                     propName == "worldToLocalMatrix" || propName == "localToWorldMatrix")
315 |                 {
316 |                     // Debug.Log($"[GetComponentData] Explicitly skipping generic property: {propName}"); // Optional log
317 |                     skipProperty = true;
318 |                 }
319 |                 // --- End Skip Generic Properties ---
320 | 
321 |                 // --- Skip specific potentially problematic Camera properties ---
322 |                 if (componentType == typeof(Camera) &&
323 |                     (propName == "pixelRect" ||
324 |                      propName == "rect" ||
325 |                      propName == "cullingMatrix" ||
326 |                      propName == "useOcclusionCulling" ||
327 |                      propName == "worldToCameraMatrix" ||
328 |                      propName == "projectionMatrix" ||
329 |                      propName == "nonJitteredProjectionMatrix" ||
330 |                      propName == "previousViewProjectionMatrix" ||
331 |                      propName == "cameraToWorldMatrix"))
332 |                 {
333 |                     // Debug.Log($"[GetComponentData] Explicitly skipping Camera property: {propName}");
334 |                     skipProperty = true;
335 |                 }
336 |                 // --- End Skip Camera Properties ---
337 | 
338 |                 // --- Skip specific potentially problematic Transform properties ---
339 |                 if (componentType == typeof(Transform) &&
340 |                     (propName == "lossyScale" ||
341 |                      propName == "rotation" ||
342 |                      propName == "worldToLocalMatrix" ||
343 |                      propName == "localToWorldMatrix"))
344 |                 {
345 |                     // Debug.Log($"[GetComponentData] Explicitly skipping Transform property: {propName}");
346 |                     skipProperty = true;
347 |                 }
348 |                 // --- End Skip Transform Properties ---
349 | 
350 |                 // Skip if flagged
351 |                 if (skipProperty)
352 |                 {
353 |                     continue;
354 |                 }
355 | 
356 |                 try
357 |                 {
358 |                     // --- Add detailed logging --- 
359 |                     // Debug.Log($"[GetComponentData] Accessing: {componentType.Name}.{propName}");
360 |                     // --- End detailed logging ---
361 |                     
362 |                     // --- Special handling for material/mesh properties in edit mode ---
363 |                     object value;
364 |                     if (!Application.isPlaying && (propName == "material" || propName == "materials" || propName == "mesh"))
365 |                     {
366 |                         // In edit mode, use sharedMaterial/sharedMesh to avoid instantiation warnings
367 |                         if ((propName == "material" || propName == "materials") && c is Renderer renderer)
368 |                         {
369 |                             if (propName == "material")
370 |                                 value = renderer.sharedMaterial;
371 |                             else // materials
372 |                                 value = renderer.sharedMaterials;
373 |                         }
374 |                         else if (propName == "mesh" && c is MeshFilter meshFilter)
375 |                         {
376 |                             value = meshFilter.sharedMesh;
377 |                         }
378 |                         else
379 |                         {
380 |                             // Fallback to normal property access if type doesn't match
381 |                             value = propInfo.GetValue(c);
382 |                         }
383 |                     }
384 |                     else
385 |                     {
386 |                         value = propInfo.GetValue(c);
387 |                     }
388 |                     // --- End special handling ---
389 |                     
390 |                     Type propType = propInfo.PropertyType;
391 |                     AddSerializableValue(serializablePropertiesOutput, propName, propType, value);
392 |                 }
393 |                 catch (Exception)
394 |                 {
395 |                     // Debug.LogWarning($"Could not read property {propName} on {componentType.Name}");
396 |                 }
397 |             }
398 | 
399 |             // --- Add Logging Before Field Loop ---
400 |             // Debug.Log($"[GetComponentData] Starting field loop for {componentType.Name}...");
401 |             // --- End Logging Before Field Loop ---
402 | 
403 |             // Use cached fields
404 |             foreach (var fieldInfo in cachedData.SerializableFields)
405 |             {
406 |                 try
407 |                 {
408 |                     // --- Add detailed logging for fields --- 
409 |                     // Debug.Log($"[GetComponentData] Accessing Field: {componentType.Name}.{fieldInfo.Name}");
410 |                     // --- End detailed logging for fields ---
411 |                     object value = fieldInfo.GetValue(c);
412 |                     string fieldName = fieldInfo.Name;
413 |                     Type fieldType = fieldInfo.FieldType;
414 |                     AddSerializableValue(serializablePropertiesOutput, fieldName, fieldType, value);
415 |                 }
416 |                 catch (Exception)
417 |                 {
418 |                     // Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}");
419 |                 }
420 |             }
421 |             // --- End Use cached metadata ---
422 | 
423 |             if (serializablePropertiesOutput.Count > 0)
424 |             {
425 |                 data["properties"] = serializablePropertiesOutput;
426 |             }
427 | 
428 |             return data;
429 |         }
430 | 
431 |         // Helper function to decide how to serialize different types
432 |         private static void AddSerializableValue(Dictionary<string, object> dict, string name, Type type, object value)
433 |         {
434 |             // Simplified: Directly use CreateTokenFromValue which uses the serializer
435 |             if (value == null)
436 |             {
437 |                 dict[name] = null;
438 |                 return;
439 |             }
440 | 
441 |             try
442 |             {
443 |                 // Use the helper that employs our custom serializer settings
444 |                 JToken token = CreateTokenFromValue(value, type);
445 |                 if (token != null) // Check if serialization succeeded in the helper
446 |                 {
447 |                     // Convert JToken back to a basic object structure for the dictionary
448 |                     dict[name] = ConvertJTokenToPlainObject(token);
449 |                 }
450 |                 // If token is null, it means serialization failed and a warning was logged.
451 |             }
452 |             catch (Exception e)
453 |             {
454 |                 // Catch potential errors during JToken conversion or addition to dictionary
455 |                 Debug.LogWarning($"[AddSerializableValue] Error processing value for '{name}' (Type: {type.FullName}): {e.Message}. Skipping.");
456 |             }
457 |         }
458 | 
459 |         // Helper to convert JToken back to basic object structure
460 |         private static object ConvertJTokenToPlainObject(JToken token)
461 |         {
462 |             if (token == null) return null;
463 | 
464 |             switch (token.Type)
465 |             {
466 |                 case JTokenType.Object:
467 |                     var objDict = new Dictionary<string, object>();
468 |                     foreach (var prop in ((JObject)token).Properties())
469 |                     {
470 |                         objDict[prop.Name] = ConvertJTokenToPlainObject(prop.Value);
471 |                     }
472 |                     return objDict;
473 | 
474 |                 case JTokenType.Array:
475 |                     var list = new List<object>();
476 |                     foreach (var item in (JArray)token)
477 |                     {
478 |                         list.Add(ConvertJTokenToPlainObject(item));
479 |                     }
480 |                     return list;
481 | 
482 |                 case JTokenType.Integer:
483 |                     return token.ToObject<long>(); // Use long for safety
484 |                 case JTokenType.Float:
485 |                     return token.ToObject<double>(); // Use double for safety
486 |                 case JTokenType.String:
487 |                     return token.ToObject<string>();
488 |                 case JTokenType.Boolean:
489 |                     return token.ToObject<bool>();
490 |                 case JTokenType.Date:
491 |                     return token.ToObject<DateTime>();
492 |                 case JTokenType.Guid:
493 |                     return token.ToObject<Guid>();
494 |                 case JTokenType.Uri:
495 |                     return token.ToObject<Uri>();
496 |                 case JTokenType.TimeSpan:
497 |                     return token.ToObject<TimeSpan>();
498 |                 case JTokenType.Bytes:
499 |                     return token.ToObject<byte[]>();
500 |                 case JTokenType.Null:
501 |                     return null;
502 |                 case JTokenType.Undefined:
503 |                     return null; // Treat undefined as null
504 | 
505 |                 default:
506 |                     // Fallback for simple value types not explicitly listed
507 |                     if (token is JValue jValue && jValue.Value != null)
508 |                     {
509 |                         return jValue.Value;
510 |                     }
511 |                     // Debug.LogWarning($"Unsupported JTokenType encountered: {token.Type}. Returning null.");
512 |                     return null;
513 |             }
514 |         }
515 | 
516 |         // --- Define custom JsonSerializerSettings for OUTPUT ---
517 |         private static readonly JsonSerializerSettings _outputSerializerSettings = new JsonSerializerSettings
518 |         {
519 |             Converters = new List<JsonConverter>
520 |             {
521 |                 new Vector3Converter(),
522 |                 new Vector2Converter(),
523 |                 new QuaternionConverter(),
524 |                 new ColorConverter(),
525 |                 new RectConverter(),
526 |                 new BoundsConverter(),
527 |                 new UnityEngineObjectConverter() // Handles serialization of references
528 |             },
529 |             ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
530 |             // ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() } // Example if needed
531 |         };
532 |         private static readonly JsonSerializer _outputSerializer = JsonSerializer.Create(_outputSerializerSettings);
533 |         // --- End Define custom JsonSerializerSettings ---
534 | 
535 |         // Helper to create JToken using the output serializer
536 |         private static JToken CreateTokenFromValue(object value, Type type)
537 |         {
538 |             if (value == null) return JValue.CreateNull();
539 | 
540 |             try
541 |             {
542 |                 // Use the pre-configured OUTPUT serializer instance
543 |                 return JToken.FromObject(value, _outputSerializer);
544 |             }
545 |             catch (JsonSerializationException e)
546 |             {
547 |                 Debug.LogWarning($"[GameObjectSerializer] Newtonsoft.Json Error serializing value of type {type.FullName}: {e.Message}. Skipping property/field.");
548 |                 return null; // Indicate serialization failure
549 |             }
550 |             catch (Exception e) // Catch other unexpected errors
551 |             {
552 |                 Debug.LogWarning($"[GameObjectSerializer] Unexpected error serializing value of type {type.FullName}: {e}. Skipping property/field.");
553 |                 return null; // Indicate serialization failure
554 |             }
555 |         }
556 |     }
557 | }
558 | 
```

--------------------------------------------------------------------------------
/UnityMcpBridge/Editor/Setup/SetupWizardWindow.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Linq;
  3 | using MCPForUnity.Editor.Data;
  4 | using MCPForUnity.Editor.Dependencies;
  5 | using MCPForUnity.Editor.Dependencies.Models;
  6 | using MCPForUnity.Editor.Helpers;
  7 | using MCPForUnity.Editor.Models;
  8 | using UnityEditor;
  9 | using UnityEngine;
 10 | 
 11 | namespace MCPForUnity.Editor.Setup
 12 | {
 13 |     /// <summary>
 14 |     /// Setup wizard window for guiding users through dependency installation
 15 |     /// </summary>
 16 |     public class SetupWizardWindow : EditorWindow
 17 |     {
 18 |         private DependencyCheckResult _dependencyResult;
 19 |         private Vector2 _scrollPosition;
 20 |         private int _currentStep = 0;
 21 |         private McpClients _mcpClients;
 22 |         private int _selectedClientIndex = 0;
 23 | 
 24 |         private readonly string[] _stepTitles = {
 25 |             "Setup",
 26 |             "Configure",
 27 |             "Complete"
 28 |         };
 29 | 
 30 |         public static void ShowWindow(DependencyCheckResult dependencyResult = null)
 31 |         {
 32 |             var window = GetWindow<SetupWizardWindow>("MCP for Unity Setup");
 33 |             window.minSize = new Vector2(500, 400);
 34 |             window.maxSize = new Vector2(800, 600);
 35 |             window._dependencyResult = dependencyResult ?? DependencyManager.CheckAllDependencies();
 36 |             window.Show();
 37 |         }
 38 | 
 39 |         private void OnEnable()
 40 |         {
 41 |             if (_dependencyResult == null)
 42 |             {
 43 |                 _dependencyResult = DependencyManager.CheckAllDependencies();
 44 |             }
 45 | 
 46 |             _mcpClients = new McpClients();
 47 | 
 48 |             // Check client configurations on startup
 49 |             foreach (var client in _mcpClients.clients)
 50 |             {
 51 |                 CheckClientConfiguration(client);
 52 |             }
 53 |         }
 54 | 
 55 |         private void OnGUI()
 56 |         {
 57 |             DrawHeader();
 58 |             DrawProgressBar();
 59 | 
 60 |             _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
 61 | 
 62 |             switch (_currentStep)
 63 |             {
 64 |                 case 0: DrawSetupStep(); break;
 65 |                 case 1: DrawConfigureStep(); break;
 66 |                 case 2: DrawCompleteStep(); break;
 67 |             }
 68 | 
 69 |             EditorGUILayout.EndScrollView();
 70 | 
 71 |             DrawFooter();
 72 |         }
 73 | 
 74 |         private void DrawHeader()
 75 |         {
 76 |             EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
 77 |             GUILayout.Label("MCP for Unity Setup Wizard", EditorStyles.boldLabel);
 78 |             GUILayout.FlexibleSpace();
 79 |             GUILayout.Label($"Step {_currentStep + 1} of {_stepTitles.Length}");
 80 |             EditorGUILayout.EndHorizontal();
 81 | 
 82 |             EditorGUILayout.Space();
 83 | 
 84 |             // Step title
 85 |             var titleStyle = new GUIStyle(EditorStyles.largeLabel)
 86 |             {
 87 |                 fontSize = 16,
 88 |                 fontStyle = FontStyle.Bold
 89 |             };
 90 |             EditorGUILayout.LabelField(_stepTitles[_currentStep], titleStyle);
 91 |             EditorGUILayout.Space();
 92 |         }
 93 | 
 94 |         private void DrawProgressBar()
 95 |         {
 96 |             var rect = EditorGUILayout.GetControlRect(false, 4);
 97 |             var progress = (_currentStep + 1) / (float)_stepTitles.Length;
 98 |             EditorGUI.ProgressBar(rect, progress, "");
 99 |             EditorGUILayout.Space();
100 |         }
101 | 
102 |         private void DrawSetupStep()
103 |         {
104 |             // Welcome section
105 |             DrawSectionTitle("MCP for Unity Setup");
106 | 
107 |             EditorGUILayout.LabelField(
108 |                 "This wizard will help you set up MCP for Unity to connect AI assistants with your Unity Editor.",
109 |                 EditorStyles.wordWrappedLabel
110 |             );
111 |             EditorGUILayout.Space();
112 | 
113 |             // Dependency check section
114 |             EditorGUILayout.BeginHorizontal();
115 |             DrawSectionTitle("System Check", 14);
116 |             GUILayout.FlexibleSpace();
117 |             if (GUILayout.Button("Refresh", GUILayout.Width(60), GUILayout.Height(20)))
118 |             {
119 |                 _dependencyResult = DependencyManager.CheckAllDependencies();
120 |             }
121 |             EditorGUILayout.EndHorizontal();
122 | 
123 |             // Show simplified dependency status
124 |             foreach (var dep in _dependencyResult.Dependencies)
125 |             {
126 |                 DrawSimpleDependencyStatus(dep);
127 |             }
128 | 
129 |             // Overall status and installation guidance
130 |             EditorGUILayout.Space();
131 |             if (!_dependencyResult.IsSystemReady)
132 |             {
133 |                 // Only show critical warnings when dependencies are actually missing
134 |                 EditorGUILayout.HelpBox(
135 |                     "⚠️ Missing Dependencies: MCP for Unity requires Python 3.10+ and UV package manager to function properly.",
136 |                     MessageType.Warning
137 |                 );
138 | 
139 |                 EditorGUILayout.Space();
140 |                 EditorGUILayout.BeginVertical(EditorStyles.helpBox);
141 |                 DrawErrorStatus("Installation Required");
142 | 
143 |                 var recommendations = DependencyManager.GetInstallationRecommendations();
144 |                 EditorGUILayout.LabelField(recommendations, EditorStyles.wordWrappedLabel);
145 | 
146 |                 EditorGUILayout.Space();
147 |                 if (GUILayout.Button("Open Installation Links", GUILayout.Height(25)))
148 |                 {
149 |                     OpenInstallationUrls();
150 |                 }
151 |                 EditorGUILayout.EndVertical();
152 |             }
153 |             else
154 |             {
155 |                 DrawSuccessStatus("System Ready");
156 |                 EditorGUILayout.LabelField("All requirements are met. You can proceed to configure your AI clients.", EditorStyles.wordWrappedLabel);
157 |             }
158 |         }
159 | 
160 | 
161 | 
162 |         private void DrawCompleteStep()
163 |         {
164 |             DrawSectionTitle("Setup Complete");
165 | 
166 |             // Refresh dependency check with caching to avoid heavy operations on every repaint
167 |             if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2)
168 |             {
169 |                 _dependencyResult = DependencyManager.CheckAllDependencies();
170 |             }
171 | 
172 |             if (_dependencyResult.IsSystemReady)
173 |             {
174 |                 DrawSuccessStatus("MCP for Unity Ready!");
175 | 
176 |                 EditorGUILayout.HelpBox(
177 |                     "🎉 MCP for Unity is now set up and ready to use!\n\n" +
178 |                     "• Dependencies verified\n" +
179 |                     "• MCP server ready\n" +
180 |                     "• Client configuration accessible",
181 |                     MessageType.Info
182 |                 );
183 | 
184 |                 EditorGUILayout.Space();
185 |                 EditorGUILayout.BeginHorizontal();
186 |                 if (GUILayout.Button("Documentation", GUILayout.Height(30)))
187 |                 {
188 |                     Application.OpenURL("https://github.com/CoplayDev/unity-mcp");
189 |                 }
190 |                 if (GUILayout.Button("Client Settings", GUILayout.Height(30)))
191 |                 {
192 |                     Windows.MCPForUnityEditorWindow.ShowWindow();
193 |                 }
194 |                 EditorGUILayout.EndHorizontal();
195 |             }
196 |             else
197 |             {
198 |                 DrawErrorStatus("Setup Incomplete - Package Non-Functional");
199 | 
200 |                 EditorGUILayout.HelpBox(
201 |                     "🚨 MCP for Unity CANNOT work - dependencies still missing!\n\n" +
202 |                     "Install ALL required dependencies before the package will function.",
203 |                     MessageType.Error
204 |                 );
205 | 
206 |                 var missingDeps = _dependencyResult.GetMissingRequired();
207 |                 if (missingDeps.Count > 0)
208 |                 {
209 |                     EditorGUILayout.Space();
210 |                     EditorGUILayout.LabelField("Still Missing:", EditorStyles.boldLabel);
211 |                     foreach (var dep in missingDeps)
212 |                     {
213 |                         EditorGUILayout.LabelField($"✗ {dep.Name}", EditorStyles.label);
214 |                     }
215 |                 }
216 | 
217 |                 EditorGUILayout.Space();
218 |                 if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30)))
219 |                 {
220 |                     _currentStep = 0;
221 |                 }
222 |             }
223 |         }
224 | 
225 |         // Helper methods for consistent UI components
226 |         private void DrawSectionTitle(string title, int fontSize = 16)
227 |         {
228 |             var titleStyle = new GUIStyle(EditorStyles.boldLabel)
229 |             {
230 |                 fontSize = fontSize,
231 |                 fontStyle = FontStyle.Bold
232 |             };
233 |             EditorGUILayout.LabelField(title, titleStyle);
234 |             EditorGUILayout.Space();
235 |         }
236 | 
237 |         private void DrawSuccessStatus(string message)
238 |         {
239 |             var originalColor = GUI.color;
240 |             GUI.color = Color.green;
241 |             EditorGUILayout.LabelField($"✓ {message}", EditorStyles.boldLabel);
242 |             GUI.color = originalColor;
243 |             EditorGUILayout.Space();
244 |         }
245 | 
246 |         private void DrawErrorStatus(string message)
247 |         {
248 |             var originalColor = GUI.color;
249 |             GUI.color = Color.red;
250 |             EditorGUILayout.LabelField($"✗ {message}", EditorStyles.boldLabel);
251 |             GUI.color = originalColor;
252 |             EditorGUILayout.Space();
253 |         }
254 | 
255 |         private void DrawSimpleDependencyStatus(DependencyStatus dep)
256 |         {
257 |             EditorGUILayout.BeginHorizontal();
258 | 
259 |             var statusIcon = dep.IsAvailable ? "✓" : "✗";
260 |             var statusColor = dep.IsAvailable ? Color.green : Color.red;
261 | 
262 |             var originalColor = GUI.color;
263 |             GUI.color = statusColor;
264 |             GUILayout.Label(statusIcon, GUILayout.Width(20));
265 |             EditorGUILayout.LabelField(dep.Name, EditorStyles.boldLabel);
266 |             GUI.color = originalColor;
267 | 
268 |             if (!dep.IsAvailable && !string.IsNullOrEmpty(dep.ErrorMessage))
269 |             {
270 |                 EditorGUILayout.LabelField($"({dep.ErrorMessage})", EditorStyles.miniLabel);
271 |             }
272 | 
273 |             EditorGUILayout.EndHorizontal();
274 |         }
275 | 
276 |         private void DrawConfigureStep()
277 |         {
278 |             DrawSectionTitle("AI Client Configuration");
279 | 
280 |             // Check dependencies first (with caching to avoid heavy operations on every repaint)
281 |             if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2)
282 |             {
283 |                 _dependencyResult = DependencyManager.CheckAllDependencies();
284 |             }
285 |             if (!_dependencyResult.IsSystemReady)
286 |             {
287 |                 DrawErrorStatus("Cannot Configure - System Requirements Not Met");
288 | 
289 |                 EditorGUILayout.HelpBox(
290 |                     "Client configuration requires system dependencies to be installed first. Please complete setup before proceeding.",
291 |                     MessageType.Warning
292 |                 );
293 | 
294 |                 if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30)))
295 |                 {
296 |                     _currentStep = 0;
297 |                 }
298 |                 return;
299 |             }
300 | 
301 |             EditorGUILayout.LabelField(
302 |                 "Configure your AI assistants to work with Unity. Select a client below to set it up:",
303 |                 EditorStyles.wordWrappedLabel
304 |             );
305 |             EditorGUILayout.Space();
306 | 
307 |             // Client selection and configuration
308 |             if (_mcpClients.clients.Count > 0)
309 |             {
310 |                 // Client selector dropdown
311 |                 string[] clientNames = _mcpClients.clients.Select(c => c.name).ToArray();
312 |                 EditorGUI.BeginChangeCheck();
313 |                 _selectedClientIndex = EditorGUILayout.Popup("Select AI Client:", _selectedClientIndex, clientNames);
314 |                 if (EditorGUI.EndChangeCheck())
315 |                 {
316 |                     _selectedClientIndex = Mathf.Clamp(_selectedClientIndex, 0, _mcpClients.clients.Count - 1);
317 |                     // Refresh client status when selection changes
318 |                     CheckClientConfiguration(_mcpClients.clients[_selectedClientIndex]);
319 |                 }
320 | 
321 |                 EditorGUILayout.Space();
322 | 
323 |                 var selectedClient = _mcpClients.clients[_selectedClientIndex];
324 |                 DrawClientConfigurationInWizard(selectedClient);
325 | 
326 |                 EditorGUILayout.Space();
327 | 
328 |                 // Batch configuration option
329 |                 EditorGUILayout.BeginVertical(EditorStyles.helpBox);
330 |                 EditorGUILayout.LabelField("Quick Setup", EditorStyles.boldLabel);
331 |                 EditorGUILayout.LabelField(
332 |                     "Automatically configure all detected AI clients at once:",
333 |                     EditorStyles.wordWrappedLabel
334 |                 );
335 |                 EditorGUILayout.Space();
336 | 
337 |                 if (GUILayout.Button("Configure All Detected Clients", GUILayout.Height(30)))
338 |                 {
339 |                     ConfigureAllClientsInWizard();
340 |                 }
341 |                 EditorGUILayout.EndVertical();
342 |             }
343 |             else
344 |             {
345 |                 EditorGUILayout.HelpBox("No AI clients detected. Make sure you have Claude Code, Cursor, or VSCode installed.", MessageType.Info);
346 |             }
347 | 
348 |             EditorGUILayout.Space();
349 |             EditorGUILayout.HelpBox(
350 |                 "💡 You might need to restart your AI client after configuring.",
351 |                 MessageType.Info
352 |             );
353 |         }
354 | 
355 |         private void DrawFooter()
356 |         {
357 |             EditorGUILayout.Space();
358 |             EditorGUILayout.BeginHorizontal();
359 | 
360 |             // Back button
361 |             GUI.enabled = _currentStep > 0;
362 |             if (GUILayout.Button("Back", GUILayout.Width(60)))
363 |             {
364 |                 _currentStep--;
365 |             }
366 | 
367 |             GUILayout.FlexibleSpace();
368 | 
369 |             // Skip button
370 |             if (GUILayout.Button("Skip", GUILayout.Width(60)))
371 |             {
372 |                 bool dismiss = EditorUtility.DisplayDialog(
373 |                     "Skip Setup",
374 |                     "⚠️ Skipping setup will leave MCP for Unity non-functional!\n\n" +
375 |                     "You can restart setup from: Window > MCP for Unity > Setup Wizard (Required)",
376 |                     "Skip Anyway",
377 |                     "Cancel"
378 |                 );
379 | 
380 |                 if (dismiss)
381 |                 {
382 |                     SetupWizard.MarkSetupDismissed();
383 |                     Close();
384 |                 }
385 |             }
386 | 
387 |             // Next/Done button
388 |             GUI.enabled = true;
389 |             string buttonText = _currentStep == _stepTitles.Length - 1 ? "Done" : "Next";
390 | 
391 |             if (GUILayout.Button(buttonText, GUILayout.Width(80)))
392 |             {
393 |                 if (_currentStep == _stepTitles.Length - 1)
394 |                 {
395 |                     SetupWizard.MarkSetupCompleted();
396 |                     Close();
397 |                 }
398 |                 else
399 |                 {
400 |                     _currentStep++;
401 |                 }
402 |             }
403 | 
404 |             GUI.enabled = true;
405 |             EditorGUILayout.EndHorizontal();
406 |         }
407 | 
408 |         private void DrawClientConfigurationInWizard(McpClient client)
409 |         {
410 |             EditorGUILayout.BeginVertical(EditorStyles.helpBox);
411 | 
412 |             EditorGUILayout.LabelField($"{client.name} Configuration", EditorStyles.boldLabel);
413 |             EditorGUILayout.Space();
414 | 
415 |             // Show current status
416 |             var statusColor = GetClientStatusColor(client);
417 |             var originalColor = GUI.color;
418 |             GUI.color = statusColor;
419 |             EditorGUILayout.LabelField($"Status: {client.configStatus}", EditorStyles.label);
420 |             GUI.color = originalColor;
421 | 
422 |             EditorGUILayout.Space();
423 | 
424 |             // Configuration buttons
425 |             EditorGUILayout.BeginHorizontal();
426 | 
427 |             if (client.mcpType == McpTypes.ClaudeCode)
428 |             {
429 |                 // Special handling for Claude Code
430 |                 bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude());
431 |                 if (claudeAvailable)
432 |                 {
433 |                     bool isConfigured = client.status == McpStatus.Configured;
434 |                     string buttonText = isConfigured ? "Unregister" : "Register";
435 |                     if (GUILayout.Button($"{buttonText} with Claude Code"))
436 |                     {
437 |                         if (isConfigured)
438 |                         {
439 |                             UnregisterFromClaudeCode(client);
440 |                         }
441 |                         else
442 |                         {
443 |                             RegisterWithClaudeCode(client);
444 |                         }
445 |                     }
446 |                 }
447 |                 else
448 |                 {
449 |                     EditorGUILayout.HelpBox("Claude Code not found. Please install Claude Code first.", MessageType.Warning);
450 |                     if (GUILayout.Button("Open Claude Code Website"))
451 |                     {
452 |                         Application.OpenURL("https://claude.ai/download");
453 |                     }
454 |                 }
455 |             }
456 |             else
457 |             {
458 |                 // Standard client configuration
459 |                 if (GUILayout.Button($"Configure {client.name}"))
460 |                 {
461 |                     ConfigureClientInWizard(client);
462 |                 }
463 | 
464 |                 if (GUILayout.Button("Manual Setup"))
465 |                 {
466 |                     ShowManualSetupInWizard(client);
467 |                 }
468 |             }
469 | 
470 |             EditorGUILayout.EndHorizontal();
471 |             EditorGUILayout.EndVertical();
472 |         }
473 | 
474 |         private Color GetClientStatusColor(McpClient client)
475 |         {
476 |             return client.status switch
477 |             {
478 |                 McpStatus.Configured => Color.green,
479 |                 McpStatus.Running => Color.green,
480 |                 McpStatus.Connected => Color.green,
481 |                 McpStatus.IncorrectPath => Color.yellow,
482 |                 McpStatus.CommunicationError => Color.yellow,
483 |                 McpStatus.NoResponse => Color.yellow,
484 |                 _ => Color.red
485 |             };
486 |         }
487 | 
488 |         private void ConfigureClientInWizard(McpClient client)
489 |         {
490 |             try
491 |             {
492 |                 string result = PerformClientConfiguration(client);
493 | 
494 |                 EditorUtility.DisplayDialog(
495 |                     $"{client.name} Configuration",
496 |                     result,
497 |                     "OK"
498 |                 );
499 | 
500 |                 // Refresh client status
501 |                 CheckClientConfiguration(client);
502 |                 Repaint();
503 |             }
504 |             catch (System.Exception ex)
505 |             {
506 |                 EditorUtility.DisplayDialog(
507 |                     "Configuration Error",
508 |                     $"Failed to configure {client.name}: {ex.Message}",
509 |                     "OK"
510 |                 );
511 |             }
512 |         }
513 | 
514 |         private void ConfigureAllClientsInWizard()
515 |         {
516 |             int successCount = 0;
517 |             int totalCount = _mcpClients.clients.Count;
518 | 
519 |             foreach (var client in _mcpClients.clients)
520 |             {
521 |                 try
522 |                 {
523 |                     if (client.mcpType == McpTypes.ClaudeCode)
524 |                     {
525 |                         if (!string.IsNullOrEmpty(ExecPath.ResolveClaude()) && client.status != McpStatus.Configured)
526 |                         {
527 |                             RegisterWithClaudeCode(client);
528 |                             successCount++;
529 |                         }
530 |                         else if (client.status == McpStatus.Configured)
531 |                         {
532 |                             successCount++; // Already configured
533 |                         }
534 |                     }
535 |                     else
536 |                     {
537 |                         string result = PerformClientConfiguration(client);
538 |                         if (result.Contains("success", System.StringComparison.OrdinalIgnoreCase))
539 |                         {
540 |                             successCount++;
541 |                         }
542 |                     }
543 | 
544 |                     CheckClientConfiguration(client);
545 |                 }
546 |                 catch (System.Exception ex)
547 |                 {
548 |                     McpLog.Error($"Failed to configure {client.name}: {ex.Message}");
549 |                 }
550 |             }
551 | 
552 |             EditorUtility.DisplayDialog(
553 |                 "Batch Configuration Complete",
554 |                 $"Successfully configured {successCount} out of {totalCount} clients.\n\n" +
555 |                 "Restart your AI clients for changes to take effect.",
556 |                 "OK"
557 |             );
558 | 
559 |             Repaint();
560 |         }
561 | 
562 |         private void RegisterWithClaudeCode(McpClient client)
563 |         {
564 |             try
565 |             {
566 |                 string pythonDir = McpPathResolver.FindPackagePythonDirectory();
567 |                 string claudePath = ExecPath.ResolveClaude();
568 |                 string uvPath = ExecPath.ResolveUv() ?? "uv";
569 | 
570 |                 string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py";
571 | 
572 |                 if (!ExecPath.TryRun(claudePath, args, null, out var stdout, out var stderr, 15000, McpPathResolver.GetPathPrepend()))
573 |                 {
574 |                     if ((stdout + stderr).Contains("already exists", System.StringComparison.OrdinalIgnoreCase))
575 |                     {
576 |                         CheckClientConfiguration(client);
577 |                         EditorUtility.DisplayDialog("Claude Code", "MCP for Unity is already registered with Claude Code.", "OK");
578 |                     }
579 |                     else
580 |                     {
581 |                         throw new System.Exception($"Registration failed: {stderr}");
582 |                     }
583 |                 }
584 |                 else
585 |                 {
586 |                     CheckClientConfiguration(client);
587 |                     EditorUtility.DisplayDialog("Claude Code", "Successfully registered MCP for Unity with Claude Code!", "OK");
588 |                 }
589 |             }
590 |             catch (System.Exception ex)
591 |             {
592 |                 EditorUtility.DisplayDialog("Registration Error", $"Failed to register with Claude Code: {ex.Message}", "OK");
593 |             }
594 |         }
595 | 
596 |         private void UnregisterFromClaudeCode(McpClient client)
597 |         {
598 |             try
599 |             {
600 |                 string claudePath = ExecPath.ResolveClaude();
601 |                 if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", null, out var stdout, out var stderr, 10000, McpPathResolver.GetPathPrepend()))
602 |                 {
603 |                     CheckClientConfiguration(client);
604 |                     EditorUtility.DisplayDialog("Claude Code", "Successfully unregistered MCP for Unity from Claude Code.", "OK");
605 |                 }
606 |                 else
607 |                 {
608 |                     throw new System.Exception($"Unregistration failed: {stderr}");
609 |                 }
610 |             }
611 |             catch (System.Exception ex)
612 |             {
613 |                 EditorUtility.DisplayDialog("Unregistration Error", $"Failed to unregister from Claude Code: {ex.Message}", "OK");
614 |             }
615 |         }
616 | 
617 |         private string PerformClientConfiguration(McpClient client)
618 |         {
619 |             // This mirrors the logic from MCPForUnityEditorWindow.ConfigureMcpClient
620 |             string configPath = McpConfigurationHelper.GetClientConfigPath(client);
621 |             string pythonDir = McpPathResolver.FindPackagePythonDirectory();
622 | 
623 |             if (string.IsNullOrEmpty(pythonDir))
624 |             {
625 |                 return "Manual configuration required - Python server directory not found.";
626 |             }
627 | 
628 |             McpConfigurationHelper.EnsureConfigDirectoryExists(configPath);
629 |             // Use TOML writer for Codex; JSON writer for others
630 |             if (client != null && client.mcpType == McpTypes.Codex)
631 |             {
632 |                 return McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, client);
633 |             }
634 |             else
635 |             {
636 |                 return McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client);
637 |             }
638 |         }
639 | 
640 |         private void ShowManualSetupInWizard(McpClient client)
641 |         {
642 |             string configPath = McpConfigurationHelper.GetClientConfigPath(client);
643 |             string pythonDir = McpPathResolver.FindPackagePythonDirectory();
644 |             string uvPath = ServerInstaller.FindUvPath();
645 | 
646 |             if (string.IsNullOrEmpty(uvPath))
647 |             {
648 |                 EditorUtility.DisplayDialog("Manual Setup", "UV package manager not found. Please install UV first.", "OK");
649 |                 return;
650 |             }
651 | 
652 |             // Build manual configuration using the sophisticated helper logic
653 |             string result = (client != null && client.mcpType == McpTypes.Codex)
654 |                 ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, client)
655 |                 : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client);
656 |             string manualConfig;
657 | 
658 |             if (result == "Configured successfully")
659 |             {
660 |                 // Read back the configuration that was written
661 |                 try
662 |                 {
663 |                     manualConfig = System.IO.File.ReadAllText(configPath);
664 |                 }
665 |                 catch
666 |                 {
667 |                     manualConfig = "Configuration written successfully, but could not read back for display.";
668 |                 }
669 |             }
670 |             else
671 |             {
672 |                 manualConfig = $"Configuration failed: {result}";
673 |             }
674 | 
675 |             EditorUtility.DisplayDialog(
676 |                 $"Manual Setup - {client.name}",
677 |                 $"Configuration file location:\n{configPath}\n\n" +
678 |                 $"Configuration result:\n{manualConfig}",
679 |                 "OK"
680 |             );
681 |         }
682 | 
683 |         private void CheckClientConfiguration(McpClient client)
684 |         {
685 |             // Basic status check - could be enhanced to mirror MCPForUnityEditorWindow logic
686 |             try
687 |             {
688 |                 string configPath = McpConfigurationHelper.GetClientConfigPath(client);
689 |                 if (System.IO.File.Exists(configPath))
690 |                 {
691 |                     client.configStatus = "Configured";
692 |                     client.status = McpStatus.Configured;
693 |                 }
694 |                 else
695 |                 {
696 |                     client.configStatus = "Not Configured";
697 |                     client.status = McpStatus.NotConfigured;
698 |                 }
699 |             }
700 |             catch
701 |             {
702 |                 client.configStatus = "Error";
703 |                 client.status = McpStatus.Error;
704 |             }
705 |         }
706 | 
707 |         private void OpenInstallationUrls()
708 |         {
709 |             var (pythonUrl, uvUrl) = DependencyManager.GetInstallationUrls();
710 | 
711 |             bool openPython = EditorUtility.DisplayDialog(
712 |                 "Open Installation URLs",
713 |                 "Open Python installation page?",
714 |                 "Yes",
715 |                 "No"
716 |             );
717 | 
718 |             if (openPython)
719 |             {
720 |                 Application.OpenURL(pythonUrl);
721 |             }
722 | 
723 |             bool openUV = EditorUtility.DisplayDialog(
724 |                 "Open Installation URLs",
725 |                 "Open UV installation page?",
726 |                 "Yes",
727 |                 "No"
728 |             );
729 | 
730 |             if (openUV)
731 |             {
732 |                 Application.OpenURL(uvUrl);
733 |             }
734 |         }
735 |     }
736 | }
737 | 
```

--------------------------------------------------------------------------------
/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.IO;
  3 | using System.Runtime.InteropServices;
  4 | using System.Text;
  5 | using System.Collections.Generic;
  6 | using System.Linq;
  7 | using UnityEditor;
  8 | using UnityEngine;
  9 | 
 10 | namespace MCPForUnity.Editor.Helpers
 11 | {
 12 |     public static class ServerInstaller
 13 |     {
 14 |         private const string RootFolder = "UnityMCP";
 15 |         private const string ServerFolder = "UnityMcpServer";
 16 |         private const string VersionFileName = "server_version.txt";
 17 | 
 18 |         /// <summary>
 19 |         /// Ensures the mcp-for-unity-server is installed locally by copying from the embedded package source.
 20 |         /// No network calls or Git operations are performed.
 21 |         /// </summary>
 22 |         public static void EnsureServerInstalled()
 23 |         {
 24 |             try
 25 |             {
 26 |                 string saveLocation = GetSaveLocation();
 27 |                 TryCreateMacSymlinkForAppSupport();
 28 |                 string destRoot = Path.Combine(saveLocation, ServerFolder);
 29 |                 string destSrc = Path.Combine(destRoot, "src");
 30 | 
 31 |                 // Detect legacy installs and version state (logs)
 32 |                 DetectAndLogLegacyInstallStates(destRoot);
 33 | 
 34 |                 // Resolve embedded source and versions
 35 |                 if (!TryGetEmbeddedServerSource(out string embeddedSrc))
 36 |                 {
 37 |                     throw new Exception("Could not find embedded UnityMcpServer/src in the package.");
 38 |                 }
 39 |                 string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc, VersionFileName)) ?? "unknown";
 40 |                 string installedVer = ReadVersionFile(Path.Combine(destSrc, VersionFileName));
 41 | 
 42 |                 bool destHasServer = File.Exists(Path.Combine(destSrc, "server.py"));
 43 |                 bool needOverwrite = !destHasServer
 44 |                                      || string.IsNullOrEmpty(installedVer)
 45 |                                      || (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(installedVer, embeddedVer) < 0);
 46 | 
 47 |                 // Ensure destination exists
 48 |                 Directory.CreateDirectory(destRoot);
 49 | 
 50 |                 if (needOverwrite)
 51 |                 {
 52 |                     // Copy the entire UnityMcpServer folder (parent of src)
 53 |                     string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc; // go up from src to UnityMcpServer
 54 |                     CopyDirectoryRecursive(embeddedRoot, destRoot);
 55 |                     // Write/refresh version file
 56 |                     try { File.WriteAllText(Path.Combine(destSrc, VersionFileName), embeddedVer ?? "unknown"); } catch { }
 57 |                     McpLog.Info($"Installed/updated server to {destRoot} (version {embeddedVer}).");
 58 |                 }
 59 | 
 60 |                 // Cleanup legacy installs that are missing version or older than embedded
 61 |                 foreach (var legacyRoot in GetLegacyRootsForDetection())
 62 |                 {
 63 |                     try
 64 |                     {
 65 |                         string legacySrc = Path.Combine(legacyRoot, "src");
 66 |                         if (!File.Exists(Path.Combine(legacySrc, "server.py"))) continue;
 67 |                         string legacyVer = ReadVersionFile(Path.Combine(legacySrc, VersionFileName));
 68 |                         bool legacyOlder = string.IsNullOrEmpty(legacyVer)
 69 |                                            || (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(legacyVer, embeddedVer) < 0);
 70 |                         if (legacyOlder)
 71 |                         {
 72 |                             TryKillUvForPath(legacySrc);
 73 |                             try
 74 |                             {
 75 |                                 Directory.Delete(legacyRoot, recursive: true);
 76 |                                 McpLog.Info($"Removed legacy server at '{legacyRoot}'.");
 77 |                             }
 78 |                             catch (Exception ex)
 79 |                             {
 80 |                                 McpLog.Warn($"Failed to remove legacy server at '{legacyRoot}': {ex.Message}");
 81 |                             }
 82 |                         }
 83 |                     }
 84 |                     catch { }
 85 |                 }
 86 | 
 87 |                 // Clear overrides that might point at legacy locations
 88 |                 try
 89 |                 {
 90 |                     EditorPrefs.DeleteKey("MCPForUnity.ServerSrc");
 91 |                     EditorPrefs.DeleteKey("MCPForUnity.PythonDirOverride");
 92 |                 }
 93 |                 catch { }
 94 |                 return;
 95 |             }
 96 |             catch (Exception ex)
 97 |             {
 98 |                 // If a usable server is already present (installed or embedded), don't fail hard—just warn.
 99 |                 bool hasInstalled = false;
100 |                 try { hasInstalled = File.Exists(Path.Combine(GetServerPath(), "server.py")); } catch { }
101 | 
102 |                 if (hasInstalled || TryGetEmbeddedServerSource(out _))
103 |                 {
104 |                     McpLog.Warn($"Using existing server; skipped install. Details: {ex.Message}");
105 |                     return;
106 |                 }
107 | 
108 |                 McpLog.Error($"Failed to ensure server installation: {ex.Message}");
109 |             }
110 |         }
111 | 
112 |         public static string GetServerPath()
113 |         {
114 |             return Path.Combine(GetSaveLocation(), ServerFolder, "src");
115 |         }
116 | 
117 |         /// <summary>
118 |         /// Gets the platform-specific save location for the server.
119 |         /// </summary>
120 |         private static string GetSaveLocation()
121 |         {
122 |             if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
123 |             {
124 |                 // Use per-user LocalApplicationData for canonical install location
125 |                 var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
126 |                                    ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, "AppData", "Local");
127 |                 return Path.Combine(localAppData, RootFolder);
128 |             }
129 |             else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
130 |             {
131 |                 var xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
132 |                 if (string.IsNullOrEmpty(xdg))
133 |                 {
134 |                     xdg = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty,
135 |                                        ".local", "share");
136 |                 }
137 |                 return Path.Combine(xdg, RootFolder);
138 |             }
139 |             else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
140 |             {
141 |                 // On macOS, use LocalApplicationData (~/Library/Application Support)
142 |                 var localAppSupport = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
143 |                 // Unity/Mono may map LocalApplicationData to ~/.local/share on macOS; normalize to Application Support
144 |                 bool looksLikeXdg = !string.IsNullOrEmpty(localAppSupport) && localAppSupport.Replace('\\', '/').Contains("/.local/share");
145 |                 if (string.IsNullOrEmpty(localAppSupport) || looksLikeXdg)
146 |                 {
147 |                     // Fallback: construct from $HOME
148 |                     var home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
149 |                     localAppSupport = Path.Combine(home, "Library", "Application Support");
150 |                 }
151 |                 TryCreateMacSymlinkForAppSupport();
152 |                 return Path.Combine(localAppSupport, RootFolder);
153 |             }
154 |             throw new Exception("Unsupported operating system.");
155 |         }
156 | 
157 |         /// <summary>
158 |         /// On macOS, create a no-spaces symlink ~/Library/AppSupport -> ~/Library/Application Support
159 |         /// to mitigate arg parsing and quoting issues in some MCP clients.
160 |         /// Safe to call repeatedly.
161 |         /// </summary>
162 |         private static void TryCreateMacSymlinkForAppSupport()
163 |         {
164 |             try
165 |             {
166 |                 if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return;
167 |                 string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
168 |                 if (string.IsNullOrEmpty(home)) return;
169 | 
170 |                 string canonical = Path.Combine(home, "Library", "Application Support");
171 |                 string symlink = Path.Combine(home, "Library", "AppSupport");
172 | 
173 |                 // If symlink exists already, nothing to do
174 |                 if (Directory.Exists(symlink) || File.Exists(symlink)) return;
175 | 
176 |                 // Create symlink only if canonical exists
177 |                 if (!Directory.Exists(canonical)) return;
178 | 
179 |                 // Use 'ln -s' to create a directory symlink (macOS)
180 |                 var psi = new System.Diagnostics.ProcessStartInfo
181 |                 {
182 |                     FileName = "/bin/ln",
183 |                     Arguments = $"-s \"{canonical}\" \"{symlink}\"",
184 |                     UseShellExecute = false,
185 |                     RedirectStandardOutput = true,
186 |                     RedirectStandardError = true,
187 |                     CreateNoWindow = true
188 |                 };
189 |                 using var p = System.Diagnostics.Process.Start(psi);
190 |                 p?.WaitForExit(2000);
191 |             }
192 |             catch { /* best-effort */ }
193 |         }
194 | 
195 |         private static bool IsDirectoryWritable(string path)
196 |         {
197 |             try
198 |             {
199 |                 File.Create(Path.Combine(path, "test.txt")).Dispose();
200 |                 File.Delete(Path.Combine(path, "test.txt"));
201 |                 return true;
202 |             }
203 |             catch
204 |             {
205 |                 return false;
206 |             }
207 |         }
208 | 
209 |         /// <summary>
210 |         /// Checks if the server is installed at the specified location.
211 |         /// </summary>
212 |         private static bool IsServerInstalled(string location)
213 |         {
214 |             return Directory.Exists(location)
215 |                 && File.Exists(Path.Combine(location, ServerFolder, "src", "server.py"));
216 |         }
217 | 
218 |         /// <summary>
219 |         /// Detects legacy installs or older versions and logs findings (no deletion yet).
220 |         /// </summary>
221 |         private static void DetectAndLogLegacyInstallStates(string canonicalRoot)
222 |         {
223 |             try
224 |             {
225 |                 string canonicalSrc = Path.Combine(canonicalRoot, "src");
226 |                 // Normalize canonical root for comparisons
227 |                 string normCanonicalRoot = NormalizePathSafe(canonicalRoot);
228 |                 string embeddedSrc = null;
229 |                 TryGetEmbeddedServerSource(out embeddedSrc);
230 | 
231 |                 string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc ?? string.Empty, VersionFileName));
232 |                 string installedVer = ReadVersionFile(Path.Combine(canonicalSrc, VersionFileName));
233 | 
234 |                 // Legacy paths (macOS/Linux .config; Windows roaming as example)
235 |                 foreach (var legacyRoot in GetLegacyRootsForDetection())
236 |                 {
237 |                     // Skip logging for the canonical root itself
238 |                     if (PathsEqualSafe(legacyRoot, normCanonicalRoot))
239 |                         continue;
240 |                     string legacySrc = Path.Combine(legacyRoot, "src");
241 |                     bool hasServer = File.Exists(Path.Combine(legacySrc, "server.py"));
242 |                     string legacyVer = ReadVersionFile(Path.Combine(legacySrc, VersionFileName));
243 | 
244 |                     if (hasServer)
245 |                     {
246 |                         // Case 1: No version file
247 |                         if (string.IsNullOrEmpty(legacyVer))
248 |                         {
249 |                             McpLog.Info("Detected legacy install without version file at: " + legacyRoot, always: false);
250 |                         }
251 | 
252 |                         // Case 2: Lives in legacy path
253 |                         McpLog.Info("Detected legacy install path: " + legacyRoot, always: false);
254 | 
255 |                         // Case 3: Has version but appears older than embedded
256 |                         if (!string.IsNullOrEmpty(embeddedVer) && !string.IsNullOrEmpty(legacyVer) && CompareSemverSafe(legacyVer, embeddedVer) < 0)
257 |                         {
258 |                             McpLog.Info($"Legacy install version {legacyVer} is older than embedded {embeddedVer}", always: false);
259 |                         }
260 |                     }
261 |                 }
262 | 
263 |                 // Also log if canonical is missing version (treated as older)
264 |                 if (Directory.Exists(canonicalRoot))
265 |                 {
266 |                     if (string.IsNullOrEmpty(installedVer))
267 |                     {
268 |                         McpLog.Info("Canonical install missing version file (treat as older). Path: " + canonicalRoot, always: false);
269 |                     }
270 |                     else if (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(installedVer, embeddedVer) < 0)
271 |                     {
272 |                         McpLog.Info($"Canonical install version {installedVer} is older than embedded {embeddedVer}", always: false);
273 |                     }
274 |                 }
275 |             }
276 |             catch (Exception ex)
277 |             {
278 |                 McpLog.Warn("Detect legacy/version state failed: " + ex.Message);
279 |             }
280 |         }
281 | 
282 |         private static string NormalizePathSafe(string path)
283 |         {
284 |             try { return string.IsNullOrEmpty(path) ? path : Path.GetFullPath(path.Trim()); }
285 |             catch { return path; }
286 |         }
287 | 
288 |         private static bool PathsEqualSafe(string a, string b)
289 |         {
290 |             if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false;
291 |             string na = NormalizePathSafe(a);
292 |             string nb = NormalizePathSafe(b);
293 |             try
294 |             {
295 |                 if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
296 |                 {
297 |                     return string.Equals(na, nb, StringComparison.OrdinalIgnoreCase);
298 |                 }
299 |                 return string.Equals(na, nb, StringComparison.Ordinal);
300 |             }
301 |             catch { return false; }
302 |         }
303 | 
304 |         private static IEnumerable<string> GetLegacyRootsForDetection()
305 |         {
306 |             var roots = new System.Collections.Generic.List<string>();
307 |             string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
308 |             // macOS/Linux legacy
309 |             roots.Add(Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer"));
310 |             roots.Add(Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer"));
311 |             // Windows roaming example
312 |             try
313 |             {
314 |                 string roaming = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
315 |                 if (!string.IsNullOrEmpty(roaming))
316 |                     roots.Add(Path.Combine(roaming, "UnityMCP", "UnityMcpServer"));
317 |                 // Windows legacy: early installers/dev scripts used %LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer
318 |                 // Detect this location so we can clean up older copies during install/update.
319 |                 string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
320 |                 if (!string.IsNullOrEmpty(localAppData))
321 |                     roots.Add(Path.Combine(localAppData, "Programs", "UnityMCP", "UnityMcpServer"));
322 |             }
323 |             catch { }
324 |             return roots;
325 |         }
326 | 
327 |         private static void TryKillUvForPath(string serverSrcPath)
328 |         {
329 |             try
330 |             {
331 |                 if (string.IsNullOrEmpty(serverSrcPath)) return;
332 |                 if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
333 | 
334 |                 var psi = new System.Diagnostics.ProcessStartInfo
335 |                 {
336 |                     FileName = "/usr/bin/pgrep",
337 |                     Arguments = $"-f \"uv .*--directory {serverSrcPath}\"",
338 |                     UseShellExecute = false,
339 |                     RedirectStandardOutput = true,
340 |                     RedirectStandardError = true,
341 |                     CreateNoWindow = true
342 |                 };
343 |                 using var p = System.Diagnostics.Process.Start(psi);
344 |                 if (p == null) return;
345 |                 string outp = p.StandardOutput.ReadToEnd();
346 |                 p.WaitForExit(1500);
347 |                 if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp))
348 |                 {
349 |                     foreach (var line in outp.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries))
350 |                     {
351 |                         if (int.TryParse(line.Trim(), out int pid))
352 |                         {
353 |                             try { System.Diagnostics.Process.GetProcessById(pid).Kill(); } catch { }
354 |                         }
355 |                     }
356 |                 }
357 |             }
358 |             catch { }
359 |         }
360 | 
361 |         private static string ReadVersionFile(string path)
362 |         {
363 |             try
364 |             {
365 |                 if (string.IsNullOrEmpty(path) || !File.Exists(path)) return null;
366 |                 string v = File.ReadAllText(path).Trim();
367 |                 return string.IsNullOrEmpty(v) ? null : v;
368 |             }
369 |             catch { return null; }
370 |         }
371 | 
372 |         private static int CompareSemverSafe(string a, string b)
373 |         {
374 |             try
375 |             {
376 |                 if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return 0;
377 |                 var ap = a.Split('.');
378 |                 var bp = b.Split('.');
379 |                 for (int i = 0; i < Math.Max(ap.Length, bp.Length); i++)
380 |                 {
381 |                     int ai = (i < ap.Length && int.TryParse(ap[i], out var t1)) ? t1 : 0;
382 |                     int bi = (i < bp.Length && int.TryParse(bp[i], out var t2)) ? t2 : 0;
383 |                     if (ai != bi) return ai.CompareTo(bi);
384 |                 }
385 |                 return 0;
386 |             }
387 |             catch { return 0; }
388 |         }
389 | 
390 |         /// <summary>
391 |         /// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package
392 |         /// or common development locations.
393 |         /// </summary>
394 |         private static bool TryGetEmbeddedServerSource(out string srcPath)
395 |         {
396 |             return ServerPathResolver.TryFindEmbeddedServerSource(out srcPath);
397 |         }
398 | 
399 |         private static readonly string[] _skipDirs = { ".venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".git" };
400 |         private static void CopyDirectoryRecursive(string sourceDir, string destinationDir)
401 |         {
402 |             Directory.CreateDirectory(destinationDir);
403 | 
404 |             foreach (string filePath in Directory.GetFiles(sourceDir))
405 |             {
406 |                 string fileName = Path.GetFileName(filePath);
407 |                 string destFile = Path.Combine(destinationDir, fileName);
408 |                 File.Copy(filePath, destFile, overwrite: true);
409 |             }
410 | 
411 |             foreach (string dirPath in Directory.GetDirectories(sourceDir))
412 |             {
413 |                 string dirName = Path.GetFileName(dirPath);
414 |                 foreach (var skip in _skipDirs)
415 |                 {
416 |                     if (dirName.Equals(skip, StringComparison.OrdinalIgnoreCase))
417 |                         goto NextDir;
418 |                 }
419 |                 try { if ((File.GetAttributes(dirPath) & FileAttributes.ReparsePoint) != 0) continue; } catch { }
420 |                 string destSubDir = Path.Combine(destinationDir, dirName);
421 |                 CopyDirectoryRecursive(dirPath, destSubDir);
422 |             NextDir:;
423 |             }
424 |         }
425 | 
426 |         public static bool RebuildMcpServer()
427 |         {
428 |             try
429 |             {
430 |                 // Find embedded source
431 |                 if (!TryGetEmbeddedServerSource(out string embeddedSrc))
432 |                 {
433 |                     Debug.LogError("RebuildMcpServer: Could not find embedded server source.");
434 |                     return false;
435 |                 }
436 | 
437 |                 string saveLocation = GetSaveLocation();
438 |                 string destRoot = Path.Combine(saveLocation, ServerFolder);
439 |                 string destSrc = Path.Combine(destRoot, "src");
440 | 
441 |                 // Kill any running uv processes for this server
442 |                 TryKillUvForPath(destSrc);
443 | 
444 |                 // Delete the entire installed server directory
445 |                 if (Directory.Exists(destRoot))
446 |                 {
447 |                     try
448 |                     {
449 |                         Directory.Delete(destRoot, recursive: true);
450 |                         Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Deleted existing server at {destRoot}");
451 |                     }
452 |                     catch (Exception ex)
453 |                     {
454 |                         Debug.LogError($"Failed to delete existing server: {ex.Message}");
455 |                         return false;
456 |                     }
457 |                 }
458 | 
459 |                 // Re-copy from embedded source
460 |                 string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc;
461 |                 Directory.CreateDirectory(destRoot);
462 |                 CopyDirectoryRecursive(embeddedRoot, destRoot);
463 | 
464 |                 // Write version file
465 |                 string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc, VersionFileName)) ?? "unknown";
466 |                 try
467 |                 {
468 |                     File.WriteAllText(Path.Combine(destSrc, VersionFileName), embeddedVer);
469 |                 }
470 |                 catch (Exception ex)
471 |                 {
472 |                     Debug.LogWarning($"Failed to write version file: {ex.Message}");
473 |                 }
474 | 
475 |                 Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Server rebuilt successfully at {destRoot} (version {embeddedVer})");
476 |                 return true;
477 |             }
478 |             catch (Exception ex)
479 |             {
480 |                 Debug.LogError($"RebuildMcpServer failed: {ex.Message}");
481 |                 return false;
482 |             }
483 |         }
484 | 
485 |         internal static string FindUvPath()
486 |         {
487 |             // Allow user override via EditorPrefs
488 |             try
489 |             {
490 |                 string overridePath = EditorPrefs.GetString("MCPForUnity.UvPath", string.Empty);
491 |                 if (!string.IsNullOrEmpty(overridePath) && File.Exists(overridePath))
492 |                 {
493 |                     if (ValidateUvBinary(overridePath)) return overridePath;
494 |                 }
495 |             }
496 |             catch { }
497 | 
498 |             string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
499 | 
500 |             // Platform-specific candidate lists
501 |             string[] candidates;
502 |             if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
503 |             {
504 |                 string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
505 |                 string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) ?? string.Empty;
506 |                 string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
507 | 
508 |                 // Fast path: resolve from PATH first
509 |                 try
510 |                 {
511 |                     var wherePsi = new System.Diagnostics.ProcessStartInfo
512 |                     {
513 |                         FileName = "where",
514 |                         Arguments = "uv.exe",
515 |                         UseShellExecute = false,
516 |                         RedirectStandardOutput = true,
517 |                         RedirectStandardError = true,
518 |                         CreateNoWindow = true
519 |                     };
520 |                     using var wp = System.Diagnostics.Process.Start(wherePsi);
521 |                     string output = wp.StandardOutput.ReadToEnd().Trim();
522 |                     wp.WaitForExit(1500);
523 |                     if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output))
524 |                     {
525 |                         foreach (var line in output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
526 |                         {
527 |                             string path = line.Trim();
528 |                             if (File.Exists(path) && ValidateUvBinary(path)) return path;
529 |                         }
530 |                     }
531 |                 }
532 |                 catch { }
533 | 
534 |                 // Windows Store (PythonSoftwareFoundation) install location probe
535 |                 // Example: %LOCALAPPDATA%\Packages\PythonSoftwareFoundation.Python.3.13_*\LocalCache\local-packages\Python313\Scripts\uv.exe
536 |                 try
537 |                 {
538 |                     string pkgsRoot = Path.Combine(localAppData, "Packages");
539 |                     if (Directory.Exists(pkgsRoot))
540 |                     {
541 |                         var pythonPkgs = Directory.GetDirectories(pkgsRoot, "PythonSoftwareFoundation.Python.*", SearchOption.TopDirectoryOnly)
542 |                                                  .OrderByDescending(p => p, StringComparer.OrdinalIgnoreCase);
543 |                         foreach (var pkg in pythonPkgs)
544 |                         {
545 |                             string localCache = Path.Combine(pkg, "LocalCache", "local-packages");
546 |                             if (!Directory.Exists(localCache)) continue;
547 |                             var pyRoots = Directory.GetDirectories(localCache, "Python*", SearchOption.TopDirectoryOnly)
548 |                                                    .OrderByDescending(d => d, StringComparer.OrdinalIgnoreCase);
549 |                             foreach (var pyRoot in pyRoots)
550 |                             {
551 |                                 string uvExe = Path.Combine(pyRoot, "Scripts", "uv.exe");
552 |                                 if (File.Exists(uvExe) && ValidateUvBinary(uvExe)) return uvExe;
553 |                             }
554 |                         }
555 |                     }
556 |                 }
557 |                 catch { }
558 | 
559 |                 candidates = new[]
560 |                 {
561 |                     // Preferred: WinGet Links shims (stable entrypoints)
562 |                     // Per-user shim (LOCALAPPDATA) → machine-wide shim (Program Files\WinGet\Links)
563 |                     Path.Combine(localAppData, "Microsoft", "WinGet", "Links", "uv.exe"),
564 |                     Path.Combine(programFiles, "WinGet", "Links", "uv.exe"),
565 | 
566 |                     // Common per-user installs
567 |                     Path.Combine(localAppData, @"Programs\Python\Python313\Scripts\uv.exe"),
568 |                     Path.Combine(localAppData, @"Programs\Python\Python312\Scripts\uv.exe"),
569 |                     Path.Combine(localAppData, @"Programs\Python\Python311\Scripts\uv.exe"),
570 |                     Path.Combine(localAppData, @"Programs\Python\Python310\Scripts\uv.exe"),
571 |                     Path.Combine(appData, @"Python\Python313\Scripts\uv.exe"),
572 |                     Path.Combine(appData, @"Python\Python312\Scripts\uv.exe"),
573 |                     Path.Combine(appData, @"Python\Python311\Scripts\uv.exe"),
574 |                     Path.Combine(appData, @"Python\Python310\Scripts\uv.exe"),
575 | 
576 |                     // Program Files style installs (if a native installer was used)
577 |                     Path.Combine(programFiles, @"uv\uv.exe"),
578 | 
579 |                     // Try simple name resolution later via PATH
580 |                     "uv.exe",
581 |                     "uv"
582 |                 };
583 |             }
584 |             else
585 |             {
586 |                 candidates = new[]
587 |                 {
588 |                     "/opt/homebrew/bin/uv",
589 |                     "/usr/local/bin/uv",
590 |                     "/usr/bin/uv",
591 |                     "/opt/local/bin/uv",
592 |                     Path.Combine(home, ".local", "bin", "uv"),
593 |                     "/opt/homebrew/opt/uv/bin/uv",
594 |                     // Framework Python installs
595 |                     "/Library/Frameworks/Python.framework/Versions/3.13/bin/uv",
596 |                     "/Library/Frameworks/Python.framework/Versions/3.12/bin/uv",
597 |                     // Fallback to PATH resolution by name
598 |                     "uv"
599 |                 };
600 |             }
601 | 
602 |             foreach (string c in candidates)
603 |             {
604 |                 try
605 |                 {
606 |                     if (File.Exists(c) && ValidateUvBinary(c)) return c;
607 |                 }
608 |                 catch { /* ignore */ }
609 |             }
610 | 
611 |             // Use platform-appropriate which/where to resolve from PATH (non-Windows handled here; Windows tried earlier)
612 |             try
613 |             {
614 |                 if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
615 |                 {
616 |                     var whichPsi = new System.Diagnostics.ProcessStartInfo
617 |                     {
618 |                         FileName = "/usr/bin/which",
619 |                         Arguments = "uv",
620 |                         UseShellExecute = false,
621 |                         RedirectStandardOutput = true,
622 |                         RedirectStandardError = true,
623 |                         CreateNoWindow = true
624 |                     };
625 |                     try
626 |                     {
627 |                         // Prepend common user-local and package manager locations so 'which' can see them in Unity's GUI env
628 |                         string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
629 |                         string prepend = string.Join(":", new[]
630 |                         {
631 |                             System.IO.Path.Combine(homeDir, ".local", "bin"),
632 |                             "/opt/homebrew/bin",
633 |                             "/usr/local/bin",
634 |                             "/usr/bin",
635 |                             "/bin"
636 |                         });
637 |                         string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
638 |                         whichPsi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath) ? prepend : (prepend + ":" + currentPath);
639 |                     }
640 |                     catch { }
641 |                     using var wp = System.Diagnostics.Process.Start(whichPsi);
642 |                     string output = wp.StandardOutput.ReadToEnd().Trim();
643 |                     wp.WaitForExit(3000);
644 |                     if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
645 |                     {
646 |                         if (ValidateUvBinary(output)) return output;
647 |                     }
648 |                 }
649 |             }
650 |             catch { }
651 | 
652 |             // Manual PATH scan
653 |             try
654 |             {
655 |                 string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
656 |                 string[] parts = pathEnv.Split(Path.PathSeparator);
657 |                 foreach (string part in parts)
658 |                 {
659 |                     try
660 |                     {
661 |                         // Check both uv and uv.exe
662 |                         string candidateUv = Path.Combine(part, "uv");
663 |                         string candidateUvExe = Path.Combine(part, "uv.exe");
664 |                         if (File.Exists(candidateUv) && ValidateUvBinary(candidateUv)) return candidateUv;
665 |                         if (File.Exists(candidateUvExe) && ValidateUvBinary(candidateUvExe)) return candidateUvExe;
666 |                     }
667 |                     catch { }
668 |                 }
669 |             }
670 |             catch { }
671 | 
672 |             return null;
673 |         }
674 | 
675 |         private static bool ValidateUvBinary(string uvPath)
676 |         {
677 |             try
678 |             {
679 |                 var psi = new System.Diagnostics.ProcessStartInfo
680 |                 {
681 |                     FileName = uvPath,
682 |                     Arguments = "--version",
683 |                     UseShellExecute = false,
684 |                     RedirectStandardOutput = true,
685 |                     RedirectStandardError = true,
686 |                     CreateNoWindow = true
687 |                 };
688 |                 using var p = System.Diagnostics.Process.Start(psi);
689 |                 if (!p.WaitForExit(5000)) { try { p.Kill(); } catch { } return false; }
690 |                 if (p.ExitCode == 0)
691 |                 {
692 |                     string output = p.StandardOutput.ReadToEnd().Trim();
693 |                     return output.StartsWith("uv ");
694 |                 }
695 |             }
696 |             catch { }
697 |             return false;
698 |         }
699 |     }
700 | }
701 | 
```
Page 9/19FirstPrevNextLast