#
tokens: 49068/50000 16/263 files (page 5/18)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 5 of 18. 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
│       │   │   │   ├── 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/Services/PathResolverService.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Diagnostics;
  3 | using System.IO;
  4 | using MCPForUnity.Editor.Helpers;
  5 | using UnityEditor;
  6 | using UnityEngine;
  7 | 
  8 | namespace MCPForUnity.Editor.Services
  9 | {
 10 |     /// <summary>
 11 |     /// Implementation of path resolver service with override support
 12 |     /// </summary>
 13 |     public class PathResolverService : IPathResolverService
 14 |     {
 15 |         private const string PythonDirOverrideKey = "MCPForUnity.PythonDirOverride";
 16 |         private const string UvPathOverrideKey = "MCPForUnity.UvPath";
 17 |         private const string ClaudeCliPathOverrideKey = "MCPForUnity.ClaudeCliPath";
 18 | 
 19 |         public bool HasMcpServerOverride => !string.IsNullOrEmpty(EditorPrefs.GetString(PythonDirOverrideKey, null));
 20 |         public bool HasUvPathOverride => !string.IsNullOrEmpty(EditorPrefs.GetString(UvPathOverrideKey, null));
 21 |         public bool HasClaudeCliPathOverride => !string.IsNullOrEmpty(EditorPrefs.GetString(ClaudeCliPathOverrideKey, null));
 22 | 
 23 |         public string GetMcpServerPath()
 24 |         {
 25 |             // Check for override first
 26 |             string overridePath = EditorPrefs.GetString(PythonDirOverrideKey, null);
 27 |             if (!string.IsNullOrEmpty(overridePath) && File.Exists(Path.Combine(overridePath, "server.py")))
 28 |             {
 29 |                 return overridePath;
 30 |             }
 31 | 
 32 |             // Fall back to automatic detection
 33 |             return McpPathResolver.FindPackagePythonDirectory(false);
 34 |         }
 35 | 
 36 |         public string GetUvPath()
 37 |         {
 38 |             // Check for override first
 39 |             string overridePath = EditorPrefs.GetString(UvPathOverrideKey, null);
 40 |             if (!string.IsNullOrEmpty(overridePath) && File.Exists(overridePath))
 41 |             {
 42 |                 return overridePath;
 43 |             }
 44 | 
 45 |             // Fall back to automatic detection
 46 |             try
 47 |             {
 48 |                 return ServerInstaller.FindUvPath();
 49 |             }
 50 |             catch
 51 |             {
 52 |                 return null;
 53 |             }
 54 |         }
 55 | 
 56 |         public string GetClaudeCliPath()
 57 |         {
 58 |             // Check for override first
 59 |             string overridePath = EditorPrefs.GetString(ClaudeCliPathOverrideKey, null);
 60 |             if (!string.IsNullOrEmpty(overridePath) && File.Exists(overridePath))
 61 |             {
 62 |                 return overridePath;
 63 |             }
 64 | 
 65 |             // Fall back to automatic detection
 66 |             return ExecPath.ResolveClaude();
 67 |         }
 68 | 
 69 |         public bool IsPythonDetected()
 70 |         {
 71 |             try
 72 |             {
 73 |                 // Windows-specific Python detection
 74 |                 if (Application.platform == RuntimePlatform.WindowsEditor)
 75 |                 {
 76 |                     // Common Windows Python installation paths
 77 |                     string[] windowsCandidates =
 78 |                     {
 79 |                         @"C:\Python313\python.exe",
 80 |                         @"C:\Python312\python.exe",
 81 |                         @"C:\Python311\python.exe",
 82 |                         @"C:\Python310\python.exe",
 83 |                         @"C:\Python39\python.exe",
 84 |                         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python313\python.exe"),
 85 |                         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python312\python.exe"),
 86 |                         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python311\python.exe"),
 87 |                         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python310\python.exe"),
 88 |                         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python39\python.exe"),
 89 |                         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python313\python.exe"),
 90 |                         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python312\python.exe"),
 91 |                         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python311\python.exe"),
 92 |                         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python310\python.exe"),
 93 |                         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python39\python.exe"),
 94 |                     };
 95 | 
 96 |                     foreach (string c in windowsCandidates)
 97 |                     {
 98 |                         if (File.Exists(c)) return true;
 99 |                     }
100 | 
101 |                     // Try 'where python' command (Windows equivalent of 'which')
102 |                     var psi = new ProcessStartInfo
103 |                     {
104 |                         FileName = "where",
105 |                         Arguments = "python",
106 |                         UseShellExecute = false,
107 |                         RedirectStandardOutput = true,
108 |                         RedirectStandardError = true,
109 |                         CreateNoWindow = true
110 |                     };
111 |                     using (var p = Process.Start(psi))
112 |                     {
113 |                         string outp = p.StandardOutput.ReadToEnd().Trim();
114 |                         p.WaitForExit(2000);
115 |                         if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp))
116 |                         {
117 |                             string[] lines = outp.Split('\n');
118 |                             foreach (string line in lines)
119 |                             {
120 |                                 string trimmed = line.Trim();
121 |                                 if (File.Exists(trimmed)) return true;
122 |                             }
123 |                         }
124 |                     }
125 |                 }
126 |                 else
127 |                 {
128 |                     // macOS/Linux detection
129 |                     string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
130 |                     string[] candidates =
131 |                     {
132 |                         "/opt/homebrew/bin/python3",
133 |                         "/usr/local/bin/python3",
134 |                         "/usr/bin/python3",
135 |                         "/opt/local/bin/python3",
136 |                         Path.Combine(home, ".local", "bin", "python3"),
137 |                         "/Library/Frameworks/Python.framework/Versions/3.13/bin/python3",
138 |                         "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3",
139 |                     };
140 |                     foreach (string c in candidates)
141 |                     {
142 |                         if (File.Exists(c)) return true;
143 |                     }
144 | 
145 |                     // Try 'which python3'
146 |                     var psi = new ProcessStartInfo
147 |                     {
148 |                         FileName = "/usr/bin/which",
149 |                         Arguments = "python3",
150 |                         UseShellExecute = false,
151 |                         RedirectStandardOutput = true,
152 |                         RedirectStandardError = true,
153 |                         CreateNoWindow = true
154 |                     };
155 |                     using (var p = Process.Start(psi))
156 |                     {
157 |                         string outp = p.StandardOutput.ReadToEnd().Trim();
158 |                         p.WaitForExit(2000);
159 |                         if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp) && File.Exists(outp)) return true;
160 |                     }
161 |                 }
162 |             }
163 |             catch { }
164 |             return false;
165 |         }
166 | 
167 |         public bool IsUvDetected()
168 |         {
169 |             return !string.IsNullOrEmpty(GetUvPath());
170 |         }
171 | 
172 |         public bool IsClaudeCliDetected()
173 |         {
174 |             return !string.IsNullOrEmpty(GetClaudeCliPath());
175 |         }
176 | 
177 |         public void SetMcpServerOverride(string path)
178 |         {
179 |             if (string.IsNullOrEmpty(path))
180 |             {
181 |                 ClearMcpServerOverride();
182 |                 return;
183 |             }
184 | 
185 |             if (!File.Exists(Path.Combine(path, "server.py")))
186 |             {
187 |                 throw new ArgumentException("The selected folder does not contain server.py");
188 |             }
189 | 
190 |             EditorPrefs.SetString(PythonDirOverrideKey, path);
191 |         }
192 | 
193 |         public void SetUvPathOverride(string path)
194 |         {
195 |             if (string.IsNullOrEmpty(path))
196 |             {
197 |                 ClearUvPathOverride();
198 |                 return;
199 |             }
200 | 
201 |             if (!File.Exists(path))
202 |             {
203 |                 throw new ArgumentException("The selected UV executable does not exist");
204 |             }
205 | 
206 |             EditorPrefs.SetString(UvPathOverrideKey, path);
207 |         }
208 | 
209 |         public void SetClaudeCliPathOverride(string path)
210 |         {
211 |             if (string.IsNullOrEmpty(path))
212 |             {
213 |                 ClearClaudeCliPathOverride();
214 |                 return;
215 |             }
216 | 
217 |             if (!File.Exists(path))
218 |             {
219 |                 throw new ArgumentException("The selected Claude CLI executable does not exist");
220 |             }
221 | 
222 |             EditorPrefs.SetString(ClaudeCliPathOverrideKey, path);
223 |             // Also update the ExecPath helper for backwards compatibility
224 |             ExecPath.SetClaudeCliPath(path);
225 |         }
226 | 
227 |         public void ClearMcpServerOverride()
228 |         {
229 |             EditorPrefs.DeleteKey(PythonDirOverrideKey);
230 |         }
231 | 
232 |         public void ClearUvPathOverride()
233 |         {
234 |             EditorPrefs.DeleteKey(UvPathOverrideKey);
235 |         }
236 | 
237 |         public void ClearClaudeCliPathOverride()
238 |         {
239 |             EditorPrefs.DeleteKey(ClaudeCliPathOverrideKey);
240 |         }
241 |     }
242 | }
243 | 
```

--------------------------------------------------------------------------------
/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/WriteToConfigTests.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Diagnostics;
  3 | using System.IO;
  4 | using System.Runtime.InteropServices;
  5 | using Newtonsoft.Json.Linq;
  6 | using NUnit.Framework;
  7 | using UnityEditor;
  8 | using MCPForUnity.Editor.Helpers;
  9 | using MCPForUnity.Editor.Models;
 10 | 
 11 | namespace MCPForUnityTests.Editor.Helpers
 12 | {
 13 |     public class WriteToConfigTests
 14 |     {
 15 |         private string _tempRoot;
 16 |         private string _fakeUvPath;
 17 |         private string _serverSrcDir;
 18 | 
 19 |         [SetUp]
 20 |         public void SetUp()
 21 |         {
 22 |             // Tests are designed for Linux/macOS runners. Skip on Windows due to ProcessStartInfo
 23 |             // restrictions when UseShellExecute=false for .cmd/.bat scripts.
 24 |             if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
 25 |             {
 26 |                 Assert.Ignore("WriteToConfig tests are skipped on Windows (CI runs linux).\n" +
 27 |                               "ValidateUvBinarySafe requires launching an actual exe on Windows.");
 28 |             }
 29 |             _tempRoot = Path.Combine(Path.GetTempPath(), "UnityMCPTests", Guid.NewGuid().ToString("N"));
 30 |             Directory.CreateDirectory(_tempRoot);
 31 | 
 32 |             // Create a fake uv executable that prints a valid version string
 33 |             _fakeUvPath = Path.Combine(_tempRoot, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "uv.cmd" : "uv");
 34 |             File.WriteAllText(_fakeUvPath, "#!/bin/sh\n\necho 'uv 9.9.9'\n");
 35 |             TryChmodX(_fakeUvPath);
 36 | 
 37 |             // Create a fake server directory with server.py
 38 |             _serverSrcDir = Path.Combine(_tempRoot, "server-src");
 39 |             Directory.CreateDirectory(_serverSrcDir);
 40 |             File.WriteAllText(Path.Combine(_serverSrcDir, "server.py"), "# dummy server\n");
 41 | 
 42 |             // Point the editor to our server dir (so ResolveServerSrc() uses this)
 43 |             EditorPrefs.SetString("MCPForUnity.ServerSrc", _serverSrcDir);
 44 |             // Ensure no lock is enabled
 45 |             EditorPrefs.SetBool("MCPForUnity.LockCursorConfig", false);
 46 |             // Disable auto-registration to avoid hitting user configs during tests
 47 |             EditorPrefs.SetBool("MCPForUnity.AutoRegisterEnabled", false);
 48 |         }
 49 | 
 50 |         [TearDown]
 51 |         public void TearDown()
 52 |         {
 53 |             // Clean up editor preferences set during SetUp
 54 |             EditorPrefs.DeleteKey("MCPForUnity.ServerSrc");
 55 |             EditorPrefs.DeleteKey("MCPForUnity.LockCursorConfig");
 56 |             EditorPrefs.DeleteKey("MCPForUnity.AutoRegisterEnabled");
 57 | 
 58 |             // Remove temp files
 59 |             try { if (Directory.Exists(_tempRoot)) Directory.Delete(_tempRoot, true); } catch { }
 60 |         }
 61 | 
 62 |         // --- Tests ---
 63 | 
 64 |         [Test]
 65 |         public void AddsEnvAndDisabledFalse_ForWindsurf()
 66 |         {
 67 |             var configPath = Path.Combine(_tempRoot, "windsurf.json");
 68 |             WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");
 69 | 
 70 |             var client = new McpClient { name = "Windsurf", mcpType = McpTypes.Windsurf };
 71 |             InvokeWriteToConfig(configPath, client);
 72 | 
 73 |             var root = JObject.Parse(File.ReadAllText(configPath));
 74 |             var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
 75 |             Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
 76 |             Assert.NotNull(unity["env"], "env should be present for all clients");
 77 |             Assert.IsTrue(unity["env"]!.Type == JTokenType.Object, "env should be an object");
 78 |             Assert.AreEqual(false, (bool)unity["disabled"], "disabled:false should be set for Windsurf when missing");
 79 |         }
 80 | 
 81 |         [Test]
 82 |         public void AddsEnvAndDisabledFalse_ForKiro()
 83 |         {
 84 |             var configPath = Path.Combine(_tempRoot, "kiro.json");
 85 |             WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");
 86 | 
 87 |             var client = new McpClient { name = "Kiro", mcpType = McpTypes.Kiro };
 88 |             InvokeWriteToConfig(configPath, client);
 89 | 
 90 |             var root = JObject.Parse(File.ReadAllText(configPath));
 91 |             var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
 92 |             Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
 93 |             Assert.NotNull(unity["env"], "env should be present for all clients");
 94 |             Assert.IsTrue(unity["env"]!.Type == JTokenType.Object, "env should be an object");
 95 |             Assert.AreEqual(false, (bool)unity["disabled"], "disabled:false should be set for Kiro when missing");
 96 |         }
 97 | 
 98 |         [Test]
 99 |         public void DoesNotAddEnvOrDisabled_ForCursor()
100 |         {
101 |             var configPath = Path.Combine(_tempRoot, "cursor.json");
102 |             WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");
103 | 
104 |             var client = new McpClient { name = "Cursor", mcpType = McpTypes.Cursor };
105 |             InvokeWriteToConfig(configPath, client);
106 | 
107 |             var root = JObject.Parse(File.ReadAllText(configPath));
108 |             var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
109 |             Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
110 |             Assert.IsNull(unity["env"], "env should not be added for non-Windsurf/Kiro clients");
111 |             Assert.IsNull(unity["disabled"], "disabled should not be added for non-Windsurf/Kiro clients");
112 |         }
113 | 
114 |         [Test]
115 |         public void DoesNotAddEnvOrDisabled_ForVSCode()
116 |         {
117 |             var configPath = Path.Combine(_tempRoot, "vscode.json");
118 |             WriteInitialConfig(configPath, isVSCode: true, command: _fakeUvPath, directory: "/old/path");
119 | 
120 |             var client = new McpClient { name = "VSCode", mcpType = McpTypes.VSCode };
121 |             InvokeWriteToConfig(configPath, client);
122 | 
123 |             var root = JObject.Parse(File.ReadAllText(configPath));
124 |             var unity = (JObject)root.SelectToken("servers.unityMCP");
125 |             Assert.NotNull(unity, "Expected servers.unityMCP node");
126 |             Assert.IsNull(unity["env"], "env should not be added for VSCode client");
127 |             Assert.IsNull(unity["disabled"], "disabled should not be added for VSCode client");
128 |             Assert.AreEqual("stdio", (string)unity["type"], "VSCode entry should include type=stdio");
129 |         }
130 | 
131 |         [Test]
132 |         public void PreservesExistingEnvAndDisabled()
133 |         {
134 |             var configPath = Path.Combine(_tempRoot, "preserve.json");
135 | 
136 |             // Existing config with env and disabled=true should be preserved
137 |             var json = new JObject
138 |             {
139 |                 ["mcpServers"] = new JObject
140 |                 {
141 |                     ["unityMCP"] = new JObject
142 |                     {
143 |                         ["command"] = _fakeUvPath,
144 |                         ["args"] = new JArray("run", "--directory", "/old/path", "server.py"),
145 |                         ["env"] = new JObject { ["FOO"] = "bar" },
146 |                         ["disabled"] = true
147 |                     }
148 |                 }
149 |             };
150 |             File.WriteAllText(configPath, json.ToString());
151 | 
152 |             var client = new McpClient { name = "Windsurf", mcpType = McpTypes.Windsurf };
153 |             InvokeWriteToConfig(configPath, client);
154 | 
155 |             var root = JObject.Parse(File.ReadAllText(configPath));
156 |             var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
157 |             Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
158 |             Assert.AreEqual("bar", (string)unity["env"]!["FOO"], "Existing env should be preserved");
159 |             Assert.AreEqual(true, (bool)unity["disabled"], "Existing disabled value should be preserved");
160 |         }
161 | 
162 |         // --- Helpers ---
163 | 
164 |         private static void TryChmodX(string path)
165 |         {
166 |             try
167 |             {
168 |                 var psi = new ProcessStartInfo
169 |                 {
170 |                     FileName = "/bin/chmod",
171 |                     Arguments = "+x \"" + path + "\"",
172 |                     UseShellExecute = false,
173 |                     RedirectStandardOutput = true,
174 |                     RedirectStandardError = true,
175 |                     CreateNoWindow = true
176 |                 };
177 |                 using var p = Process.Start(psi);
178 |                 p?.WaitForExit(2000);
179 |             }
180 |             catch { /* best-effort on non-Unix */ }
181 |         }
182 | 
183 |         private static void WriteInitialConfig(string configPath, bool isVSCode, string command, string directory)
184 |         {
185 |             Directory.CreateDirectory(Path.GetDirectoryName(configPath)!);
186 |             JObject root;
187 |             if (isVSCode)
188 |             {
189 |                 root = new JObject
190 |                 {
191 |                     ["servers"] = new JObject
192 |                     {
193 |                         ["unityMCP"] = new JObject
194 |                         {
195 |                             ["command"] = command,
196 |                             ["args"] = new JArray("run", "--directory", directory, "server.py"),
197 |                             ["type"] = "stdio"
198 |                         }
199 |                     }
200 |                 };
201 |             }
202 |             else
203 |             {
204 |                 root = new JObject
205 |                 {
206 |                     ["mcpServers"] = new JObject
207 |                     {
208 |                         ["unityMCP"] = new JObject
209 |                         {
210 |                             ["command"] = command,
211 |                             ["args"] = new JArray("run", "--directory", directory, "server.py")
212 |                         }
213 |                     }
214 |                 };
215 |             }
216 |             File.WriteAllText(configPath, root.ToString());
217 |         }
218 | 
219 |         private static void InvokeWriteToConfig(string configPath, McpClient client)
220 |         {
221 |             var result = McpConfigurationHelper.WriteMcpConfiguration(
222 |                 pythonDir: string.Empty,
223 |                 configPath: configPath,
224 |                 mcpClient: client
225 |             );
226 | 
227 |             Assert.AreEqual("Configured successfully", result, "WriteMcpConfiguration should return success");
228 |         }
229 |     }
230 | }
231 | 
```

--------------------------------------------------------------------------------
/MCPForUnity/Editor/Windows/VSCodeManualSetupWindow.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System.Runtime.InteropServices;
  2 | using UnityEditor;
  3 | using UnityEngine;
  4 | using MCPForUnity.Editor.Models;
  5 | 
  6 | namespace MCPForUnity.Editor.Windows
  7 | {
  8 |     public class VSCodeManualSetupWindow : ManualConfigEditorWindow
  9 |     {
 10 |         public static void ShowWindow(string configPath, string configJson)
 11 |         {
 12 |             var window = GetWindow<VSCodeManualSetupWindow>("VSCode GitHub Copilot Setup");
 13 |             window.configPath = configPath;
 14 |             window.configJson = configJson;
 15 |             window.minSize = new Vector2(550, 500);
 16 | 
 17 |             // Create a McpClient for VSCode
 18 |             window.mcpClient = new McpClient
 19 |             {
 20 |                 name = "VSCode GitHub Copilot",
 21 |                 mcpType = McpTypes.VSCode
 22 |             };
 23 | 
 24 |             window.Show();
 25 |         }
 26 | 
 27 |         protected override void OnGUI()
 28 |         {
 29 |             scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
 30 | 
 31 |             // Header with improved styling
 32 |             EditorGUILayout.Space(10);
 33 |             Rect titleRect = EditorGUILayout.GetControlRect(false, 30);
 34 |             EditorGUI.DrawRect(
 35 |                 new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height),
 36 |                 new Color(0.2f, 0.2f, 0.2f, 0.1f)
 37 |             );
 38 |             GUI.Label(
 39 |                 new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height),
 40 |                 "VSCode GitHub Copilot MCP Setup",
 41 |                 EditorStyles.boldLabel
 42 |             );
 43 |             EditorGUILayout.Space(10);
 44 | 
 45 |             // Instructions with improved styling
 46 |             EditorGUILayout.BeginVertical(EditorStyles.helpBox);
 47 | 
 48 |             Rect headerRect = EditorGUILayout.GetControlRect(false, 24);
 49 |             EditorGUI.DrawRect(
 50 |                 new Rect(headerRect.x, headerRect.y, headerRect.width, headerRect.height),
 51 |                 new Color(0.1f, 0.1f, 0.1f, 0.2f)
 52 |             );
 53 |             GUI.Label(
 54 |                 new Rect(
 55 |                     headerRect.x + 8,
 56 |                     headerRect.y + 4,
 57 |                     headerRect.width - 16,
 58 |                     headerRect.height
 59 |                 ),
 60 |                 "Setting up GitHub Copilot in VSCode with MCP for Unity",
 61 |                 EditorStyles.boldLabel
 62 |             );
 63 |             EditorGUILayout.Space(10);
 64 | 
 65 |             GUIStyle instructionStyle = new(EditorStyles.wordWrappedLabel)
 66 |             {
 67 |                 margin = new RectOffset(10, 10, 5, 5),
 68 |             };
 69 | 
 70 |             EditorGUILayout.LabelField(
 71 |                 "1. Prerequisites",
 72 |                 EditorStyles.boldLabel
 73 |             );
 74 |             EditorGUILayout.LabelField(
 75 |                 "• Ensure you have VSCode installed",
 76 |                 instructionStyle
 77 |             );
 78 |             EditorGUILayout.LabelField(
 79 |                 "• Ensure you have GitHub Copilot extension installed in VSCode",
 80 |                 instructionStyle
 81 |             );
 82 |             EditorGUILayout.LabelField(
 83 |                 "• Ensure you have a valid GitHub Copilot subscription",
 84 |                 instructionStyle
 85 |             );
 86 |             EditorGUILayout.Space(5);
 87 | 
 88 |             EditorGUILayout.LabelField(
 89 |                 "2. Steps to Configure",
 90 |                 EditorStyles.boldLabel
 91 |             );
 92 |             EditorGUILayout.LabelField(
 93 |                 "a) Open or create your VSCode MCP config file (mcp.json) at the path below",
 94 |                 instructionStyle
 95 |             );
 96 |             EditorGUILayout.LabelField(
 97 |                 "b) Paste the JSON shown below into mcp.json",
 98 |                 instructionStyle
 99 |             );
100 |             EditorGUILayout.LabelField(
101 |                 "c) Save the file and restart VSCode",
102 |                 instructionStyle
103 |             );
104 |             EditorGUILayout.Space(5);
105 | 
106 |             EditorGUILayout.LabelField(
107 |                 "3. VSCode mcp.json location:",
108 |                 EditorStyles.boldLabel
109 |             );
110 | 
111 |             // Path section with improved styling
112 |             EditorGUILayout.BeginVertical(EditorStyles.helpBox);
113 |             string displayPath;
114 |             if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
115 |             {
116 |                 displayPath = System.IO.Path.Combine(
117 |                     System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData),
118 |                     "Code",
119 |                     "User",
120 |                     "mcp.json"
121 |                 );
122 |             }
123 |             else
124 |             {
125 |                 displayPath = System.IO.Path.Combine(
126 |                     System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile),
127 |                     "Library",
128 |                     "Application Support",
129 |                     "Code",
130 |                     "User",
131 |                     "mcp.json"
132 |                 );
133 |             }
134 | 
135 |             // Store the path in the base class config path
136 |             if (string.IsNullOrEmpty(configPath))
137 |             {
138 |                 configPath = displayPath;
139 |             }
140 | 
141 |             // Prevent text overflow by allowing the text field to wrap
142 |             GUIStyle pathStyle = new(EditorStyles.textField) { wordWrap = true };
143 | 
144 |             EditorGUILayout.TextField(
145 |                 displayPath,
146 |                 pathStyle,
147 |                 GUILayout.Height(EditorGUIUtility.singleLineHeight)
148 |             );
149 | 
150 |             // Copy button with improved styling
151 |             EditorGUILayout.BeginHorizontal();
152 |             GUILayout.FlexibleSpace();
153 |             GUIStyle copyButtonStyle = new(GUI.skin.button)
154 |             {
155 |                 padding = new RectOffset(15, 15, 5, 5),
156 |                 margin = new RectOffset(10, 10, 5, 5),
157 |             };
158 | 
159 |             if (
160 |                 GUILayout.Button(
161 |                     "Copy Path",
162 |                     copyButtonStyle,
163 |                     GUILayout.Height(25),
164 |                     GUILayout.Width(100)
165 |                 )
166 |             )
167 |             {
168 |                 EditorGUIUtility.systemCopyBuffer = displayPath;
169 |                 pathCopied = true;
170 |                 copyFeedbackTimer = 2f;
171 |             }
172 | 
173 |             if (
174 |                 GUILayout.Button(
175 |                     "Open File",
176 |                     copyButtonStyle,
177 |                     GUILayout.Height(25),
178 |                     GUILayout.Width(100)
179 |                 )
180 |             )
181 |             {
182 |                 // Open the file using the system's default application
183 |                 System.Diagnostics.Process.Start(
184 |                     new System.Diagnostics.ProcessStartInfo
185 |                     {
186 |                         FileName = displayPath,
187 |                         UseShellExecute = true,
188 |                     }
189 |                 );
190 |             }
191 | 
192 |             if (pathCopied)
193 |             {
194 |                 GUIStyle feedbackStyle = new(EditorStyles.label);
195 |                 feedbackStyle.normal.textColor = Color.green;
196 |                 EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60));
197 |             }
198 | 
199 |             EditorGUILayout.EndHorizontal();
200 |             EditorGUILayout.EndVertical();
201 |             EditorGUILayout.Space(10);
202 | 
203 |             EditorGUILayout.LabelField(
204 |                 "4. Add this configuration to your mcp.json:",
205 |                 EditorStyles.boldLabel
206 |             );
207 | 
208 |             // JSON section with improved styling
209 |             EditorGUILayout.BeginVertical(EditorStyles.helpBox);
210 | 
211 |             // Improved text area for JSON with syntax highlighting colors
212 |             GUIStyle jsonStyle = new(EditorStyles.textArea)
213 |             {
214 |                 font = EditorStyles.boldFont,
215 |                 wordWrap = true,
216 |             };
217 |             jsonStyle.normal.textColor = new Color(0.3f, 0.6f, 0.9f); // Syntax highlighting blue
218 | 
219 |             // Draw the JSON in a text area with a taller height for better readability
220 |             EditorGUILayout.TextArea(configJson, jsonStyle, GUILayout.Height(200));
221 | 
222 |             // Copy JSON button with improved styling
223 |             EditorGUILayout.BeginHorizontal();
224 |             GUILayout.FlexibleSpace();
225 | 
226 |             if (
227 |                 GUILayout.Button(
228 |                     "Copy JSON",
229 |                     copyButtonStyle,
230 |                     GUILayout.Height(25),
231 |                     GUILayout.Width(100)
232 |                 )
233 |             )
234 |             {
235 |                 EditorGUIUtility.systemCopyBuffer = configJson;
236 |                 jsonCopied = true;
237 |                 copyFeedbackTimer = 2f;
238 |             }
239 | 
240 |             if (jsonCopied)
241 |             {
242 |                 GUIStyle feedbackStyle = new(EditorStyles.label);
243 |                 feedbackStyle.normal.textColor = Color.green;
244 |                 EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60));
245 |             }
246 | 
247 |             EditorGUILayout.EndHorizontal();
248 |             EditorGUILayout.EndVertical();
249 | 
250 |             EditorGUILayout.Space(10);
251 |             EditorGUILayout.LabelField(
252 |                 "5. After configuration:",
253 |                 EditorStyles.boldLabel
254 |             );
255 |             EditorGUILayout.LabelField(
256 |                 "• Restart VSCode",
257 |                 instructionStyle
258 |             );
259 |             EditorGUILayout.LabelField(
260 |                 "• GitHub Copilot will now be able to interact with your Unity project through the MCP protocol",
261 |                 instructionStyle
262 |             );
263 |             EditorGUILayout.LabelField(
264 |                 "• Remember to have the MCP for Unity Bridge running in Unity Editor",
265 |                 instructionStyle
266 |             );
267 | 
268 |             EditorGUILayout.EndVertical();
269 | 
270 |             EditorGUILayout.Space(10);
271 | 
272 |             // Close button at the bottom
273 |             EditorGUILayout.BeginHorizontal();
274 |             GUILayout.FlexibleSpace();
275 |             if (GUILayout.Button("Close", GUILayout.Height(30), GUILayout.Width(100)))
276 |             {
277 |                 Close();
278 |             }
279 |             GUILayout.FlexibleSpace();
280 |             EditorGUILayout.EndHorizontal();
281 | 
282 |             EditorGUILayout.EndScrollView();
283 |         }
284 | 
285 |         protected override void Update()
286 |         {
287 |             // Call the base implementation which handles the copy feedback timer
288 |             base.Update();
289 |         }
290 |     }
291 | }
292 | 
```

--------------------------------------------------------------------------------
/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System.Runtime.InteropServices;
  2 | using UnityEditor;
  3 | using UnityEngine;
  4 | using MCPForUnity.Editor.Models;
  5 | 
  6 | namespace MCPForUnity.Editor.Windows
  7 | {
  8 |     public class VSCodeManualSetupWindow : ManualConfigEditorWindow
  9 |     {
 10 |         public static void ShowWindow(string configPath, string configJson)
 11 |         {
 12 |             var window = GetWindow<VSCodeManualSetupWindow>("VSCode GitHub Copilot Setup");
 13 |             window.configPath = configPath;
 14 |             window.configJson = configJson;
 15 |             window.minSize = new Vector2(550, 500);
 16 | 
 17 |             // Create a McpClient for VSCode
 18 |             window.mcpClient = new McpClient
 19 |             {
 20 |                 name = "VSCode GitHub Copilot",
 21 |                 mcpType = McpTypes.VSCode
 22 |             };
 23 | 
 24 |             window.Show();
 25 |         }
 26 | 
 27 |         protected override void OnGUI()
 28 |         {
 29 |             scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
 30 | 
 31 |             // Header with improved styling
 32 |             EditorGUILayout.Space(10);
 33 |             Rect titleRect = EditorGUILayout.GetControlRect(false, 30);
 34 |             EditorGUI.DrawRect(
 35 |                 new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height),
 36 |                 new Color(0.2f, 0.2f, 0.2f, 0.1f)
 37 |             );
 38 |             GUI.Label(
 39 |                 new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height),
 40 |                 "VSCode GitHub Copilot MCP Setup",
 41 |                 EditorStyles.boldLabel
 42 |             );
 43 |             EditorGUILayout.Space(10);
 44 | 
 45 |             // Instructions with improved styling
 46 |             EditorGUILayout.BeginVertical(EditorStyles.helpBox);
 47 | 
 48 |             Rect headerRect = EditorGUILayout.GetControlRect(false, 24);
 49 |             EditorGUI.DrawRect(
 50 |                 new Rect(headerRect.x, headerRect.y, headerRect.width, headerRect.height),
 51 |                 new Color(0.1f, 0.1f, 0.1f, 0.2f)
 52 |             );
 53 |             GUI.Label(
 54 |                 new Rect(
 55 |                     headerRect.x + 8,
 56 |                     headerRect.y + 4,
 57 |                     headerRect.width - 16,
 58 |                     headerRect.height
 59 |                 ),
 60 |                 "Setting up GitHub Copilot in VSCode with MCP for Unity",
 61 |                 EditorStyles.boldLabel
 62 |             );
 63 |             EditorGUILayout.Space(10);
 64 | 
 65 |             GUIStyle instructionStyle = new(EditorStyles.wordWrappedLabel)
 66 |             {
 67 |                 margin = new RectOffset(10, 10, 5, 5),
 68 |             };
 69 | 
 70 |             EditorGUILayout.LabelField(
 71 |                 "1. Prerequisites",
 72 |                 EditorStyles.boldLabel
 73 |             );
 74 |             EditorGUILayout.LabelField(
 75 |                 "• Ensure you have VSCode installed",
 76 |                 instructionStyle
 77 |             );
 78 |             EditorGUILayout.LabelField(
 79 |                 "• Ensure you have GitHub Copilot extension installed in VSCode",
 80 |                 instructionStyle
 81 |             );
 82 |             EditorGUILayout.LabelField(
 83 |                 "• Ensure you have a valid GitHub Copilot subscription",
 84 |                 instructionStyle
 85 |             );
 86 |             EditorGUILayout.Space(5);
 87 | 
 88 |             EditorGUILayout.LabelField(
 89 |                 "2. Steps to Configure",
 90 |                 EditorStyles.boldLabel
 91 |             );
 92 |             EditorGUILayout.LabelField(
 93 |                 "a) Open or create your VSCode MCP config file (mcp.json) at the path below",
 94 |                 instructionStyle
 95 |             );
 96 |             EditorGUILayout.LabelField(
 97 |                 "b) Paste the JSON shown below into mcp.json",
 98 |                 instructionStyle
 99 |             );
100 |             EditorGUILayout.LabelField(
101 |                 "c) Save the file and restart VSCode",
102 |                 instructionStyle
103 |             );
104 |             EditorGUILayout.Space(5);
105 | 
106 |             EditorGUILayout.LabelField(
107 |                 "3. VSCode mcp.json location:",
108 |                 EditorStyles.boldLabel
109 |             );
110 | 
111 |             // Path section with improved styling
112 |             EditorGUILayout.BeginVertical(EditorStyles.helpBox);
113 |             string displayPath;
114 |             if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
115 |             {
116 |                 displayPath = System.IO.Path.Combine(
117 |                     System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData),
118 |                     "Code",
119 |                     "User",
120 |                     "mcp.json"
121 |                 );
122 |             }
123 |             else
124 |             {
125 |                 displayPath = System.IO.Path.Combine(
126 |                     System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile),
127 |                     "Library",
128 |                     "Application Support",
129 |                     "Code",
130 |                     "User",
131 |                     "mcp.json"
132 |                 );
133 |             }
134 | 
135 |             // Store the path in the base class config path
136 |             if (string.IsNullOrEmpty(configPath))
137 |             {
138 |                 configPath = displayPath;
139 |             }
140 | 
141 |             // Prevent text overflow by allowing the text field to wrap
142 |             GUIStyle pathStyle = new(EditorStyles.textField) { wordWrap = true };
143 | 
144 |             EditorGUILayout.TextField(
145 |                 displayPath,
146 |                 pathStyle,
147 |                 GUILayout.Height(EditorGUIUtility.singleLineHeight)
148 |             );
149 | 
150 |             // Copy button with improved styling
151 |             EditorGUILayout.BeginHorizontal();
152 |             GUILayout.FlexibleSpace();
153 |             GUIStyle copyButtonStyle = new(GUI.skin.button)
154 |             {
155 |                 padding = new RectOffset(15, 15, 5, 5),
156 |                 margin = new RectOffset(10, 10, 5, 5),
157 |             };
158 | 
159 |             if (
160 |                 GUILayout.Button(
161 |                     "Copy Path",
162 |                     copyButtonStyle,
163 |                     GUILayout.Height(25),
164 |                     GUILayout.Width(100)
165 |                 )
166 |             )
167 |             {
168 |                 EditorGUIUtility.systemCopyBuffer = displayPath;
169 |                 pathCopied = true;
170 |                 copyFeedbackTimer = 2f;
171 |             }
172 | 
173 |             if (
174 |                 GUILayout.Button(
175 |                     "Open File",
176 |                     copyButtonStyle,
177 |                     GUILayout.Height(25),
178 |                     GUILayout.Width(100)
179 |                 )
180 |             )
181 |             {
182 |                 // Open the file using the system's default application
183 |                 System.Diagnostics.Process.Start(
184 |                     new System.Diagnostics.ProcessStartInfo
185 |                     {
186 |                         FileName = displayPath,
187 |                         UseShellExecute = true,
188 |                     }
189 |                 );
190 |             }
191 | 
192 |             if (pathCopied)
193 |             {
194 |                 GUIStyle feedbackStyle = new(EditorStyles.label);
195 |                 feedbackStyle.normal.textColor = Color.green;
196 |                 EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60));
197 |             }
198 | 
199 |             EditorGUILayout.EndHorizontal();
200 |             EditorGUILayout.EndVertical();
201 |             EditorGUILayout.Space(10);
202 | 
203 |             EditorGUILayout.LabelField(
204 |                 "4. Add this configuration to your mcp.json:",
205 |                 EditorStyles.boldLabel
206 |             );
207 | 
208 |             // JSON section with improved styling
209 |             EditorGUILayout.BeginVertical(EditorStyles.helpBox);
210 | 
211 |             // Improved text area for JSON with syntax highlighting colors
212 |             GUIStyle jsonStyle = new(EditorStyles.textArea)
213 |             {
214 |                 font = EditorStyles.boldFont,
215 |                 wordWrap = true,
216 |             };
217 |             jsonStyle.normal.textColor = new Color(0.3f, 0.6f, 0.9f); // Syntax highlighting blue
218 | 
219 |             // Draw the JSON in a text area with a taller height for better readability
220 |             EditorGUILayout.TextArea(configJson, jsonStyle, GUILayout.Height(200));
221 | 
222 |             // Copy JSON button with improved styling
223 |             EditorGUILayout.BeginHorizontal();
224 |             GUILayout.FlexibleSpace();
225 | 
226 |             if (
227 |                 GUILayout.Button(
228 |                     "Copy JSON",
229 |                     copyButtonStyle,
230 |                     GUILayout.Height(25),
231 |                     GUILayout.Width(100)
232 |                 )
233 |             )
234 |             {
235 |                 EditorGUIUtility.systemCopyBuffer = configJson;
236 |                 jsonCopied = true;
237 |                 copyFeedbackTimer = 2f;
238 |             }
239 | 
240 |             if (jsonCopied)
241 |             {
242 |                 GUIStyle feedbackStyle = new(EditorStyles.label);
243 |                 feedbackStyle.normal.textColor = Color.green;
244 |                 EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60));
245 |             }
246 | 
247 |             EditorGUILayout.EndHorizontal();
248 |             EditorGUILayout.EndVertical();
249 | 
250 |             EditorGUILayout.Space(10);
251 |             EditorGUILayout.LabelField(
252 |                 "5. After configuration:",
253 |                 EditorStyles.boldLabel
254 |             );
255 |             EditorGUILayout.LabelField(
256 |                 "• Restart VSCode",
257 |                 instructionStyle
258 |             );
259 |             EditorGUILayout.LabelField(
260 |                 "• GitHub Copilot will now be able to interact with your Unity project through the MCP protocol",
261 |                 instructionStyle
262 |             );
263 |             EditorGUILayout.LabelField(
264 |                 "• Remember to have the MCP for Unity Bridge running in Unity Editor",
265 |                 instructionStyle
266 |             );
267 | 
268 |             EditorGUILayout.EndVertical();
269 | 
270 |             EditorGUILayout.Space(10);
271 | 
272 |             // Close button at the bottom
273 |             EditorGUILayout.BeginHorizontal();
274 |             GUILayout.FlexibleSpace();
275 |             if (GUILayout.Button("Close", GUILayout.Height(30), GUILayout.Width(100)))
276 |             {
277 |                 Close();
278 |             }
279 |             GUILayout.FlexibleSpace();
280 |             EditorGUILayout.EndHorizontal();
281 | 
282 |             EditorGUILayout.EndScrollView();
283 |         }
284 | 
285 |         protected override void Update()
286 |         {
287 |             // Call the base implementation which handles the copy feedback timer
288 |             base.Update();
289 |         }
290 |     }
291 | }
292 | 
```

--------------------------------------------------------------------------------
/docs/README-DEV.md:
--------------------------------------------------------------------------------

```markdown
  1 | # MCP for Unity Development Tools
  2 | 
  3 | | [English](README-DEV.md) | [简体中文](README-DEV-zh.md) |
  4 | |---------------------------|------------------------------|
  5 | 
  6 | Welcome to the MCP for Unity development environment! This directory contains tools and utilities to streamline MCP for Unity core development.
  7 | 
  8 | ## 🚀 Available Development Features
  9 | 
 10 | ### ✅ Development Deployment Scripts
 11 | Quick deployment and testing tools for MCP for Unity core changes.
 12 | 
 13 | ### 🔄 Coming Soon
 14 | - **Development Mode Toggle**: Built-in Unity editor development features
 15 | - **Hot Reload System**: Real-time code updates without Unity restarts  
 16 | - **Plugin Development Kit**: Tools for creating custom MCP for Unity extensions
 17 | - **Automated Testing Suite**: Comprehensive testing framework for contributions
 18 | - **Debug Dashboard**: Advanced debugging and monitoring tools
 19 | 
 20 | ---
 21 | 
 22 | ## Switching MCP package sources quickly
 23 | 
 24 | Run this from the unity-mcp repo, not your game's root directory. Use `mcp_source.py` to quickly switch between different MCP for Unity package sources:
 25 | 
 26 | **Usage:**
 27 | ```bash
 28 | python mcp_source.py [--manifest /path/to/manifest.json] [--repo /path/to/unity-mcp] [--choice 1|2|3]
 29 | ```
 30 | 
 31 | **Options:**
 32 | - **1** Upstream main (CoplayDev/unity-mcp)
 33 | - **2** Remote current branch (origin + branch)
 34 | - **3** Local workspace (file: MCPForUnity)
 35 | 
 36 | After switching, open Package Manager and Refresh to re-resolve packages.
 37 | 
 38 | ## Development Deployment Scripts
 39 | 
 40 | These deployment scripts help you quickly test changes to MCP for Unity core code.
 41 | 
 42 | ## Scripts
 43 | 
 44 | ### `deploy-dev.bat`
 45 | Deploys your development code to the actual installation locations for testing.
 46 | 
 47 | **What it does:**
 48 | 1. Backs up original files to a timestamped folder
 49 | 2. Copies Unity Bridge code to Unity's package cache
 50 | 3. Copies Python Server code to the MCP installation folder
 51 | 
 52 | **Usage:**
 53 | 1. Run `deploy-dev.bat`
 54 | 2. Enter Unity package cache path (example provided)
 55 | 3. Enter server path (or use default: `%LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer\src`)
 56 | 4. Enter backup location (or use default: `%USERPROFILE%\Desktop\unity-mcp-backup`)
 57 | 
 58 | **Note:** Dev deploy skips `.venv`, `__pycache__`, `.pytest_cache`, `.mypy_cache`, `.git`; reduces churn and avoids copying virtualenvs.
 59 | 
 60 | ### `restore-dev.bat`
 61 | Restores original files from backup.
 62 | 
 63 | **What it does:**
 64 | 1. Lists available backups with timestamps
 65 | 2. Allows you to select which backup to restore
 66 | 3. Restores both Unity Bridge and Python Server files
 67 | 
 68 | ### `prune_tool_results.py`
 69 | Compacts large `tool_result` blobs in conversation JSON into concise one-line summaries.
 70 | 
 71 | **Usage:**
 72 | ```bash
 73 | python3 prune_tool_results.py < reports/claude-execution-output.json > reports/claude-execution-output.pruned.json
 74 | ```
 75 | 
 76 | The script reads a conversation from `stdin` and writes the pruned version to `stdout`, making logs much easier to inspect or archive.
 77 | 
 78 | These defaults dramatically cut token usage without affecting essential information.
 79 | 
 80 | ## Finding Unity Package Cache Path
 81 | 
 82 | Unity stores Git packages under a version-or-hash folder. Expect something like:
 83 | ```
 84 | X:\UnityProject\Library\PackageCache\com.coplaydev.unity-mcp@<version-or-hash>
 85 | ```
 86 | Example (hash):
 87 | ```
 88 | X:\UnityProject\Library\PackageCache\com.coplaydev.unity-mcp@272123cfd97e
 89 | 
 90 | ```
 91 | 
 92 | To find it reliably:
 93 | 1. Open Unity Package Manager
 94 | 2. Select "MCP for Unity" package
 95 | 3. Right click the package and choose "Show in Explorer"
 96 | 4. That opens the exact cache folder Unity is using for your project
 97 | 
 98 | Note: In recent builds, the Python server sources are also bundled inside the package under `UnityMcpServer~/src`. This is handy for local testing or pointing MCP clients directly at the packaged server.
 99 | 
100 | ## MCP Bridge Stress Test
101 | 
102 | An on-demand stress utility exercises the MCP bridge with multiple concurrent clients while triggering real script reloads via immediate script edits (no menu calls required).
103 | 
104 | ### Script
105 | - `tools/stress_mcp.py`
106 | 
107 | ### What it does
108 | - Starts N TCP clients against the MCP for Unity bridge (default port auto-discovered from `~/.unity-mcp/unity-mcp-status-*.json`).
109 | - Sends lightweight framed `ping` keepalives to maintain concurrency.
110 | - In parallel, appends a unique marker comment to a target C# file using `manage_script.apply_text_edits` with:
111 |   - `options.refresh = "immediate"` to force an import/compile immediately (triggers domain reload), and
112 |   - `precondition_sha256` computed from the current file contents to avoid drift.
113 | - Uses EOF insertion to avoid header/`using`-guard edits.
114 | 
115 | ### Usage (local)
116 | ```bash
117 | # Recommended: use the included large script in the test project
118 | python3 tools/stress_mcp.py \
119 |   --duration 60 \
120 |   --clients 8 \
121 |   --unity-file "TestProjects/UnityMCPTests/Assets/Scripts/LongUnityScriptClaudeTest.cs"
122 | ```
123 | 
124 | Flags:
125 | - `--project` Unity project path (auto-detected to the included test project by default)
126 | - `--unity-file` C# file to edit (defaults to the long test script)
127 | - `--clients` number of concurrent clients (default 10)
128 | - `--duration` seconds to run (default 60)
129 | 
130 | ### Expected outcome
131 | - No Unity Editor crashes during reload churn
132 | - Immediate reloads after each applied edit (no `Assets/Refresh` menu calls)
133 | - Some transient disconnects or a few failed calls may occur during domain reload; the tool retries and continues
134 | - JSON summary printed at the end, e.g.:
135 |   - `{"port": 6400, "stats": {"pings": 28566, "applies": 69, "disconnects": 0, "errors": 0}}`
136 | 
137 | ### Notes and troubleshooting
138 | - Immediate vs debounced:
139 |   - The tool sets `options.refresh = "immediate"` so changes compile instantly. If you only need churn (not per-edit confirmation), switch to debounced to reduce mid-reload failures.
140 | - Precondition required:
141 |   - `apply_text_edits` requires `precondition_sha256` on larger files. The tool reads the file first to compute the SHA.
142 | - Edit location:
143 |   - To avoid header guards or complex ranges, the tool appends a one-line marker at EOF each cycle.
144 | - Read API:
145 |   - The bridge currently supports `manage_script.read` for file reads. You may see a deprecation warning; it's harmless for this internal tool.
146 | - Transient failures:
147 |   - Occasional `apply_errors` often indicate the connection reloaded mid-reply. Edits still typically apply; the loop continues on the next iteration.
148 | 
149 | ### CI guidance
150 | - Keep this out of default PR CI due to Unity/editor requirements and runtime variability.
151 | - Optionally run it as a manual workflow or nightly job on a Unity-capable runner.
152 | 
153 | ## CI Test Workflow (GitHub Actions)
154 | 
155 | We provide a CI job to run a Natural Language Editing suite against the Unity test project. It spins up a headless Unity container and connects via the MCP bridge. To run from your fork, you need the following GitHub "secrets": an `ANTHROPIC_API_KEY` and Unity credentials (usually `UNITY_EMAIL` + `UNITY_PASSWORD` or `UNITY_LICENSE` / `UNITY_SERIAL`.) These are redacted in logs so never visible.
156 | 
157 | ***To run it***
158 |  - Trigger: In GitHun "Actions" for the repo, trigger `workflow dispatch` (`Claude NL/T Full Suite (Unity live)`).
159 |  - Image: `UNITY_IMAGE` (UnityCI) pulled by tag; the job resolves a digest at runtime. Logs are sanitized.
160 |  - Execution: single pass with immediate per‑test fragment emissions (strict single `<testcase>` per file). A placeholder guard fails fast if any fragment is a bare ID. Staging (`reports/_staging`) is promoted to `reports/` to reduce partial writes.
161 |  - Reports: JUnit at `reports/junit-nl-suite.xml`, Markdown at `reports/junit-nl-suite.md`.
162 |  - Publishing: JUnit is normalized to `reports/junit-for-actions.xml` and published; artifacts upload all files under `reports/`.
163 | 
164 | ### Test target script
165 | - The repo includes a long, standalone C# script used to exercise larger edits and windows:
166 |   - `TestProjects/UnityMCPTests/Assets/Scripts/LongUnityScriptClaudeTest.cs`
167 |   Use this file locally and in CI to validate multi-edit batches, anchor inserts, and windowed reads on a sizable script.
168 | 
169 | ### Adjust tests / prompts
170 | - Edit `.claude/prompts/nl-unity-suite-t.md` to modify the NL/T steps. Follow the conventions: emit one XML fragment per test under `reports/<TESTID>_results.xml`, each containing exactly one `<testcase>` with a `name` that begins with the test ID. No prologue/epilogue or code fences.
171 | - Keep edits minimal and reversible; include concise evidence.
172 | 
173 | ### Run the suite
174 | 1) Push your branch, then manually run the workflow from the Actions tab.
175 | 2) The job writes reports into `reports/` and uploads artifacts.
176 | 3) The “JUnit Test Report” check summarizes results; open the Job Summary for full markdown.
177 | 
178 | ### View results
179 | - Job Summary: inline markdown summary of the run on the Actions tab in GitHub
180 | - Check: “JUnit Test Report” on the PR/commit.
181 | - Artifacts: `claude-nl-suite-artifacts` includes XML and MD.
182 | 
183 | ### MCP Connection Debugging
184 | - *Enable debug logs* in the MCP for Unity window (inside the Editor) to view connection status, auto-setup results, and MCP client paths. It shows:
185 |   - bridge startup/port, client connections, strict framing negotiation, and parsed frames
186 |   - auto-config path detection (Windows/macOS/Linux), uv/claude resolution, and surfaced errors
187 | - In CI, the job tails Unity logs (redacted for serial/license/password/token) and prints socket/status JSON diagnostics if startup fails.
188 | ## Workflow
189 | 
190 | 1. **Make changes** to your source code in this directory
191 | 2. **Deploy** using `deploy-dev.bat`
192 | 3. **Test** in Unity (restart Unity Editor first)
193 | 4. **Iterate** - repeat steps 1-3 as needed
194 | 5. **Restore** original files when done using `restore-dev.bat`
195 | 
196 | ## Troubleshooting
197 | 
198 | ### "Path not found" errors running the .bat file
199 | - Verify Unity package cache path is correct
200 | - Check that MCP for Unity package is actually installed
201 | - Ensure server is installed via MCP client
202 | 
203 | ### "Permission denied" errors
204 | - Run cmd as Administrator
205 | - Close Unity Editor before deploying
206 | - Close any MCP clients before deploying
207 | 
208 | ### "Backup not found" errors
209 | - Run `deploy-dev.bat` first to create initial backup
210 | - Check backup directory permissions
211 | - Verify backup directory path is correct
212 | 
213 | ### Windows uv path issues
214 | - On Windows, when testing GUI clients, prefer the WinGet Links `uv.exe`; if multiple `uv.exe` exist, use "Choose `uv` Install Location" to pin the Links shim.
```

--------------------------------------------------------------------------------
/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.IO;
  3 | using MCPForUnity.Editor.Helpers;
  4 | using Newtonsoft.Json.Linq;
  5 | using UnityEditor;
  6 | using UnityEditor.SceneManagement;
  7 | using UnityEngine;
  8 | using UnityEngine.SceneManagement;
  9 | 
 10 | namespace MCPForUnity.Editor.Tools.Prefabs
 11 | {
 12 |     [McpForUnityTool("manage_prefabs")]
 13 |     public static class ManagePrefabs
 14 |     {
 15 |         private const string SupportedActions = "open_stage, close_stage, save_open_stage, create_from_gameobject";
 16 | 
 17 |         public static object HandleCommand(JObject @params)
 18 |         {
 19 |             if (@params == null)
 20 |             {
 21 |                 return Response.Error("Parameters cannot be null.");
 22 |             }
 23 | 
 24 |             string action = @params["action"]?.ToString()?.ToLowerInvariant();
 25 |             if (string.IsNullOrEmpty(action))
 26 |             {
 27 |                 return Response.Error($"Action parameter is required. Valid actions are: {SupportedActions}.");
 28 |             }
 29 | 
 30 |             try
 31 |             {
 32 |                 switch (action)
 33 |                 {
 34 |                     case "open_stage":
 35 |                         return OpenStage(@params);
 36 |                     case "close_stage":
 37 |                         return CloseStage(@params);
 38 |                     case "save_open_stage":
 39 |                         return SaveOpenStage();
 40 |                     case "create_from_gameobject":
 41 |                         return CreatePrefabFromGameObject(@params);
 42 |                     default:
 43 |                         return Response.Error($"Unknown action: '{action}'. Valid actions are: {SupportedActions}.");
 44 |                 }
 45 |             }
 46 |             catch (Exception e)
 47 |             {
 48 |                 McpLog.Error($"[ManagePrefabs] Action '{action}' failed: {e}");
 49 |                 return Response.Error($"Internal error: {e.Message}");
 50 |             }
 51 |         }
 52 | 
 53 |         private static object OpenStage(JObject @params)
 54 |         {
 55 |             string prefabPath = @params["prefabPath"]?.ToString();
 56 |             if (string.IsNullOrEmpty(prefabPath))
 57 |             {
 58 |                 return Response.Error("'prefabPath' parameter is required for open_stage.");
 59 |             }
 60 | 
 61 |             string sanitizedPath = AssetPathUtility.SanitizeAssetPath(prefabPath);
 62 |             GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(sanitizedPath);
 63 |             if (prefabAsset == null)
 64 |             {
 65 |                 return Response.Error($"No prefab asset found at path '{sanitizedPath}'.");
 66 |             }
 67 | 
 68 |             string modeValue = @params["mode"]?.ToString();
 69 |             if (!string.IsNullOrEmpty(modeValue) && !modeValue.Equals(PrefabStage.Mode.InIsolation.ToString(), StringComparison.OrdinalIgnoreCase))
 70 |             {
 71 |                 return Response.Error("Only PrefabStage mode 'InIsolation' is supported at this time.");
 72 |             }
 73 | 
 74 |             PrefabStage stage = PrefabStageUtility.OpenPrefab(sanitizedPath);
 75 |             if (stage == null)
 76 |             {
 77 |                 return Response.Error($"Failed to open prefab stage for '{sanitizedPath}'.");
 78 |             }
 79 | 
 80 |             return Response.Success($"Opened prefab stage for '{sanitizedPath}'.", SerializeStage(stage));
 81 |         }
 82 | 
 83 |         private static object CloseStage(JObject @params)
 84 |         {
 85 |             PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
 86 |             if (stage == null)
 87 |             {
 88 |                 return Response.Success("No prefab stage was open.");
 89 |             }
 90 | 
 91 |             bool saveBeforeClose = @params["saveBeforeClose"]?.ToObject<bool>() ?? false;
 92 |             if (saveBeforeClose && stage.scene.isDirty)
 93 |             {
 94 |                 SaveStagePrefab(stage);
 95 |                 AssetDatabase.SaveAssets();
 96 |             }
 97 | 
 98 |             StageUtility.GoToMainStage();
 99 |             return Response.Success($"Closed prefab stage for '{stage.assetPath}'.");
100 |         }
101 | 
102 |         private static object SaveOpenStage()
103 |         {
104 |             PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
105 |             if (stage == null)
106 |             {
107 |                 return Response.Error("No prefab stage is currently open.");
108 |             }
109 | 
110 |             SaveStagePrefab(stage);
111 |             AssetDatabase.SaveAssets();
112 |             return Response.Success($"Saved prefab stage for '{stage.assetPath}'.", SerializeStage(stage));
113 |         }
114 | 
115 |         private static void SaveStagePrefab(PrefabStage stage)
116 |         {
117 |             if (stage?.prefabContentsRoot == null)
118 |             {
119 |                 throw new InvalidOperationException("Cannot save prefab stage without a prefab root.");
120 |             }
121 | 
122 |             bool saved = PrefabUtility.SaveAsPrefabAsset(stage.prefabContentsRoot, stage.assetPath);
123 |             if (!saved)
124 |             {
125 |                 throw new InvalidOperationException($"Failed to save prefab asset at '{stage.assetPath}'.");
126 |             }
127 |         }
128 | 
129 |         private static object CreatePrefabFromGameObject(JObject @params)
130 |         {
131 |             string targetName = @params["target"]?.ToString() ?? @params["name"]?.ToString();
132 |             if (string.IsNullOrEmpty(targetName))
133 |             {
134 |                 return Response.Error("'target' parameter is required for create_from_gameobject.");
135 |             }
136 | 
137 |             bool includeInactive = @params["searchInactive"]?.ToObject<bool>() ?? false;
138 |             GameObject sourceObject = FindSceneObjectByName(targetName, includeInactive);
139 |             if (sourceObject == null)
140 |             {
141 |                 return Response.Error($"GameObject '{targetName}' not found in the active scene.");
142 |             }
143 | 
144 |             if (PrefabUtility.IsPartOfPrefabAsset(sourceObject))
145 |             {
146 |                 return Response.Error(
147 |                     $"GameObject '{sourceObject.name}' is part of a prefab asset. Open the prefab stage to save changes instead."
148 |                 );
149 |             }
150 | 
151 |             PrefabInstanceStatus status = PrefabUtility.GetPrefabInstanceStatus(sourceObject);
152 |             if (status != PrefabInstanceStatus.NotAPrefab)
153 |             {
154 |                 return Response.Error(
155 |                     $"GameObject '{sourceObject.name}' is already linked to an existing prefab instance."
156 |                 );
157 |             }
158 | 
159 |             string requestedPath = @params["prefabPath"]?.ToString();
160 |             if (string.IsNullOrWhiteSpace(requestedPath))
161 |             {
162 |                 return Response.Error("'prefabPath' parameter is required for create_from_gameobject.");
163 |             }
164 | 
165 |             string sanitizedPath = AssetPathUtility.SanitizeAssetPath(requestedPath);
166 |             if (!sanitizedPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
167 |             {
168 |                 sanitizedPath += ".prefab";
169 |             }
170 | 
171 |             bool allowOverwrite = @params["allowOverwrite"]?.ToObject<bool>() ?? false;
172 |             string finalPath = sanitizedPath;
173 | 
174 |             if (!allowOverwrite && AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(finalPath) != null)
175 |             {
176 |                 finalPath = AssetDatabase.GenerateUniqueAssetPath(finalPath);
177 |             }
178 | 
179 |             EnsureAssetDirectoryExists(finalPath);
180 | 
181 |             try
182 |             {
183 |                 GameObject connectedInstance = PrefabUtility.SaveAsPrefabAssetAndConnect(
184 |                     sourceObject,
185 |                     finalPath,
186 |                     InteractionMode.AutomatedAction
187 |                 );
188 | 
189 |                 if (connectedInstance == null)
190 |                 {
191 |                     return Response.Error($"Failed to save prefab asset at '{finalPath}'.");
192 |                 }
193 | 
194 |                 Selection.activeGameObject = connectedInstance;
195 | 
196 |                 return Response.Success(
197 |                     $"Prefab created at '{finalPath}' and instance linked.",
198 |                     new
199 |                     {
200 |                         prefabPath = finalPath,
201 |                         instanceId = connectedInstance.GetInstanceID()
202 |                     }
203 |                 );
204 |             }
205 |             catch (Exception e)
206 |             {
207 |                 return Response.Error($"Error saving prefab asset at '{finalPath}': {e.Message}");
208 |             }
209 |         }
210 | 
211 |         private static void EnsureAssetDirectoryExists(string assetPath)
212 |         {
213 |             string directory = Path.GetDirectoryName(assetPath);
214 |             if (string.IsNullOrEmpty(directory))
215 |             {
216 |                 return;
217 |             }
218 | 
219 |             string fullDirectory = Path.Combine(Directory.GetCurrentDirectory(), directory);
220 |             if (!Directory.Exists(fullDirectory))
221 |             {
222 |                 Directory.CreateDirectory(fullDirectory);
223 |                 AssetDatabase.Refresh();
224 |             }
225 |         }
226 | 
227 |         private static GameObject FindSceneObjectByName(string name, bool includeInactive)
228 |         {
229 |             PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
230 |             if (stage?.prefabContentsRoot != null)
231 |             {
232 |                 foreach (Transform transform in stage.prefabContentsRoot.GetComponentsInChildren<Transform>(includeInactive))
233 |                 {
234 |                     if (transform.name == name)
235 |                     {
236 |                         return transform.gameObject;
237 |                     }
238 |                 }
239 |             }
240 | 
241 |             Scene activeScene = SceneManager.GetActiveScene();
242 |             foreach (GameObject root in activeScene.GetRootGameObjects())
243 |             {
244 |                 foreach (Transform transform in root.GetComponentsInChildren<Transform>(includeInactive))
245 |                 {
246 |                     GameObject candidate = transform.gameObject;
247 |                     if (candidate.name == name)
248 |                     {
249 |                         return candidate;
250 |                     }
251 |                 }
252 |             }
253 | 
254 |             return null;
255 |         }
256 | 
257 |         private static object SerializeStage(PrefabStage stage)
258 |         {
259 |             if (stage == null)
260 |             {
261 |                 return new { isOpen = false };
262 |             }
263 | 
264 |             return new
265 |             {
266 |                 isOpen = true,
267 |                 assetPath = stage.assetPath,
268 |                 prefabRootName = stage.prefabContentsRoot != null ? stage.prefabContentsRoot.name : null,
269 |                 mode = stage.mode.ToString(),
270 |                 isDirty = stage.scene.isDirty
271 |             };
272 |         }
273 | 
274 |     }
275 | }
276 | 
```

--------------------------------------------------------------------------------
/UnityMcpBridge/Editor/Tools/Prefabs/ManagePrefabs.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.IO;
  3 | using MCPForUnity.Editor.Helpers;
  4 | using Newtonsoft.Json.Linq;
  5 | using UnityEditor;
  6 | using UnityEditor.SceneManagement;
  7 | using UnityEngine;
  8 | using UnityEngine.SceneManagement;
  9 | 
 10 | namespace MCPForUnity.Editor.Tools.Prefabs
 11 | {
 12 |     [McpForUnityTool("manage_prefabs")]
 13 |     public static class ManagePrefabs
 14 |     {
 15 |         private const string SupportedActions = "open_stage, close_stage, save_open_stage, create_from_gameobject";
 16 | 
 17 |         public static object HandleCommand(JObject @params)
 18 |         {
 19 |             if (@params == null)
 20 |             {
 21 |                 return Response.Error("Parameters cannot be null.");
 22 |             }
 23 | 
 24 |             string action = @params["action"]?.ToString()?.ToLowerInvariant();
 25 |             if (string.IsNullOrEmpty(action))
 26 |             {
 27 |                 return Response.Error($"Action parameter is required. Valid actions are: {SupportedActions}.");
 28 |             }
 29 | 
 30 |             try
 31 |             {
 32 |                 switch (action)
 33 |                 {
 34 |                     case "open_stage":
 35 |                         return OpenStage(@params);
 36 |                     case "close_stage":
 37 |                         return CloseStage(@params);
 38 |                     case "save_open_stage":
 39 |                         return SaveOpenStage();
 40 |                     case "create_from_gameobject":
 41 |                         return CreatePrefabFromGameObject(@params);
 42 |                     default:
 43 |                         return Response.Error($"Unknown action: '{action}'. Valid actions are: {SupportedActions}.");
 44 |                 }
 45 |             }
 46 |             catch (Exception e)
 47 |             {
 48 |                 McpLog.Error($"[ManagePrefabs] Action '{action}' failed: {e}");
 49 |                 return Response.Error($"Internal error: {e.Message}");
 50 |             }
 51 |         }
 52 | 
 53 |         private static object OpenStage(JObject @params)
 54 |         {
 55 |             string prefabPath = @params["prefabPath"]?.ToString();
 56 |             if (string.IsNullOrEmpty(prefabPath))
 57 |             {
 58 |                 return Response.Error("'prefabPath' parameter is required for open_stage.");
 59 |             }
 60 | 
 61 |             string sanitizedPath = AssetPathUtility.SanitizeAssetPath(prefabPath);
 62 |             GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(sanitizedPath);
 63 |             if (prefabAsset == null)
 64 |             {
 65 |                 return Response.Error($"No prefab asset found at path '{sanitizedPath}'.");
 66 |             }
 67 | 
 68 |             string modeValue = @params["mode"]?.ToString();
 69 |             if (!string.IsNullOrEmpty(modeValue) && !modeValue.Equals(PrefabStage.Mode.InIsolation.ToString(), StringComparison.OrdinalIgnoreCase))
 70 |             {
 71 |                 return Response.Error("Only PrefabStage mode 'InIsolation' is supported at this time.");
 72 |             }
 73 | 
 74 |             PrefabStage stage = PrefabStageUtility.OpenPrefab(sanitizedPath);
 75 |             if (stage == null)
 76 |             {
 77 |                 return Response.Error($"Failed to open prefab stage for '{sanitizedPath}'.");
 78 |             }
 79 | 
 80 |             return Response.Success($"Opened prefab stage for '{sanitizedPath}'.", SerializeStage(stage));
 81 |         }
 82 | 
 83 |         private static object CloseStage(JObject @params)
 84 |         {
 85 |             PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
 86 |             if (stage == null)
 87 |             {
 88 |                 return Response.Success("No prefab stage was open.");
 89 |             }
 90 | 
 91 |             bool saveBeforeClose = @params["saveBeforeClose"]?.ToObject<bool>() ?? false;
 92 |             if (saveBeforeClose && stage.scene.isDirty)
 93 |             {
 94 |                 SaveStagePrefab(stage);
 95 |                 AssetDatabase.SaveAssets();
 96 |             }
 97 | 
 98 |             StageUtility.GoToMainStage();
 99 |             return Response.Success($"Closed prefab stage for '{stage.assetPath}'.");
100 |         }
101 | 
102 |         private static object SaveOpenStage()
103 |         {
104 |             PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
105 |             if (stage == null)
106 |             {
107 |                 return Response.Error("No prefab stage is currently open.");
108 |             }
109 | 
110 |             SaveStagePrefab(stage);
111 |             AssetDatabase.SaveAssets();
112 |             return Response.Success($"Saved prefab stage for '{stage.assetPath}'.", SerializeStage(stage));
113 |         }
114 | 
115 |         private static void SaveStagePrefab(PrefabStage stage)
116 |         {
117 |             if (stage?.prefabContentsRoot == null)
118 |             {
119 |                 throw new InvalidOperationException("Cannot save prefab stage without a prefab root.");
120 |             }
121 | 
122 |             bool saved = PrefabUtility.SaveAsPrefabAsset(stage.prefabContentsRoot, stage.assetPath);
123 |             if (!saved)
124 |             {
125 |                 throw new InvalidOperationException($"Failed to save prefab asset at '{stage.assetPath}'.");
126 |             }
127 |         }
128 | 
129 |         private static object CreatePrefabFromGameObject(JObject @params)
130 |         {
131 |             string targetName = @params["target"]?.ToString() ?? @params["name"]?.ToString();
132 |             if (string.IsNullOrEmpty(targetName))
133 |             {
134 |                 return Response.Error("'target' parameter is required for create_from_gameobject.");
135 |             }
136 | 
137 |             bool includeInactive = @params["searchInactive"]?.ToObject<bool>() ?? false;
138 |             GameObject sourceObject = FindSceneObjectByName(targetName, includeInactive);
139 |             if (sourceObject == null)
140 |             {
141 |                 return Response.Error($"GameObject '{targetName}' not found in the active scene.");
142 |             }
143 | 
144 |             if (PrefabUtility.IsPartOfPrefabAsset(sourceObject))
145 |             {
146 |                 return Response.Error(
147 |                     $"GameObject '{sourceObject.name}' is part of a prefab asset. Open the prefab stage to save changes instead."
148 |                 );
149 |             }
150 | 
151 |             PrefabInstanceStatus status = PrefabUtility.GetPrefabInstanceStatus(sourceObject);
152 |             if (status != PrefabInstanceStatus.NotAPrefab)
153 |             {
154 |                 return Response.Error(
155 |                     $"GameObject '{sourceObject.name}' is already linked to an existing prefab instance."
156 |                 );
157 |             }
158 | 
159 |             string requestedPath = @params["prefabPath"]?.ToString();
160 |             if (string.IsNullOrWhiteSpace(requestedPath))
161 |             {
162 |                 return Response.Error("'prefabPath' parameter is required for create_from_gameobject.");
163 |             }
164 | 
165 |             string sanitizedPath = AssetPathUtility.SanitizeAssetPath(requestedPath);
166 |             if (!sanitizedPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
167 |             {
168 |                 sanitizedPath += ".prefab";
169 |             }
170 | 
171 |             bool allowOverwrite = @params["allowOverwrite"]?.ToObject<bool>() ?? false;
172 |             string finalPath = sanitizedPath;
173 | 
174 |             if (!allowOverwrite && AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(finalPath) != null)
175 |             {
176 |                 finalPath = AssetDatabase.GenerateUniqueAssetPath(finalPath);
177 |             }
178 | 
179 |             EnsureAssetDirectoryExists(finalPath);
180 | 
181 |             try
182 |             {
183 |                 GameObject connectedInstance = PrefabUtility.SaveAsPrefabAssetAndConnect(
184 |                     sourceObject,
185 |                     finalPath,
186 |                     InteractionMode.AutomatedAction
187 |                 );
188 | 
189 |                 if (connectedInstance == null)
190 |                 {
191 |                     return Response.Error($"Failed to save prefab asset at '{finalPath}'.");
192 |                 }
193 | 
194 |                 Selection.activeGameObject = connectedInstance;
195 | 
196 |                 return Response.Success(
197 |                     $"Prefab created at '{finalPath}' and instance linked.",
198 |                     new
199 |                     {
200 |                         prefabPath = finalPath,
201 |                         instanceId = connectedInstance.GetInstanceID()
202 |                     }
203 |                 );
204 |             }
205 |             catch (Exception e)
206 |             {
207 |                 return Response.Error($"Error saving prefab asset at '{finalPath}': {e.Message}");
208 |             }
209 |         }
210 | 
211 |         private static void EnsureAssetDirectoryExists(string assetPath)
212 |         {
213 |             string directory = Path.GetDirectoryName(assetPath);
214 |             if (string.IsNullOrEmpty(directory))
215 |             {
216 |                 return;
217 |             }
218 | 
219 |             string fullDirectory = Path.Combine(Directory.GetCurrentDirectory(), directory);
220 |             if (!Directory.Exists(fullDirectory))
221 |             {
222 |                 Directory.CreateDirectory(fullDirectory);
223 |                 AssetDatabase.Refresh();
224 |             }
225 |         }
226 | 
227 |         private static GameObject FindSceneObjectByName(string name, bool includeInactive)
228 |         {
229 |             PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
230 |             if (stage?.prefabContentsRoot != null)
231 |             {
232 |                 foreach (Transform transform in stage.prefabContentsRoot.GetComponentsInChildren<Transform>(includeInactive))
233 |                 {
234 |                     if (transform.name == name)
235 |                     {
236 |                         return transform.gameObject;
237 |                     }
238 |                 }
239 |             }
240 | 
241 |             Scene activeScene = SceneManager.GetActiveScene();
242 |             foreach (GameObject root in activeScene.GetRootGameObjects())
243 |             {
244 |                 foreach (Transform transform in root.GetComponentsInChildren<Transform>(includeInactive))
245 |                 {
246 |                     GameObject candidate = transform.gameObject;
247 |                     if (candidate.name == name)
248 |                     {
249 |                         return candidate;
250 |                     }
251 |                 }
252 |             }
253 | 
254 |             return null;
255 |         }
256 | 
257 |         private static object SerializeStage(PrefabStage stage)
258 |         {
259 |             if (stage == null)
260 |             {
261 |                 return new { isOpen = false };
262 |             }
263 | 
264 |             return new
265 |             {
266 |                 isOpen = true,
267 |                 assetPath = stage.assetPath,
268 |                 prefabRootName = stage.prefabContentsRoot != null ? stage.prefabContentsRoot.name : null,
269 |                 mode = stage.mode.ToString(),
270 |                 isDirty = stage.scene.isDirty
271 |             };
272 |         }
273 | 
274 |     }
275 | }
276 | 
```

--------------------------------------------------------------------------------
/docs/v6_NEW_UI_CHANGES.md:
--------------------------------------------------------------------------------

```markdown
  1 | # MCP for Unity v6 - New Editor Window
  2 | 
  3 | > **UI Toolkit-based window with service-oriented architecture**
  4 | 
  5 | ![New MCP Editor Window Dark](./screenshots/v6_new_ui_dark.png)
  6 | *Dark theme*
  7 | 
  8 | ![New MCP Editor Window Light](./screenshots/v6_new_ui_light.png)
  9 | *Light theme*
 10 | 
 11 | ---
 12 | 
 13 | ## Overview
 14 | 
 15 | The new MCP Editor Window is a complete rebuild using **UI Toolkit (UXML/USS)** with a **service-oriented architecture**. The design philosophy emphasizes **explicit over implicit** behavior, making the system more predictable, testable, and maintainable.
 16 | 
 17 | **Quick Access:** `Cmd/Ctrl+Shift+M` or `Window > MCP For Unity > Open MCP Window`
 18 | 
 19 | **Key Improvements:**
 20 | - 🎨 Modern UI that doesn't hide info as the window size changes
 21 | - 🏗️ Service layer separates business logic from UI
 22 | - 🔧 Explicit path overrides for troubleshooting
 23 | - 📦 Asset Store support with server download capability
 24 | - ⚡ Keyboard shortcut for quick access
 25 | 
 26 | ---
 27 | 
 28 | ## Key Differences at a Glance
 29 | 
 30 | | Feature | Old Window | New Window | Notes |
 31 | |---------|-----------|------------|-------|
 32 | | **Architecture** | Monolithic | Service-based | Better testability & reusability |
 33 | | **UI Framework** | IMGUI | UI Toolkit (UXML/USS) | Modern, responsive, themeable |
 34 | | **Auto-Setup** | ✅ Automatic | ❌ Manual | Users have explicit control |
 35 | | **Path Overrides** | ⚠️ Python only | ✅ Python + UV + Claude CLI | Advanced troubleshooting |
 36 | | **Bridge Health** | ⚠️ Hidden | ✅ Visible with test button | Separate from connection status |
 37 | | **Configure All** | ❌ None | ✅ Batch with summary | Configure all clients at once |
 38 | | **Manual Config** | ✅ Popup windows | ✅ Inline foldout | Less window clutter |
 39 | | **Server Download** | ❌ None | ✅ Asset Store support | Download server from GitHub |
 40 | | **Keyboard Shortcut** | ❌ None | ✅ Cmd/Ctrl+Shift+M | Quick access |
 41 | 
 42 | ## What's New
 43 | 
 44 | ### UI Enhancements
 45 | - **Advanced Settings Foldout** - Collapsible section for path overrides (MCP server, UV, Claude CLI)
 46 | - **Visual Path Validation** - Green/red indicators show whether override paths are valid
 47 | - **Bridge Health Indicator** - Separate from connection status, shows handshake and ping/pong results
 48 | - **Manual Connection Test Button** - Verify bridge health on demand without reconnecting
 49 | - **Inline Manual Configuration** - Copy config path and JSON without opening separate windows
 50 | 
 51 | ### Functional Improvements
 52 | - **Configure All Detected Clients** - One-click batch configuration with summary dialog
 53 | - **Keyboard Shortcut** - `Cmd/Ctrl+Shift+M` opens the window quickly
 54 | 
 55 | ### Asset Store Support
 56 | - **Server Download Button** - Asset Store users can download the server from GitHub releases
 57 | - **Dynamic UI** - Shows appropriate button based on installation type
 58 | 
 59 | ![Asset Store Version](./screenshots/v6_new_ui_asset_store_version.png)
 60 | *Asset Store version showing the "Download & Install Server" button*
 61 | 
 62 | ---
 63 | 
 64 | ## Features Not Supported (By Design)
 65 | 
 66 | The new window intentionally removes implicit behaviors and complex edge-case handling to provide a cleaner, more predictable UX.
 67 | 
 68 | ### ❌ Auto-Setup on First Run
 69 | - **Old:** Automatically configured clients on first window open
 70 | - **Why Removed:** Users should explicitly choose which clients to configure
 71 | - **Alternative:** Use "Configure All Detected Clients" button
 72 | 
 73 | ### ❌ Python Detection Warning
 74 | - **Old:** Warning banner if Python not detected on system
 75 | - **Why Removed:** Setup Wizard handles dependency checks, we also can't flood a bunch of error and warning logs when submitting to the Asset Store
 76 | - **Alternative:** Run Setup Wizard via `Window > MCP For Unity > Setup Wizard`
 77 | 
 78 | ### ❌ Separate Manual Setup Windows
 79 | - **Old:** `VSCodeManualSetupWindow`, `ManualConfigEditorWindow` popup dialogs
 80 | - **Why Removed:** Looks neater, less visual clutter
 81 | - **Alternative:** Inline "Manual Configuration" foldout with copy buttons
 82 | 
 83 | ### ❌ Server Installation Status Panel
 84 | - **Old:** Dedicated panel showing server install status with color indicators
 85 | - **Why Removed:** Simplified to focus on active configuration and the connection status, we now have a setup wizard for this
 86 | - **Alternative:** Server path override in Advanced Settings + Rebuild button
 87 | 
 88 | ---
 89 | 
 90 | ## Service Locator Architecture
 91 | 
 92 | The new window uses a **service locator pattern** to access business logic without tight coupling. This provides flexibility for testing and future dependency injection migration.
 93 | 
 94 | ### MCPServiceLocator
 95 | 
 96 | **Purpose:** Central access point for MCP services
 97 | 
 98 | **Usage:**
 99 | ```csharp
100 | // Access bridge service
101 | MCPServiceLocator.Bridge.Start();
102 | 
103 | // Access client configuration service
104 | MCPServiceLocator.Client.ConfigureAllDetectedClients();
105 | 
106 | // Access path resolver service
107 | string mcpServerPath = MCPServiceLocator.Paths.GetMcpServerPath();
108 | ```
109 | 
110 | **Benefits:**
111 | - No constructor dependencies (easy to use anywhere)
112 | - Lazy initialization (services created only when needed)
113 | - Testable (supports custom implementations via `Register()`)
114 | 
115 | ---
116 | 
117 | ### IBridgeControlService
118 | 
119 | **Purpose:** Manages MCP for Unity Bridge lifecycle and health verification
120 | 
121 | **Key Methods:**
122 | - `Start()` / `Stop()` - Bridge lifecycle management
123 | - `Verify(port)` - Health check with handshake + ping/pong validation
124 | - `IsRunning` - Current bridge status
125 | - `CurrentPort` - Active port number
126 | 
127 | **Implementation:** `BridgeControlService`
128 | 
129 | **Usage Example:**
130 | ```csharp
131 | var bridge = MCPServiceLocator.Bridge;
132 | bridge.Start();
133 | 
134 | var result = bridge.Verify(bridge.CurrentPort);
135 | if (result.Success && result.PingSucceeded)
136 | {
137 |     Debug.Log("Bridge is healthy");
138 | }
139 | ```
140 | 
141 | ---
142 | 
143 | ### IClientConfigurationService
144 | 
145 | **Purpose:** Handles MCP client configuration and registration
146 | 
147 | **Key Methods:**
148 | - `ConfigureClient(client)` - Configure a single client
149 | - `ConfigureAllDetectedClients()` - Batch configure with summary
150 | - `CheckClientStatus(client)` - Verify client status + auto-rewrite paths
151 | - `RegisterClaudeCode()` / `UnregisterClaudeCode()` - Claude Code management
152 | - `GenerateConfigJson(client)` - Get JSON for manual configuration
153 | 
154 | **Implementation:** `ClientConfigurationService`
155 | 
156 | **Usage Example:**
157 | ```csharp
158 | var clientService = MCPServiceLocator.Client;
159 | var summary = clientService.ConfigureAllDetectedClients();
160 | Debug.Log($"Configured: {summary.SuccessCount}, Failed: {summary.FailureCount}");
161 | ```
162 | 
163 | ---
164 | 
165 | ### IPathResolverService
166 | 
167 | **Purpose:** Resolves paths to required tools with override support
168 | 
169 | **Key Methods:**
170 | - `GetMcpServerPath()` - MCP server directory
171 | - `GetUvPath()` - UV executable path
172 | - `GetClaudeCliPath()` - Claude CLI path
173 | - `SetMcpServerOverride(path)` / `ClearMcpServerOverride()` - Manage MCP server overrides
174 | - `SetUvPathOverride(path)` / `ClearUvPathOverride()` - Manage UV overrides
175 | - `SetClaudeCliPathOverride(path)` / `ClearClaudeCliPathOverride()` - Manage Claude CLI overrides
176 | - `IsPythonDetected()` / `IsUvDetected()` - Detection checks
177 | 
178 | **Implementation:** `PathResolverService`
179 | 
180 | **Usage Example:**
181 | ```csharp
182 | var paths = MCPServiceLocator.Paths;
183 | 
184 | // Check if UV is detected
185 | if (!paths.IsUvDetected())
186 | {
187 |     Debug.LogWarning("UV not found");
188 | }
189 | 
190 | // Set an override
191 | paths.SetUvPathOverride("/custom/path/to/uv");
192 | ```
193 | 
194 | ## Technical Details
195 | 
196 | ### Files Created
197 | 
198 | **Services:**
199 | ```text
200 | MCPForUnity/Editor/Services/
201 | ├── IBridgeControlService.cs          # Bridge lifecycle interface
202 | ├── BridgeControlService.cs           # Bridge lifecycle implementation
203 | ├── IClientConfigurationService.cs    # Client config interface
204 | ├── ClientConfigurationService.cs     # Client config implementation
205 | ├── IPathResolverService.cs          # Path resolution interface
206 | ├── PathResolverService.cs           # Path resolution implementation
207 | └── MCPServiceLocator.cs             # Service locator pattern
208 | ```
209 | 
210 | **Helpers:**
211 | ```text
212 | MCPForUnity/Editor/Helpers/
213 | └── AssetPathUtility.cs              # Package path detection & package.json parsing
214 | ```
215 | 
216 | **UI:**
217 | ```text
218 | MCPForUnity/Editor/Windows/
219 | ├── MCPForUnityEditorWindowNew.cs    # Main window (~850 lines)
220 | ├── MCPForUnityEditorWindowNew.uxml  # UI Toolkit layout
221 | └── MCPForUnityEditorWindowNew.uss   # UI Toolkit styles
222 | ```
223 | 
224 | **CI/CD:**
225 | ```text
226 | .github/workflows/
227 | └── bump-version.yml                 # Server upload to releases
228 | ```
229 | 
230 | ### Key Files Modified
231 | 
232 | - `ServerInstaller.cs` - Added download/install logic for Asset Store
233 | - `SetupWizard.cs` - Integration with new service locator
234 | - `PackageDetector.cs` - Uses `AssetPathUtility` for version detection
235 | 
236 | ---
237 | 
238 | ## Migration Notes
239 | 
240 | ### For Users
241 | 
242 | **Immediate Changes (v6.x):**
243 | - Both old and new windows are available
244 | - New window accessible via `Cmd/Ctrl+Shift+M` or menu
245 | - Settings and overrides are shared between windows (same EditorPrefs keys)
246 | - Services can be used by both windows
247 | 
248 | **Upcoming Changes (v8.x):**
249 | - ⚠️ **Old window will be removed in v8.0**
250 | - All users will automatically use the new window
251 | - EditorPrefs keys remain the same (no migration needed)
252 | - Custom scripts using old window APIs will need updates
253 | 
254 | ### For Developers
255 | 
256 | **Using the Services:**
257 | ```csharp
258 | // Accessing services from any editor script
259 | var bridge = MCPServiceLocator.Bridge;
260 | var client = MCPServiceLocator.Client;
261 | var paths = MCPServiceLocator.Paths;
262 | 
263 | // Services are lazily initialized on first access
264 | // No need to check for null
265 | ```
266 | 
267 | **Testing with Custom Implementations:**
268 | ```csharp
269 | // In test setup
270 | var mockBridge = new MockBridgeService();
271 | MCPServiceLocator.Register(mockBridge);
272 | 
273 | // Services are now testable without Unity dependencies
274 | ```
275 | 
276 | **Reusing Service Logic:**
277 | The service layer is designed to be reused by other parts of the codebase. For example:
278 | - Build scripts can use `IClientConfigurationService` to auto-configure clients
279 | - CI/CD can use `IBridgeControlService` to verify bridge health
280 | - Tools can use `IPathResolverService` for consistent path resolution
281 | 
282 | **Notes:**
283 | - A lot of Helpers will gradually be moved to the service layer
284 | - Why not Dependency Injection? This change had a lot of changes, so we didn't want to add too much complexity to the codebase in one go
285 | 
286 | ---
287 | 
288 | ## Pull Request Reference
289 | 
290 | **PR #313:** [feat: New UI with service architecture](https://github.com/CoplayDev/unity-mcp/pull/313)
291 | 
292 | **Key Commits:**
293 | - Service layer implementation
294 | - UI Toolkit window rebuild
295 | - Asset Store server download support
296 | - CI/CD server upload automation
297 | 
298 | ---
299 | 
300 | **Last Updated:** 2025-10-10  
301 | **Unity Versions:** Unity 2021.3+ through Unity 6.x  
302 | **Architecture:** Service Locator + UI Toolkit  
303 | **Status:** Active (Old window deprecated in v8.0)
304 | 
```

--------------------------------------------------------------------------------
/MCPForUnity/Runtime/Serialization/UnityTypeConverters.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using Newtonsoft.Json;
  2 | using Newtonsoft.Json.Linq;
  3 | using System;
  4 | using UnityEngine;
  5 | #if UNITY_EDITOR
  6 | using UnityEditor; // Required for AssetDatabase and EditorUtility
  7 | #endif
  8 | 
  9 | namespace MCPForUnity.Runtime.Serialization
 10 | {
 11 |     public class Vector3Converter : JsonConverter<Vector3>
 12 |     {
 13 |         public override void WriteJson(JsonWriter writer, Vector3 value, JsonSerializer serializer)
 14 |         {
 15 |             writer.WriteStartObject();
 16 |             writer.WritePropertyName("x");
 17 |             writer.WriteValue(value.x);
 18 |             writer.WritePropertyName("y");
 19 |             writer.WriteValue(value.y);
 20 |             writer.WritePropertyName("z");
 21 |             writer.WriteValue(value.z);
 22 |             writer.WriteEndObject();
 23 |         }
 24 | 
 25 |         public override Vector3 ReadJson(JsonReader reader, Type objectType, Vector3 existingValue, bool hasExistingValue, JsonSerializer serializer)
 26 |         {
 27 |             JObject jo = JObject.Load(reader);
 28 |             return new Vector3(
 29 |                 (float)jo["x"],
 30 |                 (float)jo["y"],
 31 |                 (float)jo["z"]
 32 |             );
 33 |         }
 34 |     }
 35 | 
 36 |     public class Vector2Converter : JsonConverter<Vector2>
 37 |     {
 38 |         public override void WriteJson(JsonWriter writer, Vector2 value, JsonSerializer serializer)
 39 |         {
 40 |             writer.WriteStartObject();
 41 |             writer.WritePropertyName("x");
 42 |             writer.WriteValue(value.x);
 43 |             writer.WritePropertyName("y");
 44 |             writer.WriteValue(value.y);
 45 |             writer.WriteEndObject();
 46 |         }
 47 | 
 48 |         public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer)
 49 |         {
 50 |             JObject jo = JObject.Load(reader);
 51 |             return new Vector2(
 52 |                 (float)jo["x"],
 53 |                 (float)jo["y"]
 54 |             );
 55 |         }
 56 |     }
 57 | 
 58 |     public class QuaternionConverter : JsonConverter<Quaternion>
 59 |     {
 60 |         public override void WriteJson(JsonWriter writer, Quaternion value, JsonSerializer serializer)
 61 |         {
 62 |             writer.WriteStartObject();
 63 |             writer.WritePropertyName("x");
 64 |             writer.WriteValue(value.x);
 65 |             writer.WritePropertyName("y");
 66 |             writer.WriteValue(value.y);
 67 |             writer.WritePropertyName("z");
 68 |             writer.WriteValue(value.z);
 69 |             writer.WritePropertyName("w");
 70 |             writer.WriteValue(value.w);
 71 |             writer.WriteEndObject();
 72 |         }
 73 | 
 74 |         public override Quaternion ReadJson(JsonReader reader, Type objectType, Quaternion existingValue, bool hasExistingValue, JsonSerializer serializer)
 75 |         {
 76 |             JObject jo = JObject.Load(reader);
 77 |             return new Quaternion(
 78 |                 (float)jo["x"],
 79 |                 (float)jo["y"],
 80 |                 (float)jo["z"],
 81 |                 (float)jo["w"]
 82 |             );
 83 |         }
 84 |     }
 85 | 
 86 |     public class ColorConverter : JsonConverter<Color>
 87 |     {
 88 |         public override void WriteJson(JsonWriter writer, Color value, JsonSerializer serializer)
 89 |         {
 90 |             writer.WriteStartObject();
 91 |             writer.WritePropertyName("r");
 92 |             writer.WriteValue(value.r);
 93 |             writer.WritePropertyName("g");
 94 |             writer.WriteValue(value.g);
 95 |             writer.WritePropertyName("b");
 96 |             writer.WriteValue(value.b);
 97 |             writer.WritePropertyName("a");
 98 |             writer.WriteValue(value.a);
 99 |             writer.WriteEndObject();
100 |         }
101 | 
102 |         public override Color ReadJson(JsonReader reader, Type objectType, Color existingValue, bool hasExistingValue, JsonSerializer serializer)
103 |         {
104 |             JObject jo = JObject.Load(reader);
105 |             return new Color(
106 |                 (float)jo["r"],
107 |                 (float)jo["g"],
108 |                 (float)jo["b"],
109 |                 (float)jo["a"]
110 |             );
111 |         }
112 |     }
113 | 
114 |     public class RectConverter : JsonConverter<Rect>
115 |     {
116 |         public override void WriteJson(JsonWriter writer, Rect value, JsonSerializer serializer)
117 |         {
118 |             writer.WriteStartObject();
119 |             writer.WritePropertyName("x");
120 |             writer.WriteValue(value.x);
121 |             writer.WritePropertyName("y");
122 |             writer.WriteValue(value.y);
123 |             writer.WritePropertyName("width");
124 |             writer.WriteValue(value.width);
125 |             writer.WritePropertyName("height");
126 |             writer.WriteValue(value.height);
127 |             writer.WriteEndObject();
128 |         }
129 | 
130 |         public override Rect ReadJson(JsonReader reader, Type objectType, Rect existingValue, bool hasExistingValue, JsonSerializer serializer)
131 |         {
132 |             JObject jo = JObject.Load(reader);
133 |             return new Rect(
134 |                 (float)jo["x"],
135 |                 (float)jo["y"],
136 |                 (float)jo["width"],
137 |                 (float)jo["height"]
138 |             );
139 |         }
140 |     }
141 | 
142 |     public class BoundsConverter : JsonConverter<Bounds>
143 |     {
144 |         public override void WriteJson(JsonWriter writer, Bounds value, JsonSerializer serializer)
145 |         {
146 |             writer.WriteStartObject();
147 |             writer.WritePropertyName("center");
148 |             serializer.Serialize(writer, value.center); // Use serializer to handle nested Vector3
149 |             writer.WritePropertyName("size");
150 |             serializer.Serialize(writer, value.size);   // Use serializer to handle nested Vector3
151 |             writer.WriteEndObject();
152 |         }
153 | 
154 |         public override Bounds ReadJson(JsonReader reader, Type objectType, Bounds existingValue, bool hasExistingValue, JsonSerializer serializer)
155 |         {
156 |             JObject jo = JObject.Load(reader);
157 |             Vector3 center = jo["center"].ToObject<Vector3>(serializer); // Use serializer to handle nested Vector3
158 |             Vector3 size = jo["size"].ToObject<Vector3>(serializer);     // Use serializer to handle nested Vector3
159 |             return new Bounds(center, size);
160 |         }
161 |     }
162 | 
163 |     // Converter for UnityEngine.Object references (GameObjects, Components, Materials, Textures, etc.)
164 |     public class UnityEngineObjectConverter : JsonConverter<UnityEngine.Object>
165 |     {
166 |         public override bool CanRead => true; // We need to implement ReadJson
167 |         public override bool CanWrite => true;
168 | 
169 |         public override void WriteJson(JsonWriter writer, UnityEngine.Object value, JsonSerializer serializer)
170 |         {
171 |             if (value == null)
172 |             {
173 |                 writer.WriteNull();
174 |                 return;
175 |             }
176 | 
177 | #if UNITY_EDITOR // AssetDatabase and EditorUtility are Editor-only
178 |             if (UnityEditor.AssetDatabase.Contains(value))
179 |             {
180 |                 // It's an asset (Material, Texture, Prefab, etc.)
181 |                 string path = UnityEditor.AssetDatabase.GetAssetPath(value);
182 |                 if (!string.IsNullOrEmpty(path))
183 |                 {
184 |                     writer.WriteValue(path);
185 |                 }
186 |                 else
187 |                 {
188 |                     // Asset exists but path couldn't be found? Write minimal info.
189 |                     writer.WriteStartObject();
190 |                     writer.WritePropertyName("name");
191 |                     writer.WriteValue(value.name);
192 |                     writer.WritePropertyName("instanceID");
193 |                     writer.WriteValue(value.GetInstanceID());
194 |                     writer.WritePropertyName("isAssetWithoutPath");
195 |                     writer.WriteValue(true);
196 |                     writer.WriteEndObject();
197 |                 }
198 |             }
199 |             else
200 |             {
201 |                 // It's a scene object (GameObject, Component, etc.)
202 |                 writer.WriteStartObject();
203 |                 writer.WritePropertyName("name");
204 |                 writer.WriteValue(value.name);
205 |                 writer.WritePropertyName("instanceID");
206 |                 writer.WriteValue(value.GetInstanceID());
207 |                 writer.WriteEndObject();
208 |             }
209 | #else
210 |             // Runtime fallback: Write basic info without AssetDatabase
211 |             writer.WriteStartObject();
212 |             writer.WritePropertyName("name");
213 |             writer.WriteValue(value.name);
214 |             writer.WritePropertyName("instanceID");
215 |             writer.WriteValue(value.GetInstanceID());
216 |              writer.WritePropertyName("warning");
217 |             writer.WriteValue("UnityEngineObjectConverter running in non-Editor mode, asset path unavailable.");
218 |             writer.WriteEndObject();
219 | #endif
220 |         }
221 | 
222 |         public override UnityEngine.Object ReadJson(JsonReader reader, Type objectType, UnityEngine.Object existingValue, bool hasExistingValue, JsonSerializer serializer)
223 |         {
224 |             if (reader.TokenType == JsonToken.Null)
225 |             {
226 |                 return null;
227 |             }
228 | 
229 | #if UNITY_EDITOR
230 |             if (reader.TokenType == JsonToken.String)
231 |             {
232 |                 // Assume it's an asset path
233 |                 string path = reader.Value.ToString();
234 |                 return UnityEditor.AssetDatabase.LoadAssetAtPath(path, objectType);
235 |             }
236 | 
237 |             if (reader.TokenType == JsonToken.StartObject)
238 |             {
239 |                 JObject jo = JObject.Load(reader);
240 |                 if (jo.TryGetValue("instanceID", out JToken idToken) && idToken.Type == JTokenType.Integer)
241 |                 {
242 |                     int instanceId = idToken.ToObject<int>();
243 |                     UnityEngine.Object obj = UnityEditor.EditorUtility.InstanceIDToObject(instanceId);
244 |                     if (obj != null && objectType.IsAssignableFrom(obj.GetType()))
245 |                     {
246 |                         return obj;
247 |                     }
248 |                 }
249 |                 // Could potentially try finding by name as a fallback if ID lookup fails/isn't present
250 |                 // but that's less reliable.
251 |             }
252 | #else
253 |              // Runtime deserialization is tricky without AssetDatabase/EditorUtility
254 |              // Maybe log a warning and return null or existingValue?
255 |              Debug.LogWarning("UnityEngineObjectConverter cannot deserialize complex objects in non-Editor mode.");
256 |              // Skip the token to avoid breaking the reader
257 |              if (reader.TokenType == JsonToken.StartObject) JObject.Load(reader);
258 |              else if (reader.TokenType == JsonToken.String) reader.ReadAsString(); 
259 |              // Return null or existing value, depending on desired behavior
260 |              return existingValue; 
261 | #endif
262 | 
263 |             throw new JsonSerializationException($"Unexpected token type '{reader.TokenType}' when deserializing UnityEngine.Object");
264 |         }
265 |     }
266 | }
```

--------------------------------------------------------------------------------
/UnityMcpBridge/Runtime/Serialization/UnityTypeConverters.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using Newtonsoft.Json;
  2 | using Newtonsoft.Json.Linq;
  3 | using System;
  4 | using UnityEngine;
  5 | #if UNITY_EDITOR
  6 | using UnityEditor; // Required for AssetDatabase and EditorUtility
  7 | #endif
  8 | 
  9 | namespace MCPForUnity.Runtime.Serialization
 10 | {
 11 |     public class Vector3Converter : JsonConverter<Vector3>
 12 |     {
 13 |         public override void WriteJson(JsonWriter writer, Vector3 value, JsonSerializer serializer)
 14 |         {
 15 |             writer.WriteStartObject();
 16 |             writer.WritePropertyName("x");
 17 |             writer.WriteValue(value.x);
 18 |             writer.WritePropertyName("y");
 19 |             writer.WriteValue(value.y);
 20 |             writer.WritePropertyName("z");
 21 |             writer.WriteValue(value.z);
 22 |             writer.WriteEndObject();
 23 |         }
 24 | 
 25 |         public override Vector3 ReadJson(JsonReader reader, Type objectType, Vector3 existingValue, bool hasExistingValue, JsonSerializer serializer)
 26 |         {
 27 |             JObject jo = JObject.Load(reader);
 28 |             return new Vector3(
 29 |                 (float)jo["x"],
 30 |                 (float)jo["y"],
 31 |                 (float)jo["z"]
 32 |             );
 33 |         }
 34 |     }
 35 | 
 36 |     public class Vector2Converter : JsonConverter<Vector2>
 37 |     {
 38 |         public override void WriteJson(JsonWriter writer, Vector2 value, JsonSerializer serializer)
 39 |         {
 40 |             writer.WriteStartObject();
 41 |             writer.WritePropertyName("x");
 42 |             writer.WriteValue(value.x);
 43 |             writer.WritePropertyName("y");
 44 |             writer.WriteValue(value.y);
 45 |             writer.WriteEndObject();
 46 |         }
 47 | 
 48 |         public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer)
 49 |         {
 50 |             JObject jo = JObject.Load(reader);
 51 |             return new Vector2(
 52 |                 (float)jo["x"],
 53 |                 (float)jo["y"]
 54 |             );
 55 |         }
 56 |     }
 57 | 
 58 |     public class QuaternionConverter : JsonConverter<Quaternion>
 59 |     {
 60 |         public override void WriteJson(JsonWriter writer, Quaternion value, JsonSerializer serializer)
 61 |         {
 62 |             writer.WriteStartObject();
 63 |             writer.WritePropertyName("x");
 64 |             writer.WriteValue(value.x);
 65 |             writer.WritePropertyName("y");
 66 |             writer.WriteValue(value.y);
 67 |             writer.WritePropertyName("z");
 68 |             writer.WriteValue(value.z);
 69 |             writer.WritePropertyName("w");
 70 |             writer.WriteValue(value.w);
 71 |             writer.WriteEndObject();
 72 |         }
 73 | 
 74 |         public override Quaternion ReadJson(JsonReader reader, Type objectType, Quaternion existingValue, bool hasExistingValue, JsonSerializer serializer)
 75 |         {
 76 |             JObject jo = JObject.Load(reader);
 77 |             return new Quaternion(
 78 |                 (float)jo["x"],
 79 |                 (float)jo["y"],
 80 |                 (float)jo["z"],
 81 |                 (float)jo["w"]
 82 |             );
 83 |         }
 84 |     }
 85 | 
 86 |     public class ColorConverter : JsonConverter<Color>
 87 |     {
 88 |         public override void WriteJson(JsonWriter writer, Color value, JsonSerializer serializer)
 89 |         {
 90 |             writer.WriteStartObject();
 91 |             writer.WritePropertyName("r");
 92 |             writer.WriteValue(value.r);
 93 |             writer.WritePropertyName("g");
 94 |             writer.WriteValue(value.g);
 95 |             writer.WritePropertyName("b");
 96 |             writer.WriteValue(value.b);
 97 |             writer.WritePropertyName("a");
 98 |             writer.WriteValue(value.a);
 99 |             writer.WriteEndObject();
100 |         }
101 | 
102 |         public override Color ReadJson(JsonReader reader, Type objectType, Color existingValue, bool hasExistingValue, JsonSerializer serializer)
103 |         {
104 |             JObject jo = JObject.Load(reader);
105 |             return new Color(
106 |                 (float)jo["r"],
107 |                 (float)jo["g"],
108 |                 (float)jo["b"],
109 |                 (float)jo["a"]
110 |             );
111 |         }
112 |     }
113 | 
114 |     public class RectConverter : JsonConverter<Rect>
115 |     {
116 |         public override void WriteJson(JsonWriter writer, Rect value, JsonSerializer serializer)
117 |         {
118 |             writer.WriteStartObject();
119 |             writer.WritePropertyName("x");
120 |             writer.WriteValue(value.x);
121 |             writer.WritePropertyName("y");
122 |             writer.WriteValue(value.y);
123 |             writer.WritePropertyName("width");
124 |             writer.WriteValue(value.width);
125 |             writer.WritePropertyName("height");
126 |             writer.WriteValue(value.height);
127 |             writer.WriteEndObject();
128 |         }
129 | 
130 |         public override Rect ReadJson(JsonReader reader, Type objectType, Rect existingValue, bool hasExistingValue, JsonSerializer serializer)
131 |         {
132 |             JObject jo = JObject.Load(reader);
133 |             return new Rect(
134 |                 (float)jo["x"],
135 |                 (float)jo["y"],
136 |                 (float)jo["width"],
137 |                 (float)jo["height"]
138 |             );
139 |         }
140 |     }
141 | 
142 |     public class BoundsConverter : JsonConverter<Bounds>
143 |     {
144 |         public override void WriteJson(JsonWriter writer, Bounds value, JsonSerializer serializer)
145 |         {
146 |             writer.WriteStartObject();
147 |             writer.WritePropertyName("center");
148 |             serializer.Serialize(writer, value.center); // Use serializer to handle nested Vector3
149 |             writer.WritePropertyName("size");
150 |             serializer.Serialize(writer, value.size);   // Use serializer to handle nested Vector3
151 |             writer.WriteEndObject();
152 |         }
153 | 
154 |         public override Bounds ReadJson(JsonReader reader, Type objectType, Bounds existingValue, bool hasExistingValue, JsonSerializer serializer)
155 |         {
156 |             JObject jo = JObject.Load(reader);
157 |             Vector3 center = jo["center"].ToObject<Vector3>(serializer); // Use serializer to handle nested Vector3
158 |             Vector3 size = jo["size"].ToObject<Vector3>(serializer);     // Use serializer to handle nested Vector3
159 |             return new Bounds(center, size);
160 |         }
161 |     }
162 | 
163 |     // Converter for UnityEngine.Object references (GameObjects, Components, Materials, Textures, etc.)
164 |     public class UnityEngineObjectConverter : JsonConverter<UnityEngine.Object>
165 |     {
166 |         public override bool CanRead => true; // We need to implement ReadJson
167 |         public override bool CanWrite => true;
168 | 
169 |         public override void WriteJson(JsonWriter writer, UnityEngine.Object value, JsonSerializer serializer)
170 |         {
171 |             if (value == null)
172 |             {
173 |                 writer.WriteNull();
174 |                 return;
175 |             }
176 | 
177 | #if UNITY_EDITOR // AssetDatabase and EditorUtility are Editor-only
178 |             if (UnityEditor.AssetDatabase.Contains(value))
179 |             {
180 |                 // It's an asset (Material, Texture, Prefab, etc.)
181 |                 string path = UnityEditor.AssetDatabase.GetAssetPath(value);
182 |                 if (!string.IsNullOrEmpty(path))
183 |                 {
184 |                     writer.WriteValue(path);
185 |                 }
186 |                 else
187 |                 {
188 |                     // Asset exists but path couldn't be found? Write minimal info.
189 |                     writer.WriteStartObject();
190 |                     writer.WritePropertyName("name");
191 |                     writer.WriteValue(value.name);
192 |                     writer.WritePropertyName("instanceID");
193 |                     writer.WriteValue(value.GetInstanceID());
194 |                     writer.WritePropertyName("isAssetWithoutPath");
195 |                     writer.WriteValue(true);
196 |                     writer.WriteEndObject();
197 |                 }
198 |             }
199 |             else
200 |             {
201 |                 // It's a scene object (GameObject, Component, etc.)
202 |                 writer.WriteStartObject();
203 |                 writer.WritePropertyName("name");
204 |                 writer.WriteValue(value.name);
205 |                 writer.WritePropertyName("instanceID");
206 |                 writer.WriteValue(value.GetInstanceID());
207 |                 writer.WriteEndObject();
208 |             }
209 | #else
210 |             // Runtime fallback: Write basic info without AssetDatabase
211 |             writer.WriteStartObject();
212 |             writer.WritePropertyName("name");
213 |             writer.WriteValue(value.name);
214 |             writer.WritePropertyName("instanceID");
215 |             writer.WriteValue(value.GetInstanceID());
216 |              writer.WritePropertyName("warning");
217 |             writer.WriteValue("UnityEngineObjectConverter running in non-Editor mode, asset path unavailable.");
218 |             writer.WriteEndObject();
219 | #endif
220 |         }
221 | 
222 |         public override UnityEngine.Object ReadJson(JsonReader reader, Type objectType, UnityEngine.Object existingValue, bool hasExistingValue, JsonSerializer serializer)
223 |         {
224 |             if (reader.TokenType == JsonToken.Null)
225 |             {
226 |                 return null;
227 |             }
228 | 
229 | #if UNITY_EDITOR
230 |             if (reader.TokenType == JsonToken.String)
231 |             {
232 |                 // Assume it's an asset path
233 |                 string path = reader.Value.ToString();
234 |                 return UnityEditor.AssetDatabase.LoadAssetAtPath(path, objectType);
235 |             }
236 | 
237 |             if (reader.TokenType == JsonToken.StartObject)
238 |             {
239 |                 JObject jo = JObject.Load(reader);
240 |                 if (jo.TryGetValue("instanceID", out JToken idToken) && idToken.Type == JTokenType.Integer)
241 |                 {
242 |                     int instanceId = idToken.ToObject<int>();
243 |                     UnityEngine.Object obj = UnityEditor.EditorUtility.InstanceIDToObject(instanceId);
244 |                     if (obj != null && objectType.IsAssignableFrom(obj.GetType()))
245 |                     {
246 |                         return obj;
247 |                     }
248 |                 }
249 |                 // Could potentially try finding by name as a fallback if ID lookup fails/isn't present
250 |                 // but that's less reliable.
251 |             }
252 | #else
253 |              // Runtime deserialization is tricky without AssetDatabase/EditorUtility
254 |              // Maybe log a warning and return null or existingValue?
255 |              Debug.LogWarning("UnityEngineObjectConverter cannot deserialize complex objects in non-Editor mode.");
256 |              // Skip the token to avoid breaking the reader
257 |              if (reader.TokenType == JsonToken.StartObject) JObject.Load(reader);
258 |              else if (reader.TokenType == JsonToken.String) reader.ReadAsString(); 
259 |              // Return null or existing value, depending on desired behavior
260 |              return existingValue; 
261 | #endif
262 | 
263 |             throw new JsonSerializationException($"Unexpected token type '{reader.TokenType}' when deserializing UnityEngine.Object");
264 |         }
265 |     }
266 | }
```

--------------------------------------------------------------------------------
/MCPForUnity/Editor/Windows/ManualConfigEditorWindow.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System.Runtime.InteropServices;
  2 | using UnityEditor;
  3 | using UnityEngine;
  4 | using MCPForUnity.Editor.Models;
  5 | 
  6 | namespace MCPForUnity.Editor.Windows
  7 | {
  8 |     // Editor window to display manual configuration instructions
  9 |     public class ManualConfigEditorWindow : EditorWindow
 10 |     {
 11 |         protected string configPath;
 12 |         protected string configJson;
 13 |         protected Vector2 scrollPos;
 14 |         protected bool pathCopied = false;
 15 |         protected bool jsonCopied = false;
 16 |         protected float copyFeedbackTimer = 0;
 17 |         protected McpClient mcpClient;
 18 | 
 19 |         public static void ShowWindow(string configPath, string configJson, McpClient mcpClient)
 20 |         {
 21 |             var window = GetWindow<ManualConfigEditorWindow>("Manual Configuration");
 22 |             window.configPath = configPath;
 23 |             window.configJson = configJson;
 24 |             window.mcpClient = mcpClient;
 25 |             window.minSize = new Vector2(500, 400);
 26 |             window.Show();
 27 |         }
 28 | 
 29 |         protected virtual void OnGUI()
 30 |         {
 31 |             scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
 32 | 
 33 |             // Header with improved styling
 34 |             EditorGUILayout.Space(10);
 35 |             Rect titleRect = EditorGUILayout.GetControlRect(false, 30);
 36 |             EditorGUI.DrawRect(
 37 |                 new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height),
 38 |                 new Color(0.2f, 0.2f, 0.2f, 0.1f)
 39 |             );
 40 |             GUI.Label(
 41 |                 new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height),
 42 |                 (mcpClient?.name ?? "Unknown") + " Manual Configuration",
 43 |                 EditorStyles.boldLabel
 44 |             );
 45 |             EditorGUILayout.Space(10);
 46 | 
 47 |             // Instructions with improved styling
 48 |             EditorGUILayout.BeginVertical(EditorStyles.helpBox);
 49 | 
 50 |             Rect headerRect = EditorGUILayout.GetControlRect(false, 24);
 51 |             EditorGUI.DrawRect(
 52 |                 new Rect(headerRect.x, headerRect.y, headerRect.width, headerRect.height),
 53 |                 new Color(0.1f, 0.1f, 0.1f, 0.2f)
 54 |             );
 55 |             GUI.Label(
 56 |                 new Rect(
 57 |                     headerRect.x + 8,
 58 |                     headerRect.y + 4,
 59 |                     headerRect.width - 16,
 60 |                     headerRect.height
 61 |                 ),
 62 |                 "The automatic configuration failed. Please follow these steps:",
 63 |                 EditorStyles.boldLabel
 64 |             );
 65 |             EditorGUILayout.Space(10);
 66 | 
 67 |             GUIStyle instructionStyle = new(EditorStyles.wordWrappedLabel)
 68 |             {
 69 |                 margin = new RectOffset(10, 10, 5, 5),
 70 |             };
 71 | 
 72 |             EditorGUILayout.LabelField(
 73 |                 "1. Open " + (mcpClient?.name ?? "Unknown") + " config file by either:",
 74 |                 instructionStyle
 75 |             );
 76 |             if (mcpClient?.mcpType == McpTypes.ClaudeDesktop)
 77 |             {
 78 |                 EditorGUILayout.LabelField(
 79 |                     "    a) Going to Settings > Developer > Edit Config",
 80 |                     instructionStyle
 81 |                 );
 82 |             }
 83 |             else if (mcpClient?.mcpType == McpTypes.Cursor)
 84 |             {
 85 |                 EditorGUILayout.LabelField(
 86 |                     "    a) Going to File > Preferences > Cursor Settings > MCP > Add new global MCP server",
 87 |                     instructionStyle
 88 |                 );
 89 |             }
 90 |             else if (mcpClient?.mcpType == McpTypes.Windsurf)
 91 |             {
 92 |                 EditorGUILayout.LabelField(
 93 |                     "    a) Going to File > Preferences > Windsurf Settings > MCP > Manage MCPs -> View raw config",
 94 |                     instructionStyle
 95 |                 );
 96 |             }
 97 |             else if (mcpClient?.mcpType == McpTypes.Kiro)
 98 |             {
 99 |                 EditorGUILayout.LabelField(
100 |                     "    a) Going to File > Settings > Settings > Search for \"MCP\" > Open Workspace MCP Config",
101 |                     instructionStyle
102 |                 );
103 |             }
104 |             else if (mcpClient?.mcpType == McpTypes.Codex)
105 |             {
106 |                 EditorGUILayout.LabelField(
107 |                     "    a) Running `codex config edit` in a terminal",
108 |                     instructionStyle
109 |                 );
110 |             }
111 |             EditorGUILayout.LabelField("    OR", instructionStyle);
112 |             EditorGUILayout.LabelField(
113 |                 "    b) Opening the configuration file at:",
114 |                 instructionStyle
115 |             );
116 | 
117 |             // Path section with improved styling
118 |             EditorGUILayout.BeginVertical(EditorStyles.helpBox);
119 |             string displayPath;
120 |             if (mcpClient != null)
121 |             {
122 |                 if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
123 |                 {
124 |                     displayPath = mcpClient.windowsConfigPath;
125 |                 }
126 |                 else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
127 |                 {
128 |                     displayPath = string.IsNullOrEmpty(mcpClient.macConfigPath)
129 | 
130 |                         ? configPath
131 | 
132 |                         : mcpClient.macConfigPath;
133 |                 }
134 |                 else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
135 |                 {
136 |                     displayPath = mcpClient.linuxConfigPath;
137 |                 }
138 |                 else
139 |                 {
140 |                     displayPath = configPath;
141 |                 }
142 |             }
143 |             else
144 |             {
145 |                 displayPath = configPath;
146 |             }
147 | 
148 |             // Prevent text overflow by allowing the text field to wrap
149 |             GUIStyle pathStyle = new(EditorStyles.textField) { wordWrap = true };
150 | 
151 |             EditorGUILayout.TextField(
152 |                 displayPath,
153 |                 pathStyle,
154 |                 GUILayout.Height(EditorGUIUtility.singleLineHeight)
155 |             );
156 | 
157 |             // Copy button with improved styling
158 |             EditorGUILayout.BeginHorizontal();
159 |             GUILayout.FlexibleSpace();
160 |             GUIStyle copyButtonStyle = new(GUI.skin.button)
161 |             {
162 |                 padding = new RectOffset(15, 15, 5, 5),
163 |                 margin = new RectOffset(10, 10, 5, 5),
164 |             };
165 | 
166 |             if (
167 |                 GUILayout.Button(
168 |                     "Copy Path",
169 |                     copyButtonStyle,
170 |                     GUILayout.Height(25),
171 |                     GUILayout.Width(100)
172 |                 )
173 |             )
174 |             {
175 |                 EditorGUIUtility.systemCopyBuffer = displayPath;
176 |                 pathCopied = true;
177 |                 copyFeedbackTimer = 2f;
178 |             }
179 | 
180 |             if (
181 |                 GUILayout.Button(
182 |                     "Open File",
183 |                     copyButtonStyle,
184 |                     GUILayout.Height(25),
185 |                     GUILayout.Width(100)
186 |                 )
187 |             )
188 |             {
189 |                 // Open the file using the system's default application
190 |                 System.Diagnostics.Process.Start(
191 |                     new System.Diagnostics.ProcessStartInfo
192 |                     {
193 |                         FileName = displayPath,
194 |                         UseShellExecute = true,
195 |                     }
196 |                 );
197 |             }
198 | 
199 |             if (pathCopied)
200 |             {
201 |                 GUIStyle feedbackStyle = new(EditorStyles.label);
202 |                 feedbackStyle.normal.textColor = Color.green;
203 |                 EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60));
204 |             }
205 | 
206 |             EditorGUILayout.EndHorizontal();
207 |             EditorGUILayout.EndVertical();
208 | 
209 |             EditorGUILayout.Space(10);
210 | 
211 |             string configLabel = mcpClient?.mcpType == McpTypes.Codex
212 |                 ? "2. Paste the following TOML configuration:"
213 |                 : "2. Paste the following JSON configuration:";
214 |             EditorGUILayout.LabelField(configLabel, instructionStyle);
215 | 
216 |             // JSON section with improved styling
217 |             EditorGUILayout.BeginVertical(EditorStyles.helpBox);
218 | 
219 |             // Improved text area for JSON with syntax highlighting colors
220 |             GUIStyle jsonStyle = new(EditorStyles.textArea)
221 |             {
222 |                 font = EditorStyles.boldFont,
223 |                 wordWrap = true,
224 |             };
225 |             jsonStyle.normal.textColor = new Color(0.3f, 0.6f, 0.9f); // Syntax highlighting blue
226 | 
227 |             // Draw the JSON in a text area with a taller height for better readability
228 |             EditorGUILayout.TextArea(configJson, jsonStyle, GUILayout.Height(200));
229 | 
230 |             // Copy JSON button with improved styling
231 |             EditorGUILayout.BeginHorizontal();
232 |             GUILayout.FlexibleSpace();
233 | 
234 |             if (
235 |                 GUILayout.Button(
236 |                     "Copy JSON",
237 |                     copyButtonStyle,
238 |                     GUILayout.Height(25),
239 |                     GUILayout.Width(100)
240 |                 )
241 |             )
242 |             {
243 |                 EditorGUIUtility.systemCopyBuffer = configJson;
244 |                 jsonCopied = true;
245 |                 copyFeedbackTimer = 2f;
246 |             }
247 | 
248 |             if (jsonCopied)
249 |             {
250 |                 GUIStyle feedbackStyle = new(EditorStyles.label);
251 |                 feedbackStyle.normal.textColor = Color.green;
252 |                 EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60));
253 |             }
254 | 
255 |             EditorGUILayout.EndHorizontal();
256 |             EditorGUILayout.EndVertical();
257 | 
258 |             EditorGUILayout.Space(10);
259 |             EditorGUILayout.LabelField(
260 |                 "3. Save the file and restart " + (mcpClient?.name ?? "Unknown"),
261 |                 instructionStyle
262 |             );
263 | 
264 |             EditorGUILayout.EndVertical();
265 | 
266 |             EditorGUILayout.Space(10);
267 | 
268 |             // Close button at the bottom
269 |             EditorGUILayout.BeginHorizontal();
270 |             GUILayout.FlexibleSpace();
271 |             if (GUILayout.Button("Close", GUILayout.Height(30), GUILayout.Width(100)))
272 |             {
273 |                 Close();
274 |             }
275 |             GUILayout.FlexibleSpace();
276 |             EditorGUILayout.EndHorizontal();
277 | 
278 |             EditorGUILayout.EndScrollView();
279 |         }
280 | 
281 |         protected virtual void Update()
282 |         {
283 |             // Handle the feedback message timer
284 |             if (copyFeedbackTimer > 0)
285 |             {
286 |                 copyFeedbackTimer -= Time.deltaTime;
287 |                 if (copyFeedbackTimer <= 0)
288 |                 {
289 |                     pathCopied = false;
290 |                     jsonCopied = false;
291 |                     Repaint();
292 |                 }
293 |             }
294 |         }
295 |     }
296 | }
297 | 
```

--------------------------------------------------------------------------------
/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System.Runtime.InteropServices;
  2 | using UnityEditor;
  3 | using UnityEngine;
  4 | using MCPForUnity.Editor.Models;
  5 | 
  6 | namespace MCPForUnity.Editor.Windows
  7 | {
  8 |     // Editor window to display manual configuration instructions
  9 |     public class ManualConfigEditorWindow : EditorWindow
 10 |     {
 11 |         protected string configPath;
 12 |         protected string configJson;
 13 |         protected Vector2 scrollPos;
 14 |         protected bool pathCopied = false;
 15 |         protected bool jsonCopied = false;
 16 |         protected float copyFeedbackTimer = 0;
 17 |         protected McpClient mcpClient;
 18 | 
 19 |         public static void ShowWindow(string configPath, string configJson, McpClient mcpClient)
 20 |         {
 21 |             var window = GetWindow<ManualConfigEditorWindow>("Manual Configuration");
 22 |             window.configPath = configPath;
 23 |             window.configJson = configJson;
 24 |             window.mcpClient = mcpClient;
 25 |             window.minSize = new Vector2(500, 400);
 26 |             window.Show();
 27 |         }
 28 | 
 29 |         protected virtual void OnGUI()
 30 |         {
 31 |             scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
 32 | 
 33 |             // Header with improved styling
 34 |             EditorGUILayout.Space(10);
 35 |             Rect titleRect = EditorGUILayout.GetControlRect(false, 30);
 36 |             EditorGUI.DrawRect(
 37 |                 new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height),
 38 |                 new Color(0.2f, 0.2f, 0.2f, 0.1f)
 39 |             );
 40 |             GUI.Label(
 41 |                 new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height),
 42 |                 (mcpClient?.name ?? "Unknown") + " Manual Configuration",
 43 |                 EditorStyles.boldLabel
 44 |             );
 45 |             EditorGUILayout.Space(10);
 46 | 
 47 |             // Instructions with improved styling
 48 |             EditorGUILayout.BeginVertical(EditorStyles.helpBox);
 49 | 
 50 |             Rect headerRect = EditorGUILayout.GetControlRect(false, 24);
 51 |             EditorGUI.DrawRect(
 52 |                 new Rect(headerRect.x, headerRect.y, headerRect.width, headerRect.height),
 53 |                 new Color(0.1f, 0.1f, 0.1f, 0.2f)
 54 |             );
 55 |             GUI.Label(
 56 |                 new Rect(
 57 |                     headerRect.x + 8,
 58 |                     headerRect.y + 4,
 59 |                     headerRect.width - 16,
 60 |                     headerRect.height
 61 |                 ),
 62 |                 "The automatic configuration failed. Please follow these steps:",
 63 |                 EditorStyles.boldLabel
 64 |             );
 65 |             EditorGUILayout.Space(10);
 66 | 
 67 |             GUIStyle instructionStyle = new(EditorStyles.wordWrappedLabel)
 68 |             {
 69 |                 margin = new RectOffset(10, 10, 5, 5),
 70 |             };
 71 | 
 72 |             EditorGUILayout.LabelField(
 73 |                 "1. Open " + (mcpClient?.name ?? "Unknown") + " config file by either:",
 74 |                 instructionStyle
 75 |             );
 76 |             if (mcpClient?.mcpType == McpTypes.ClaudeDesktop)
 77 |             {
 78 |                 EditorGUILayout.LabelField(
 79 |                     "    a) Going to Settings > Developer > Edit Config",
 80 |                     instructionStyle
 81 |                 );
 82 |             }
 83 |             else if (mcpClient?.mcpType == McpTypes.Cursor)
 84 |             {
 85 |                 EditorGUILayout.LabelField(
 86 |                     "    a) Going to File > Preferences > Cursor Settings > MCP > Add new global MCP server",
 87 |                     instructionStyle
 88 |                 );
 89 |             }
 90 |             else if (mcpClient?.mcpType == McpTypes.Windsurf)
 91 |             {
 92 |                 EditorGUILayout.LabelField(
 93 |                     "    a) Going to File > Preferences > Windsurf Settings > MCP > Manage MCPs -> View raw config",
 94 |                     instructionStyle
 95 |                 );
 96 |             }
 97 |             else if (mcpClient?.mcpType == McpTypes.Kiro)
 98 |             {
 99 |                 EditorGUILayout.LabelField(
100 |                     "    a) Going to File > Settings > Settings > Search for \"MCP\" > Open Workspace MCP Config",
101 |                     instructionStyle
102 |                 );
103 |             }
104 |             else if (mcpClient?.mcpType == McpTypes.Codex)
105 |             {
106 |                 EditorGUILayout.LabelField(
107 |                     "    a) Running `codex config edit` in a terminal",
108 |                     instructionStyle
109 |                 );
110 |             }
111 |             EditorGUILayout.LabelField("    OR", instructionStyle);
112 |             EditorGUILayout.LabelField(
113 |                 "    b) Opening the configuration file at:",
114 |                 instructionStyle
115 |             );
116 | 
117 |             // Path section with improved styling
118 |             EditorGUILayout.BeginVertical(EditorStyles.helpBox);
119 |             string displayPath;
120 |             if (mcpClient != null)
121 |             {
122 |                 if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
123 |                 {
124 |                     displayPath = mcpClient.windowsConfigPath;
125 |                 }
126 |                 else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
127 |                 {
128 |                     displayPath = string.IsNullOrEmpty(mcpClient.macConfigPath)
129 | 
130 |                         ? configPath
131 | 
132 |                         : mcpClient.macConfigPath;
133 |                 }
134 |                 else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
135 |                 {
136 |                     displayPath = mcpClient.linuxConfigPath;
137 |                 }
138 |                 else
139 |                 {
140 |                     displayPath = configPath;
141 |                 }
142 |             }
143 |             else
144 |             {
145 |                 displayPath = configPath;
146 |             }
147 | 
148 |             // Prevent text overflow by allowing the text field to wrap
149 |             GUIStyle pathStyle = new(EditorStyles.textField) { wordWrap = true };
150 | 
151 |             EditorGUILayout.TextField(
152 |                 displayPath,
153 |                 pathStyle,
154 |                 GUILayout.Height(EditorGUIUtility.singleLineHeight)
155 |             );
156 | 
157 |             // Copy button with improved styling
158 |             EditorGUILayout.BeginHorizontal();
159 |             GUILayout.FlexibleSpace();
160 |             GUIStyle copyButtonStyle = new(GUI.skin.button)
161 |             {
162 |                 padding = new RectOffset(15, 15, 5, 5),
163 |                 margin = new RectOffset(10, 10, 5, 5),
164 |             };
165 | 
166 |             if (
167 |                 GUILayout.Button(
168 |                     "Copy Path",
169 |                     copyButtonStyle,
170 |                     GUILayout.Height(25),
171 |                     GUILayout.Width(100)
172 |                 )
173 |             )
174 |             {
175 |                 EditorGUIUtility.systemCopyBuffer = displayPath;
176 |                 pathCopied = true;
177 |                 copyFeedbackTimer = 2f;
178 |             }
179 | 
180 |             if (
181 |                 GUILayout.Button(
182 |                     "Open File",
183 |                     copyButtonStyle,
184 |                     GUILayout.Height(25),
185 |                     GUILayout.Width(100)
186 |                 )
187 |             )
188 |             {
189 |                 // Open the file using the system's default application
190 |                 System.Diagnostics.Process.Start(
191 |                     new System.Diagnostics.ProcessStartInfo
192 |                     {
193 |                         FileName = displayPath,
194 |                         UseShellExecute = true,
195 |                     }
196 |                 );
197 |             }
198 | 
199 |             if (pathCopied)
200 |             {
201 |                 GUIStyle feedbackStyle = new(EditorStyles.label);
202 |                 feedbackStyle.normal.textColor = Color.green;
203 |                 EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60));
204 |             }
205 | 
206 |             EditorGUILayout.EndHorizontal();
207 |             EditorGUILayout.EndVertical();
208 | 
209 |             EditorGUILayout.Space(10);
210 | 
211 |             string configLabel = mcpClient?.mcpType == McpTypes.Codex
212 |                 ? "2. Paste the following TOML configuration:"
213 |                 : "2. Paste the following JSON configuration:";
214 |             EditorGUILayout.LabelField(configLabel, instructionStyle);
215 | 
216 |             // JSON section with improved styling
217 |             EditorGUILayout.BeginVertical(EditorStyles.helpBox);
218 | 
219 |             // Improved text area for JSON with syntax highlighting colors
220 |             GUIStyle jsonStyle = new(EditorStyles.textArea)
221 |             {
222 |                 font = EditorStyles.boldFont,
223 |                 wordWrap = true,
224 |             };
225 |             jsonStyle.normal.textColor = new Color(0.3f, 0.6f, 0.9f); // Syntax highlighting blue
226 | 
227 |             // Draw the JSON in a text area with a taller height for better readability
228 |             EditorGUILayout.TextArea(configJson, jsonStyle, GUILayout.Height(200));
229 | 
230 |             // Copy JSON button with improved styling
231 |             EditorGUILayout.BeginHorizontal();
232 |             GUILayout.FlexibleSpace();
233 | 
234 |             if (
235 |                 GUILayout.Button(
236 |                     "Copy JSON",
237 |                     copyButtonStyle,
238 |                     GUILayout.Height(25),
239 |                     GUILayout.Width(100)
240 |                 )
241 |             )
242 |             {
243 |                 EditorGUIUtility.systemCopyBuffer = configJson;
244 |                 jsonCopied = true;
245 |                 copyFeedbackTimer = 2f;
246 |             }
247 | 
248 |             if (jsonCopied)
249 |             {
250 |                 GUIStyle feedbackStyle = new(EditorStyles.label);
251 |                 feedbackStyle.normal.textColor = Color.green;
252 |                 EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60));
253 |             }
254 | 
255 |             EditorGUILayout.EndHorizontal();
256 |             EditorGUILayout.EndVertical();
257 | 
258 |             EditorGUILayout.Space(10);
259 |             EditorGUILayout.LabelField(
260 |                 "3. Save the file and restart " + (mcpClient?.name ?? "Unknown"),
261 |                 instructionStyle
262 |             );
263 | 
264 |             EditorGUILayout.EndVertical();
265 | 
266 |             EditorGUILayout.Space(10);
267 | 
268 |             // Close button at the bottom
269 |             EditorGUILayout.BeginHorizontal();
270 |             GUILayout.FlexibleSpace();
271 |             if (GUILayout.Button("Close", GUILayout.Height(30), GUILayout.Width(100)))
272 |             {
273 |                 Close();
274 |             }
275 |             GUILayout.FlexibleSpace();
276 |             EditorGUILayout.EndHorizontal();
277 | 
278 |             EditorGUILayout.EndScrollView();
279 |         }
280 | 
281 |         protected virtual void Update()
282 |         {
283 |             // Handle the feedback message timer
284 |             if (copyFeedbackTimer > 0)
285 |             {
286 |                 copyFeedbackTimer -= Time.deltaTime;
287 |                 if (copyFeedbackTimer <= 0)
288 |                 {
289 |                     pathCopied = false;
290 |                     jsonCopied = false;
291 |                     Repaint();
292 |                 }
293 |             }
294 |         }
295 |     }
296 | }
297 | 
```

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

```csharp
  1 | using System;
  2 | using System.IO;
  3 | using System.Linq;
  4 | using System.Runtime.InteropServices;
  5 | using Newtonsoft.Json;
  6 | using Newtonsoft.Json.Linq;
  7 | using UnityEditor;
  8 | using UnityEngine;
  9 | using MCPForUnity.Editor.Dependencies;
 10 | using MCPForUnity.Editor.Helpers;
 11 | using MCPForUnity.Editor.Models;
 12 | 
 13 | namespace MCPForUnity.Editor.Helpers
 14 | {
 15 |     /// <summary>
 16 |     /// Shared helper for MCP client configuration management with sophisticated
 17 |     /// logic for preserving existing configs and handling different client types
 18 |     /// </summary>
 19 |     public static class McpConfigurationHelper
 20 |     {
 21 |         private const string LOCK_CONFIG_KEY = "MCPForUnity.LockCursorConfig";
 22 | 
 23 |         /// <summary>
 24 |         /// Writes MCP configuration to the specified path using sophisticated logic
 25 |         /// that preserves existing configuration and only writes when necessary
 26 |         /// </summary>
 27 |         public static string WriteMcpConfiguration(string pythonDir, string configPath, McpClient mcpClient = null)
 28 |         {
 29 |             // 0) Respect explicit lock (hidden pref or UI toggle)
 30 |             try
 31 |             {
 32 |                 if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false))
 33 |                     return "Skipped (locked)";
 34 |             }
 35 |             catch { }
 36 | 
 37 |             JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented };
 38 | 
 39 |             // Read existing config if it exists
 40 |             string existingJson = "{}";
 41 |             if (File.Exists(configPath))
 42 |             {
 43 |                 try
 44 |                 {
 45 |                     existingJson = File.ReadAllText(configPath);
 46 |                 }
 47 |                 catch (Exception e)
 48 |                 {
 49 |                     Debug.LogWarning($"Error reading existing config: {e.Message}.");
 50 |                 }
 51 |             }
 52 | 
 53 |             // Parse the existing JSON while preserving all properties
 54 |             dynamic existingConfig;
 55 |             try
 56 |             {
 57 |                 if (string.IsNullOrWhiteSpace(existingJson))
 58 |                 {
 59 |                     existingConfig = new JObject();
 60 |                 }
 61 |                 else
 62 |                 {
 63 |                     existingConfig = JsonConvert.DeserializeObject(existingJson) ?? new JObject();
 64 |                 }
 65 |             }
 66 |             catch
 67 |             {
 68 |                 // If user has partial/invalid JSON (e.g., mid-edit), start from a fresh object
 69 |                 if (!string.IsNullOrWhiteSpace(existingJson))
 70 |                 {
 71 |                     Debug.LogWarning("UnityMCP: Configuration file could not be parsed; rewriting server block.");
 72 |                 }
 73 |                 existingConfig = new JObject();
 74 |             }
 75 | 
 76 |             // Determine existing entry references (command/args)
 77 |             string existingCommand = null;
 78 |             string[] existingArgs = null;
 79 |             bool isVSCode = (mcpClient?.mcpType == McpTypes.VSCode);
 80 |             try
 81 |             {
 82 |                 if (isVSCode)
 83 |                 {
 84 |                     existingCommand = existingConfig?.servers?.unityMCP?.command?.ToString();
 85 |                     existingArgs = existingConfig?.servers?.unityMCP?.args?.ToObject<string[]>();
 86 |                 }
 87 |                 else
 88 |                 {
 89 |                     existingCommand = existingConfig?.mcpServers?.unityMCP?.command?.ToString();
 90 |                     existingArgs = existingConfig?.mcpServers?.unityMCP?.args?.ToObject<string[]>();
 91 |                 }
 92 |             }
 93 |             catch { }
 94 | 
 95 |             // 1) Start from existing, only fill gaps (prefer trusted resolver)
 96 |             string uvPath = ServerInstaller.FindUvPath();
 97 |             // Optionally trust existingCommand if it looks like uv/uv.exe
 98 |             try
 99 |             {
100 |                 var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant();
101 |                 if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand))
102 |                 {
103 |                     uvPath = existingCommand;
104 |                 }
105 |             }
106 |             catch { }
107 |             if (uvPath == null) return "UV package manager not found. Please install UV first.";
108 |             string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs);
109 | 
110 |             // 2) Canonical args order
111 |             var newArgs = new[] { "run", "--directory", serverSrc, "server.py" };
112 | 
113 |             // 3) Only write if changed
114 |             bool changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal)
115 |                 || !ArgsEqual(existingArgs, newArgs);
116 |             if (!changed)
117 |             {
118 |                 return "Configured successfully"; // nothing to do
119 |             }
120 | 
121 |             // 4) Ensure containers exist and write back minimal changes
122 |             JObject existingRoot;
123 |             if (existingConfig is JObject eo)
124 |                 existingRoot = eo;
125 |             else
126 |                 existingRoot = JObject.FromObject(existingConfig);
127 | 
128 |             existingRoot = ConfigJsonBuilder.ApplyUnityServerToExistingConfig(existingRoot, uvPath, serverSrc, mcpClient);
129 | 
130 |             string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings);
131 | 
132 |             McpConfigFileHelper.WriteAtomicFile(configPath, mergedJson);
133 | 
134 |             try
135 |             {
136 |                 if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath);
137 |                 EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc);
138 |             }
139 |             catch { }
140 | 
141 |             return "Configured successfully";
142 |         }
143 | 
144 |         /// <summary>
145 |         /// Configures a Codex client with sophisticated TOML handling
146 |         /// </summary>
147 |         public static string ConfigureCodexClient(string pythonDir, string configPath, McpClient mcpClient)
148 |         {
149 |             try
150 |             {
151 |                 if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false))
152 |                     return "Skipped (locked)";
153 |             }
154 |             catch { }
155 | 
156 |             string existingToml = string.Empty;
157 |             if (File.Exists(configPath))
158 |             {
159 |                 try
160 |                 {
161 |                     existingToml = File.ReadAllText(configPath);
162 |                 }
163 |                 catch (Exception e)
164 |                 {
165 |                     Debug.LogWarning($"UnityMCP: Failed to read Codex config '{configPath}': {e.Message}");
166 |                     existingToml = string.Empty;
167 |                 }
168 |             }
169 | 
170 |             string existingCommand = null;
171 |             string[] existingArgs = null;
172 |             if (!string.IsNullOrWhiteSpace(existingToml))
173 |             {
174 |                 CodexConfigHelper.TryParseCodexServer(existingToml, out existingCommand, out existingArgs);
175 |             }
176 | 
177 |             string uvPath = ServerInstaller.FindUvPath();
178 |             try
179 |             {
180 |                 var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant();
181 |                 if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand))
182 |                 {
183 |                     uvPath = existingCommand;
184 |                 }
185 |             }
186 |             catch { }
187 | 
188 |             if (uvPath == null)
189 |             {
190 |                 return "UV package manager not found. Please install UV first.";
191 |             }
192 | 
193 |             string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs);
194 |             var newArgs = new[] { "run", "--directory", serverSrc, "server.py" };
195 | 
196 |             bool changed = true;
197 |             if (!string.IsNullOrEmpty(existingCommand) && existingArgs != null)
198 |             {
199 |                 changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal)
200 |                     || !ArgsEqual(existingArgs, newArgs);
201 |             }
202 | 
203 |             if (!changed)
204 |             {
205 |                 return "Configured successfully";
206 |             }
207 | 
208 |             string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, uvPath, serverSrc);
209 | 
210 |             McpConfigFileHelper.WriteAtomicFile(configPath, updatedToml);
211 | 
212 |             try
213 |             {
214 |                 if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath);
215 |                 EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc);
216 |             }
217 |             catch { }
218 | 
219 |             return "Configured successfully";
220 |         }
221 | 
222 |         /// <summary>
223 |         /// Validates UV binary by running --version command
224 |         /// </summary>
225 |         private static bool IsValidUvBinary(string path)
226 |         {
227 |             try
228 |             {
229 |                 if (!File.Exists(path)) return false;
230 |                 var psi = new System.Diagnostics.ProcessStartInfo
231 |                 {
232 |                     FileName = path,
233 |                     Arguments = "--version",
234 |                     UseShellExecute = false,
235 |                     RedirectStandardOutput = true,
236 |                     RedirectStandardError = true,
237 |                     CreateNoWindow = true
238 |                 };
239 |                 using var p = System.Diagnostics.Process.Start(psi);
240 |                 if (p == null) return false;
241 |                 if (!p.WaitForExit(3000)) { try { p.Kill(); } catch { } return false; }
242 |                 if (p.ExitCode != 0) return false;
243 |                 string output = p.StandardOutput.ReadToEnd().Trim();
244 |                 return output.StartsWith("uv ");
245 |             }
246 |             catch { return false; }
247 |         }
248 | 
249 |         /// <summary>
250 |         /// Compares two string arrays for equality
251 |         /// </summary>
252 |         private static bool ArgsEqual(string[] a, string[] b)
253 |         {
254 |             if (a == null || b == null) return a == b;
255 |             if (a.Length != b.Length) return false;
256 |             for (int i = 0; i < a.Length; i++)
257 |             {
258 |                 if (!string.Equals(a[i], b[i], StringComparison.Ordinal)) return false;
259 |             }
260 |             return true;
261 |         }
262 | 
263 |         /// <summary>
264 |         /// Gets the appropriate config file path for the given MCP client based on OS
265 |         /// </summary>
266 |         public static string GetClientConfigPath(McpClient mcpClient)
267 |         {
268 |             if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
269 |             {
270 |                 return mcpClient.windowsConfigPath;
271 |             }
272 |             else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
273 |             {
274 |                 return string.IsNullOrEmpty(mcpClient.macConfigPath)
275 |                     ? mcpClient.linuxConfigPath
276 |                     : mcpClient.macConfigPath;
277 |             }
278 |             else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
279 |             {
280 |                 return mcpClient.linuxConfigPath;
281 |             }
282 |             else
283 |             {
284 |                 return mcpClient.linuxConfigPath; // fallback
285 |             }
286 |         }
287 | 
288 |         /// <summary>
289 |         /// Creates the directory for the config file if it doesn't exist
290 |         /// </summary>
291 |         public static void EnsureConfigDirectoryExists(string configPath)
292 |         {
293 |             Directory.CreateDirectory(Path.GetDirectoryName(configPath));
294 |         }
295 |     }
296 | }
297 | 
```

--------------------------------------------------------------------------------
/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using NUnit.Framework;
  3 | using UnityEditor;
  4 | using MCPForUnity.Editor.Services;
  5 | 
  6 | namespace MCPForUnityTests.Editor.Services
  7 | {
  8 |     public class PackageUpdateServiceTests
  9 |     {
 10 |         private PackageUpdateService _service;
 11 |         private const string TestLastCheckDateKey = "MCPForUnity.LastUpdateCheck";
 12 |         private const string TestCachedVersionKey = "MCPForUnity.LatestKnownVersion";
 13 | 
 14 |         [SetUp]
 15 |         public void SetUp()
 16 |         {
 17 |             _service = new PackageUpdateService();
 18 | 
 19 |             // Clean up any existing test data
 20 |             CleanupEditorPrefs();
 21 |         }
 22 | 
 23 |         [TearDown]
 24 |         public void TearDown()
 25 |         {
 26 |             // Clean up test data
 27 |             CleanupEditorPrefs();
 28 |         }
 29 | 
 30 |         private void CleanupEditorPrefs()
 31 |         {
 32 |             if (EditorPrefs.HasKey(TestLastCheckDateKey))
 33 |             {
 34 |                 EditorPrefs.DeleteKey(TestLastCheckDateKey);
 35 |             }
 36 |             if (EditorPrefs.HasKey(TestCachedVersionKey))
 37 |             {
 38 |                 EditorPrefs.DeleteKey(TestCachedVersionKey);
 39 |             }
 40 |         }
 41 | 
 42 |         [Test]
 43 |         public void IsNewerVersion_ReturnsTrue_WhenMajorVersionIsNewer()
 44 |         {
 45 |             bool result = _service.IsNewerVersion("2.0.0", "1.0.0");
 46 |             Assert.IsTrue(result, "2.0.0 should be newer than 1.0.0");
 47 |         }
 48 | 
 49 |         [Test]
 50 |         public void IsNewerVersion_ReturnsTrue_WhenMinorVersionIsNewer()
 51 |         {
 52 |             bool result = _service.IsNewerVersion("1.2.0", "1.1.0");
 53 |             Assert.IsTrue(result, "1.2.0 should be newer than 1.1.0");
 54 |         }
 55 | 
 56 |         [Test]
 57 |         public void IsNewerVersion_ReturnsTrue_WhenPatchVersionIsNewer()
 58 |         {
 59 |             bool result = _service.IsNewerVersion("1.0.2", "1.0.1");
 60 |             Assert.IsTrue(result, "1.0.2 should be newer than 1.0.1");
 61 |         }
 62 | 
 63 |         [Test]
 64 |         public void IsNewerVersion_ReturnsFalse_WhenVersionsAreEqual()
 65 |         {
 66 |             bool result = _service.IsNewerVersion("1.0.0", "1.0.0");
 67 |             Assert.IsFalse(result, "Same versions should return false");
 68 |         }
 69 | 
 70 |         [Test]
 71 |         public void IsNewerVersion_ReturnsFalse_WhenVersionIsOlder()
 72 |         {
 73 |             bool result = _service.IsNewerVersion("1.0.0", "2.0.0");
 74 |             Assert.IsFalse(result, "1.0.0 should not be newer than 2.0.0");
 75 |         }
 76 | 
 77 |         [Test]
 78 |         public void IsNewerVersion_HandlesVersionPrefix_v()
 79 |         {
 80 |             bool result = _service.IsNewerVersion("v2.0.0", "v1.0.0");
 81 |             Assert.IsTrue(result, "Should handle 'v' prefix correctly");
 82 |         }
 83 | 
 84 |         [Test]
 85 |         public void IsNewerVersion_HandlesVersionPrefix_V()
 86 |         {
 87 |             bool result = _service.IsNewerVersion("V2.0.0", "V1.0.0");
 88 |             Assert.IsTrue(result, "Should handle 'V' prefix correctly");
 89 |         }
 90 | 
 91 |         [Test]
 92 |         public void IsNewerVersion_HandlesMixedPrefixes()
 93 |         {
 94 |             bool result = _service.IsNewerVersion("v2.0.0", "1.0.0");
 95 |             Assert.IsTrue(result, "Should handle mixed prefixes correctly");
 96 |         }
 97 | 
 98 |         [Test]
 99 |         public void IsNewerVersion_ComparesCorrectly_WhenMajorDiffers()
100 |         {
101 |             bool result1 = _service.IsNewerVersion("10.0.0", "9.0.0");
102 |             bool result2 = _service.IsNewerVersion("2.0.0", "10.0.0");
103 | 
104 |             Assert.IsTrue(result1, "10.0.0 should be newer than 9.0.0");
105 |             Assert.IsFalse(result2, "2.0.0 should not be newer than 10.0.0");
106 |         }
107 | 
108 |         [Test]
109 |         public void IsNewerVersion_ReturnsFalse_OnInvalidVersionFormat()
110 |         {
111 |             // Service should handle errors gracefully
112 |             bool result = _service.IsNewerVersion("invalid", "1.0.0");
113 |             Assert.IsFalse(result, "Should return false for invalid version format");
114 |         }
115 | 
116 |         [Test]
117 |         public void CheckForUpdate_ReturnsCachedVersion_WhenCacheIsValid()
118 |         {
119 |             // Arrange: Set up valid cache
120 |             string today = DateTime.Now.ToString("yyyy-MM-dd");
121 |             string cachedVersion = "5.5.5";
122 |             EditorPrefs.SetString(TestLastCheckDateKey, today);
123 |             EditorPrefs.SetString(TestCachedVersionKey, cachedVersion);
124 | 
125 |             // Act
126 |             var result = _service.CheckForUpdate("5.0.0");
127 | 
128 |             // Assert
129 |             Assert.IsTrue(result.CheckSucceeded, "Check should succeed with valid cache");
130 |             Assert.AreEqual(cachedVersion, result.LatestVersion, "Should return cached version");
131 |             Assert.IsTrue(result.UpdateAvailable, "Update should be available (5.5.5 > 5.0.0)");
132 |         }
133 | 
134 |         [Test]
135 |         public void CheckForUpdate_DetectsUpdateAvailable_WhenNewerVersionCached()
136 |         {
137 |             // Arrange
138 |             string today = DateTime.Now.ToString("yyyy-MM-dd");
139 |             EditorPrefs.SetString(TestLastCheckDateKey, today);
140 |             EditorPrefs.SetString(TestCachedVersionKey, "6.0.0");
141 | 
142 |             // Act
143 |             var result = _service.CheckForUpdate("5.0.0");
144 | 
145 |             // Assert
146 |             Assert.IsTrue(result.UpdateAvailable, "Should detect update is available");
147 |             Assert.AreEqual("6.0.0", result.LatestVersion);
148 |         }
149 | 
150 |         [Test]
151 |         public void CheckForUpdate_DetectsNoUpdate_WhenVersionsMatch()
152 |         {
153 |             // Arrange
154 |             string today = DateTime.Now.ToString("yyyy-MM-dd");
155 |             EditorPrefs.SetString(TestLastCheckDateKey, today);
156 |             EditorPrefs.SetString(TestCachedVersionKey, "5.0.0");
157 | 
158 |             // Act
159 |             var result = _service.CheckForUpdate("5.0.0");
160 | 
161 |             // Assert
162 |             Assert.IsFalse(result.UpdateAvailable, "Should detect no update needed");
163 |             Assert.AreEqual("5.0.0", result.LatestVersion);
164 |         }
165 | 
166 |         [Test]
167 |         public void CheckForUpdate_DetectsNoUpdate_WhenCurrentVersionIsNewer()
168 |         {
169 |             // Arrange
170 |             string today = DateTime.Now.ToString("yyyy-MM-dd");
171 |             EditorPrefs.SetString(TestLastCheckDateKey, today);
172 |             EditorPrefs.SetString(TestCachedVersionKey, "5.0.0");
173 | 
174 |             // Act
175 |             var result = _service.CheckForUpdate("6.0.0");
176 | 
177 |             // Assert
178 |             Assert.IsFalse(result.UpdateAvailable, "Should detect no update when current is newer");
179 |             Assert.AreEqual("5.0.0", result.LatestVersion);
180 |         }
181 | 
182 |         [Test]
183 |         public void CheckForUpdate_IgnoresExpiredCache_AndAttemptsFreshFetch()
184 |         {
185 |             // Arrange: Set cache from yesterday (expired)
186 |             string yesterday = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd");
187 |             string cachedVersion = "4.0.0";
188 |             EditorPrefs.SetString(TestLastCheckDateKey, yesterday);
189 |             EditorPrefs.SetString(TestCachedVersionKey, cachedVersion);
190 | 
191 |             // Act
192 |             var result = _service.CheckForUpdate("5.0.0");
193 | 
194 |             // Assert
195 |             Assert.IsNotNull(result, "Should return a result");
196 |             
197 |             // If the check succeeded (network available), verify it didn't use the expired cache
198 |             if (result.CheckSucceeded)
199 |             {
200 |                 Assert.AreNotEqual(cachedVersion, result.LatestVersion, 
201 |                     "Should not return expired cached version when fresh fetch succeeds");
202 |                 Assert.IsNotNull(result.LatestVersion, "Should have fetched a new version");
203 |             }
204 |             else
205 |             {
206 |                 // If offline, check should fail (not succeed with cached data)
207 |                 Assert.IsFalse(result.UpdateAvailable, 
208 |                     "Should not report update available when fetch fails and cache is expired");
209 |             }
210 |         }
211 | 
212 |         [Test]
213 |         public void CheckForUpdate_ReturnsAssetStoreMessage_ForNonGitInstallations()
214 |         {
215 |             // Note: This test verifies the service behavior when IsGitInstallation() returns false.
216 |             // Since the actual result depends on package installation method, we create a mock
217 |             // implementation to test this specific code path.
218 |             
219 |             var mockService = new MockAssetStorePackageUpdateService();
220 |             
221 |             // Act
222 |             var result = mockService.CheckForUpdate("5.0.0");
223 | 
224 |             // Assert
225 |             Assert.IsFalse(result.CheckSucceeded, "Check should not succeed for Asset Store installs");
226 |             Assert.IsFalse(result.UpdateAvailable, "No update should be reported for Asset Store installs");
227 |             Assert.AreEqual("Asset Store installations are updated via Unity Asset Store", result.Message,
228 |                 "Should return Asset Store update message");
229 |             Assert.IsNull(result.LatestVersion, "Latest version should be null for Asset Store installs");
230 |         }
231 | 
232 |         [Test]
233 |         public void ClearCache_RemovesAllCachedData()
234 |         {
235 |             // Arrange: Set up cache
236 |             EditorPrefs.SetString(TestLastCheckDateKey, DateTime.Now.ToString("yyyy-MM-dd"));
237 |             EditorPrefs.SetString(TestCachedVersionKey, "5.0.0");
238 | 
239 |             // Verify cache exists
240 |             Assert.IsTrue(EditorPrefs.HasKey(TestLastCheckDateKey), "Cache should exist before clearing");
241 |             Assert.IsTrue(EditorPrefs.HasKey(TestCachedVersionKey), "Cache should exist before clearing");
242 | 
243 |             // Act
244 |             _service.ClearCache();
245 | 
246 |             // Assert
247 |             Assert.IsFalse(EditorPrefs.HasKey(TestLastCheckDateKey), "Date cache should be cleared");
248 |             Assert.IsFalse(EditorPrefs.HasKey(TestCachedVersionKey), "Version cache should be cleared");
249 |         }
250 | 
251 |         [Test]
252 |         public void ClearCache_DoesNotThrow_WhenNoCacheExists()
253 |         {
254 |             // Ensure no cache exists
255 |             CleanupEditorPrefs();
256 | 
257 |             // Act & Assert - should not throw
258 |             Assert.DoesNotThrow(() => _service.ClearCache(), "Should not throw when clearing non-existent cache");
259 |         }
260 |     }
261 | 
262 |     /// <summary>
263 |     /// Mock implementation of IPackageUpdateService that simulates Asset Store installation behavior
264 |     /// </summary>
265 |     internal class MockAssetStorePackageUpdateService : IPackageUpdateService
266 |     {
267 |         public UpdateCheckResult CheckForUpdate(string currentVersion)
268 |         {
269 |             // Simulate Asset Store installation (IsGitInstallation returns false)
270 |             return new UpdateCheckResult
271 |             {
272 |                 CheckSucceeded = false,
273 |                 UpdateAvailable = false,
274 |                 Message = "Asset Store installations are updated via Unity Asset Store"
275 |             };
276 |         }
277 | 
278 |         public bool IsNewerVersion(string version1, string version2)
279 |         {
280 |             // Not used in the Asset Store test, but required by interface
281 |             return false;
282 |         }
283 | 
284 |         public bool IsGitInstallation()
285 |         {
286 |             // Simulate non-Git installation (Asset Store)
287 |             return false;
288 |         }
289 | 
290 |         public void ClearCache()
291 |         {
292 |             // Not used in the Asset Store test, but required by interface
293 |         }
294 |     }
295 | }
296 | 
```

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

```csharp
  1 | using System;
  2 | using System.IO;
  3 | using System.Linq;
  4 | using System.Runtime.InteropServices;
  5 | using Newtonsoft.Json;
  6 | using Newtonsoft.Json.Linq;
  7 | using UnityEditor;
  8 | using UnityEngine;
  9 | using MCPForUnity.Editor.Dependencies;
 10 | using MCPForUnity.Editor.Helpers;
 11 | using MCPForUnity.Editor.Models;
 12 | 
 13 | namespace MCPForUnity.Editor.Helpers
 14 | {
 15 |     /// <summary>
 16 |     /// Shared helper for MCP client configuration management with sophisticated
 17 |     /// logic for preserving existing configs and handling different client types
 18 |     /// </summary>
 19 |     public static class McpConfigurationHelper
 20 |     {
 21 |         private const string LOCK_CONFIG_KEY = "MCPForUnity.LockCursorConfig";
 22 | 
 23 |         /// <summary>
 24 |         /// Writes MCP configuration to the specified path using sophisticated logic
 25 |         /// that preserves existing configuration and only writes when necessary
 26 |         /// </summary>
 27 |         public static string WriteMcpConfiguration(string pythonDir, string configPath, McpClient mcpClient = null)
 28 |         {
 29 |             // 0) Respect explicit lock (hidden pref or UI toggle)
 30 |             try
 31 |             {
 32 |                 if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false))
 33 |                     return "Skipped (locked)";
 34 |             }
 35 |             catch { }
 36 | 
 37 |             JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented };
 38 | 
 39 |             // Read existing config if it exists
 40 |             string existingJson = "{}";
 41 |             if (File.Exists(configPath))
 42 |             {
 43 |                 try
 44 |                 {
 45 |                     existingJson = File.ReadAllText(configPath);
 46 |                 }
 47 |                 catch (Exception e)
 48 |                 {
 49 |                     Debug.LogWarning($"Error reading existing config: {e.Message}.");
 50 |                 }
 51 |             }
 52 | 
 53 |             // Parse the existing JSON while preserving all properties
 54 |             dynamic existingConfig;
 55 |             try
 56 |             {
 57 |                 if (string.IsNullOrWhiteSpace(existingJson))
 58 |                 {
 59 |                     existingConfig = new JObject();
 60 |                 }
 61 |                 else
 62 |                 {
 63 |                     existingConfig = JsonConvert.DeserializeObject(existingJson) ?? new JObject();
 64 |                 }
 65 |             }
 66 |             catch
 67 |             {
 68 |                 // If user has partial/invalid JSON (e.g., mid-edit), start from a fresh object
 69 |                 if (!string.IsNullOrWhiteSpace(existingJson))
 70 |                 {
 71 |                     Debug.LogWarning("UnityMCP: Configuration file could not be parsed; rewriting server block.");
 72 |                 }
 73 |                 existingConfig = new JObject();
 74 |             }
 75 | 
 76 |             // Determine existing entry references (command/args)
 77 |             string existingCommand = null;
 78 |             string[] existingArgs = null;
 79 |             bool isVSCode = (mcpClient?.mcpType == McpTypes.VSCode);
 80 |             try
 81 |             {
 82 |                 if (isVSCode)
 83 |                 {
 84 |                     existingCommand = existingConfig?.servers?.unityMCP?.command?.ToString();
 85 |                     existingArgs = existingConfig?.servers?.unityMCP?.args?.ToObject<string[]>();
 86 |                 }
 87 |                 else
 88 |                 {
 89 |                     existingCommand = existingConfig?.mcpServers?.unityMCP?.command?.ToString();
 90 |                     existingArgs = existingConfig?.mcpServers?.unityMCP?.args?.ToObject<string[]>();
 91 |                 }
 92 |             }
 93 |             catch { }
 94 | 
 95 |             // 1) Start from existing, only fill gaps (prefer trusted resolver)
 96 |             string uvPath = ServerInstaller.FindUvPath();
 97 |             // Optionally trust existingCommand if it looks like uv/uv.exe
 98 |             try
 99 |             {
100 |                 var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant();
101 |                 if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand))
102 |                 {
103 |                     uvPath = existingCommand;
104 |                 }
105 |             }
106 |             catch { }
107 |             if (uvPath == null) return "UV package manager not found. Please install UV first.";
108 |             string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs);
109 | 
110 |             // 2) Canonical args order
111 |             var newArgs = new[] { "run", "--directory", serverSrc, "server.py" };
112 | 
113 |             // 3) Only write if changed
114 |             bool changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal)
115 |                 || !ArgsEqual(existingArgs, newArgs);
116 |             if (!changed)
117 |             {
118 |                 return "Configured successfully"; // nothing to do
119 |             }
120 | 
121 |             // 4) Ensure containers exist and write back minimal changes
122 |             JObject existingRoot;
123 |             if (existingConfig is JObject eo)
124 |                 existingRoot = eo;
125 |             else
126 |                 existingRoot = JObject.FromObject(existingConfig);
127 | 
128 |             existingRoot = ConfigJsonBuilder.ApplyUnityServerToExistingConfig(existingRoot, uvPath, serverSrc, mcpClient);
129 | 
130 |             string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings);
131 | 
132 |             McpConfigFileHelper.WriteAtomicFile(configPath, mergedJson);
133 | 
134 |             try
135 |             {
136 |                 if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath);
137 |                 EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc);
138 |             }
139 |             catch { }
140 | 
141 |             return "Configured successfully";
142 |         }
143 | 
144 |         /// <summary>
145 |         /// Configures a Codex client with sophisticated TOML handling
146 |         /// </summary>
147 |         public static string ConfigureCodexClient(string pythonDir, string configPath, McpClient mcpClient)
148 |         {
149 |             try
150 |             {
151 |                 if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false))
152 |                     return "Skipped (locked)";
153 |             }
154 |             catch { }
155 | 
156 |             string existingToml = string.Empty;
157 |             if (File.Exists(configPath))
158 |             {
159 |                 try
160 |                 {
161 |                     existingToml = File.ReadAllText(configPath);
162 |                 }
163 |                 catch (Exception e)
164 |                 {
165 |                     Debug.LogWarning($"UnityMCP: Failed to read Codex config '{configPath}': {e.Message}");
166 |                     existingToml = string.Empty;
167 |                 }
168 |             }
169 | 
170 |             string existingCommand = null;
171 |             string[] existingArgs = null;
172 |             if (!string.IsNullOrWhiteSpace(existingToml))
173 |             {
174 |                 CodexConfigHelper.TryParseCodexServer(existingToml, out existingCommand, out existingArgs);
175 |             }
176 | 
177 |             string uvPath = ServerInstaller.FindUvPath();
178 |             try
179 |             {
180 |                 var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant();
181 |                 if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand))
182 |                 {
183 |                     uvPath = existingCommand;
184 |                 }
185 |             }
186 |             catch { }
187 | 
188 |             if (uvPath == null)
189 |             {
190 |                 return "UV package manager not found. Please install UV first.";
191 |             }
192 | 
193 |             string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs);
194 |             var newArgs = new[] { "run", "--directory", serverSrc, "server.py" };
195 | 
196 |             bool changed = true;
197 |             if (!string.IsNullOrEmpty(existingCommand) && existingArgs != null)
198 |             {
199 |                 changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal)
200 |                     || !ArgsEqual(existingArgs, newArgs);
201 |             }
202 | 
203 |             if (!changed)
204 |             {
205 |                 return "Configured successfully";
206 |             }
207 | 
208 |             string codexBlock = CodexConfigHelper.BuildCodexServerBlock(uvPath, serverSrc);
209 |             string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, codexBlock);
210 | 
211 |             McpConfigFileHelper.WriteAtomicFile(configPath, updatedToml);
212 | 
213 |             try
214 |             {
215 |                 if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath);
216 |                 EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc);
217 |             }
218 |             catch { }
219 | 
220 |             return "Configured successfully";
221 |         }
222 | 
223 |         /// <summary>
224 |         /// Validates UV binary by running --version command
225 |         /// </summary>
226 |         private static bool IsValidUvBinary(string path)
227 |         {
228 |             try
229 |             {
230 |                 if (!File.Exists(path)) return false;
231 |                 var psi = new System.Diagnostics.ProcessStartInfo
232 |                 {
233 |                     FileName = path,
234 |                     Arguments = "--version",
235 |                     UseShellExecute = false,
236 |                     RedirectStandardOutput = true,
237 |                     RedirectStandardError = true,
238 |                     CreateNoWindow = true
239 |                 };
240 |                 using var p = System.Diagnostics.Process.Start(psi);
241 |                 if (p == null) return false;
242 |                 if (!p.WaitForExit(3000)) { try { p.Kill(); } catch { } return false; }
243 |                 if (p.ExitCode != 0) return false;
244 |                 string output = p.StandardOutput.ReadToEnd().Trim();
245 |                 return output.StartsWith("uv ");
246 |             }
247 |             catch { return false; }
248 |         }
249 | 
250 |         /// <summary>
251 |         /// Compares two string arrays for equality
252 |         /// </summary>
253 |         private static bool ArgsEqual(string[] a, string[] b)
254 |         {
255 |             if (a == null || b == null) return a == b;
256 |             if (a.Length != b.Length) return false;
257 |             for (int i = 0; i < a.Length; i++)
258 |             {
259 |                 if (!string.Equals(a[i], b[i], StringComparison.Ordinal)) return false;
260 |             }
261 |             return true;
262 |         }
263 | 
264 |         /// <summary>
265 |         /// Gets the appropriate config file path for the given MCP client based on OS
266 |         /// </summary>
267 |         public static string GetClientConfigPath(McpClient mcpClient)
268 |         {
269 |             if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
270 |             {
271 |                 return mcpClient.windowsConfigPath;
272 |             }
273 |             else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
274 |             {
275 |                 return string.IsNullOrEmpty(mcpClient.macConfigPath)
276 |                     ? mcpClient.linuxConfigPath
277 |                     : mcpClient.macConfigPath;
278 |             }
279 |             else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
280 |             {
281 |                 return mcpClient.linuxConfigPath;
282 |             }
283 |             else
284 |             {
285 |                 return mcpClient.linuxConfigPath; // fallback
286 |             }
287 |         }
288 | 
289 |         /// <summary>
290 |         /// Creates the directory for the config file if it doesn't exist
291 |         /// </summary>
292 |         public static void EnsureConfigDirectoryExists(string configPath)
293 |         {
294 |             Directory.CreateDirectory(Path.GetDirectoryName(configPath));
295 |         }
296 |     }
297 | }
298 | 
```

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

```csharp
  1 | using System;
  2 | using System.Diagnostics;
  3 | using System.IO;
  4 | using System.Linq;
  5 | using System.Text;
  6 | using System.Runtime.InteropServices;
  7 | using UnityEditor;
  8 | 
  9 | namespace MCPForUnity.Editor.Helpers
 10 | {
 11 |     internal static class ExecPath
 12 |     {
 13 |         private const string PrefClaude = "MCPForUnity.ClaudeCliPath";
 14 | 
 15 |         // Resolve Claude CLI absolute path. Pref → env → common locations → PATH.
 16 |         internal static string ResolveClaude()
 17 |         {
 18 |             try
 19 |             {
 20 |                 string pref = EditorPrefs.GetString(PrefClaude, string.Empty);
 21 |                 if (!string.IsNullOrEmpty(pref) && File.Exists(pref)) return pref;
 22 |             }
 23 |             catch { }
 24 | 
 25 |             string env = Environment.GetEnvironmentVariable("CLAUDE_CLI");
 26 |             if (!string.IsNullOrEmpty(env) && File.Exists(env)) return env;
 27 | 
 28 |             if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
 29 |             {
 30 |                 string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
 31 |                 string[] candidates =
 32 |                 {
 33 |                     "/opt/homebrew/bin/claude",
 34 |                     "/usr/local/bin/claude",
 35 |                     Path.Combine(home, ".local", "bin", "claude"),
 36 |                 };
 37 |                 foreach (string c in candidates) { if (File.Exists(c)) return c; }
 38 |                 // Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude
 39 |                 string nvmClaude = ResolveClaudeFromNvm(home);
 40 |                 if (!string.IsNullOrEmpty(nvmClaude)) return nvmClaude;
 41 | #if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
 42 |                 return Which("claude", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin");
 43 | #else
 44 |                 return null;
 45 | #endif
 46 |             }
 47 | 
 48 |             if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
 49 |             {
 50 | #if UNITY_EDITOR_WIN
 51 |                 // Common npm global locations
 52 |                 string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
 53 |                 string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
 54 |                 string[] candidates =
 55 |                 {
 56 |                     // Prefer .cmd (most reliable from non-interactive processes)
 57 |                     Path.Combine(appData, "npm", "claude.cmd"),
 58 |                     Path.Combine(localAppData, "npm", "claude.cmd"),
 59 |                     // Fall back to PowerShell shim if only .ps1 is present
 60 |                     Path.Combine(appData, "npm", "claude.ps1"),
 61 |                     Path.Combine(localAppData, "npm", "claude.ps1"),
 62 |                 };
 63 |                 foreach (string c in candidates) { if (File.Exists(c)) return c; }
 64 |                 string fromWhere = Where("claude.exe") ?? Where("claude.cmd") ?? Where("claude.ps1") ?? Where("claude");
 65 |                 if (!string.IsNullOrEmpty(fromWhere)) return fromWhere;
 66 | #endif
 67 |                 return null;
 68 |             }
 69 | 
 70 |             // Linux
 71 |             {
 72 |                 string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
 73 |                 string[] candidates =
 74 |                 {
 75 |                     "/usr/local/bin/claude",
 76 |                     "/usr/bin/claude",
 77 |                     Path.Combine(home, ".local", "bin", "claude"),
 78 |                 };
 79 |                 foreach (string c in candidates) { if (File.Exists(c)) return c; }
 80 |                 // Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude
 81 |                 string nvmClaude = ResolveClaudeFromNvm(home);
 82 |                 if (!string.IsNullOrEmpty(nvmClaude)) return nvmClaude;
 83 | #if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
 84 |                 return Which("claude", "/usr/local/bin:/usr/bin:/bin");
 85 | #else
 86 |                 return null;
 87 | #endif
 88 |             }
 89 |         }
 90 | 
 91 |         // Attempt to resolve claude from NVM-managed Node installations, choosing the newest version
 92 |         private static string ResolveClaudeFromNvm(string home)
 93 |         {
 94 |             try
 95 |             {
 96 |                 if (string.IsNullOrEmpty(home)) return null;
 97 |                 string nvmNodeDir = Path.Combine(home, ".nvm", "versions", "node");
 98 |                 if (!Directory.Exists(nvmNodeDir)) return null;
 99 | 
100 |                 string bestPath = null;
101 |                 Version bestVersion = null;
102 |                 foreach (string versionDir in Directory.EnumerateDirectories(nvmNodeDir))
103 |                 {
104 |                     string name = Path.GetFileName(versionDir);
105 |                     if (string.IsNullOrEmpty(name)) continue;
106 |                     if (name.StartsWith("v", StringComparison.OrdinalIgnoreCase))
107 |                     {
108 |                         // Extract numeric portion: e.g., v18.19.0-nightly -> 18.19.0
109 |                         string versionStr = name.Substring(1);
110 |                         int dashIndex = versionStr.IndexOf('-');
111 |                         if (dashIndex > 0)
112 |                         {
113 |                             versionStr = versionStr.Substring(0, dashIndex);
114 |                         }
115 |                         if (Version.TryParse(versionStr, out Version parsed))
116 |                         {
117 |                             string candidate = Path.Combine(versionDir, "bin", "claude");
118 |                             if (File.Exists(candidate))
119 |                             {
120 |                                 if (bestVersion == null || parsed > bestVersion)
121 |                                 {
122 |                                     bestVersion = parsed;
123 |                                     bestPath = candidate;
124 |                                 }
125 |                             }
126 |                         }
127 |                     }
128 |                 }
129 |                 return bestPath;
130 |             }
131 |             catch { return null; }
132 |         }
133 | 
134 |         // Explicitly set the Claude CLI absolute path override in EditorPrefs
135 |         internal static void SetClaudeCliPath(string absolutePath)
136 |         {
137 |             try
138 |             {
139 |                 if (!string.IsNullOrEmpty(absolutePath) && File.Exists(absolutePath))
140 |                 {
141 |                     EditorPrefs.SetString(PrefClaude, absolutePath);
142 |                 }
143 |             }
144 |             catch { }
145 |         }
146 | 
147 |         // Clear any previously set Claude CLI override path
148 |         internal static void ClearClaudeCliPath()
149 |         {
150 |             try
151 |             {
152 |                 if (EditorPrefs.HasKey(PrefClaude))
153 |                 {
154 |                     EditorPrefs.DeleteKey(PrefClaude);
155 |                 }
156 |             }
157 |             catch { }
158 |         }
159 | 
160 |         // Use existing UV resolver; returns absolute path or null.
161 |         internal static string ResolveUv()
162 |         {
163 |             return ServerInstaller.FindUvPath();
164 |         }
165 | 
166 |         internal static bool TryRun(
167 |             string file,
168 |             string args,
169 |             string workingDir,
170 |             out string stdout,
171 |             out string stderr,
172 |             int timeoutMs = 15000,
173 |             string extraPathPrepend = null)
174 |         {
175 |             stdout = string.Empty;
176 |             stderr = string.Empty;
177 |             try
178 |             {
179 |                 // Handle PowerShell scripts on Windows by invoking through powershell.exe
180 |                 bool isPs1 = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
181 |                              file.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase);
182 | 
183 |                 var psi = new ProcessStartInfo
184 |                 {
185 |                     FileName = isPs1 ? "powershell.exe" : file,
186 |                     Arguments = isPs1
187 |                         ? $"-NoProfile -ExecutionPolicy Bypass -File \"{file}\" {args}".Trim()
188 |                         : args,
189 |                     WorkingDirectory = string.IsNullOrEmpty(workingDir) ? Environment.CurrentDirectory : workingDir,
190 |                     UseShellExecute = false,
191 |                     RedirectStandardOutput = true,
192 |                     RedirectStandardError = true,
193 |                     CreateNoWindow = true,
194 |                 };
195 |                 if (!string.IsNullOrEmpty(extraPathPrepend))
196 |                 {
197 |                     string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
198 |                     psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath)
199 |                         ? extraPathPrepend
200 |                         : (extraPathPrepend + System.IO.Path.PathSeparator + currentPath);
201 |                 }
202 | 
203 |                 using var process = new Process { StartInfo = psi, EnableRaisingEvents = false };
204 | 
205 |                 var so = new StringBuilder();
206 |                 var se = new StringBuilder();
207 |                 process.OutputDataReceived += (_, e) => { if (e.Data != null) so.AppendLine(e.Data); };
208 |                 process.ErrorDataReceived += (_, e) => { if (e.Data != null) se.AppendLine(e.Data); };
209 | 
210 |                 if (!process.Start()) return false;
211 | 
212 |                 process.BeginOutputReadLine();
213 |                 process.BeginErrorReadLine();
214 | 
215 |                 if (!process.WaitForExit(timeoutMs))
216 |                 {
217 |                     try { process.Kill(); } catch { }
218 |                     return false;
219 |                 }
220 | 
221 |                 // Ensure async buffers are flushed
222 |                 process.WaitForExit();
223 | 
224 |                 stdout = so.ToString();
225 |                 stderr = se.ToString();
226 |                 return process.ExitCode == 0;
227 |             }
228 |             catch
229 |             {
230 |                 return false;
231 |             }
232 |         }
233 | 
234 | #if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
235 |         private static string Which(string exe, string prependPath)
236 |         {
237 |             try
238 |             {
239 |                 var psi = new ProcessStartInfo("/usr/bin/which", exe)
240 |                 {
241 |                     UseShellExecute = false,
242 |                     RedirectStandardOutput = true,
243 |                     CreateNoWindow = true,
244 |                 };
245 |                 string path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
246 |                 psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(path) ? prependPath : (prependPath + Path.PathSeparator + path);
247 |                 using var p = Process.Start(psi);
248 |                 string output = p?.StandardOutput.ReadToEnd().Trim();
249 |                 p?.WaitForExit(1500);
250 |                 return (!string.IsNullOrEmpty(output) && File.Exists(output)) ? output : null;
251 |             }
252 |             catch { return null; }
253 |         }
254 | #endif
255 | 
256 | #if UNITY_EDITOR_WIN
257 |         private static string Where(string exe)
258 |         {
259 |             try
260 |             {
261 |                 var psi = new ProcessStartInfo("where", exe)
262 |                 {
263 |                     UseShellExecute = false,
264 |                     RedirectStandardOutput = true,
265 |                     CreateNoWindow = true,
266 |                 };
267 |                 using var p = Process.Start(psi);
268 |                 string first = p?.StandardOutput.ReadToEnd()
269 |                     .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
270 |                     .FirstOrDefault();
271 |                 p?.WaitForExit(1500);
272 |                 return (!string.IsNullOrEmpty(first) && File.Exists(first)) ? first : null;
273 |             }
274 |             catch { return null; }
275 |         }
276 | #endif
277 |     }
278 | }
279 | 
```
Page 5/18FirstPrevNextLast