#
tokens: 49304/50000 21/263 files (page 4/13)
lines: off (toggle) GitHub
raw markdown copy
This is page 4 of 13. Use http://codebase.md/justinpbarnett/unity-mcp?page={x} to view the full context.

# Directory Structure

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

# Files

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

```csharp
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEditor;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;

namespace MCPForUnityTests.Editor.Helpers
{
    public class WriteToConfigTests
    {
        private string _tempRoot;
        private string _fakeUvPath;
        private string _serverSrcDir;

        [SetUp]
        public void SetUp()
        {
            // Tests are designed for Linux/macOS runners. Skip on Windows due to ProcessStartInfo
            // restrictions when UseShellExecute=false for .cmd/.bat scripts.
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                Assert.Ignore("WriteToConfig tests are skipped on Windows (CI runs linux).\n" +
                              "ValidateUvBinarySafe requires launching an actual exe on Windows.");
            }
            _tempRoot = Path.Combine(Path.GetTempPath(), "UnityMCPTests", Guid.NewGuid().ToString("N"));
            Directory.CreateDirectory(_tempRoot);

            // Create a fake uv executable that prints a valid version string
            _fakeUvPath = Path.Combine(_tempRoot, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "uv.cmd" : "uv");
            File.WriteAllText(_fakeUvPath, "#!/bin/sh\n\necho 'uv 9.9.9'\n");
            TryChmodX(_fakeUvPath);

            // Create a fake server directory with server.py
            _serverSrcDir = Path.Combine(_tempRoot, "server-src");
            Directory.CreateDirectory(_serverSrcDir);
            File.WriteAllText(Path.Combine(_serverSrcDir, "server.py"), "# dummy server\n");

            // Point the editor to our server dir (so ResolveServerSrc() uses this)
            EditorPrefs.SetString("MCPForUnity.ServerSrc", _serverSrcDir);
            // Ensure no lock is enabled
            EditorPrefs.SetBool("MCPForUnity.LockCursorConfig", false);
            // Disable auto-registration to avoid hitting user configs during tests
            EditorPrefs.SetBool("MCPForUnity.AutoRegisterEnabled", false);
        }

        [TearDown]
        public void TearDown()
        {
            // Clean up editor preferences set during SetUp
            EditorPrefs.DeleteKey("MCPForUnity.ServerSrc");
            EditorPrefs.DeleteKey("MCPForUnity.LockCursorConfig");
            EditorPrefs.DeleteKey("MCPForUnity.AutoRegisterEnabled");

            // Remove temp files
            try { if (Directory.Exists(_tempRoot)) Directory.Delete(_tempRoot, true); } catch { }
        }

        // --- Tests ---

        [Test]
        public void AddsEnvAndDisabledFalse_ForWindsurf()
        {
            var configPath = Path.Combine(_tempRoot, "windsurf.json");
            WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");

            var client = new McpClient { name = "Windsurf", mcpType = McpTypes.Windsurf };
            InvokeWriteToConfig(configPath, client);

            var root = JObject.Parse(File.ReadAllText(configPath));
            var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
            Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
            Assert.NotNull(unity["env"], "env should be present for all clients");
            Assert.IsTrue(unity["env"]!.Type == JTokenType.Object, "env should be an object");
            Assert.AreEqual(false, (bool)unity["disabled"], "disabled:false should be set for Windsurf when missing");
        }

        [Test]
        public void AddsEnvAndDisabledFalse_ForKiro()
        {
            var configPath = Path.Combine(_tempRoot, "kiro.json");
            WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");

            var client = new McpClient { name = "Kiro", mcpType = McpTypes.Kiro };
            InvokeWriteToConfig(configPath, client);

            var root = JObject.Parse(File.ReadAllText(configPath));
            var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
            Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
            Assert.NotNull(unity["env"], "env should be present for all clients");
            Assert.IsTrue(unity["env"]!.Type == JTokenType.Object, "env should be an object");
            Assert.AreEqual(false, (bool)unity["disabled"], "disabled:false should be set for Kiro when missing");
        }

        [Test]
        public void DoesNotAddEnvOrDisabled_ForCursor()
        {
            var configPath = Path.Combine(_tempRoot, "cursor.json");
            WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");

            var client = new McpClient { name = "Cursor", mcpType = McpTypes.Cursor };
            InvokeWriteToConfig(configPath, client);

            var root = JObject.Parse(File.ReadAllText(configPath));
            var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
            Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
            Assert.IsNull(unity["env"], "env should not be added for non-Windsurf/Kiro clients");
            Assert.IsNull(unity["disabled"], "disabled should not be added for non-Windsurf/Kiro clients");
        }

        [Test]
        public void DoesNotAddEnvOrDisabled_ForVSCode()
        {
            var configPath = Path.Combine(_tempRoot, "vscode.json");
            WriteInitialConfig(configPath, isVSCode: true, command: _fakeUvPath, directory: "/old/path");

            var client = new McpClient { name = "VSCode", mcpType = McpTypes.VSCode };
            InvokeWriteToConfig(configPath, client);

            var root = JObject.Parse(File.ReadAllText(configPath));
            var unity = (JObject)root.SelectToken("servers.unityMCP");
            Assert.NotNull(unity, "Expected servers.unityMCP node");
            Assert.IsNull(unity["env"], "env should not be added for VSCode client");
            Assert.IsNull(unity["disabled"], "disabled should not be added for VSCode client");
            Assert.AreEqual("stdio", (string)unity["type"], "VSCode entry should include type=stdio");
        }

        [Test]
        public void PreservesExistingEnvAndDisabled()
        {
            var configPath = Path.Combine(_tempRoot, "preserve.json");

            // Existing config with env and disabled=true should be preserved
            var json = new JObject
            {
                ["mcpServers"] = new JObject
                {
                    ["unityMCP"] = new JObject
                    {
                        ["command"] = _fakeUvPath,
                        ["args"] = new JArray("run", "--directory", "/old/path", "server.py"),
                        ["env"] = new JObject { ["FOO"] = "bar" },
                        ["disabled"] = true
                    }
                }
            };
            File.WriteAllText(configPath, json.ToString());

            var client = new McpClient { name = "Windsurf", mcpType = McpTypes.Windsurf };
            InvokeWriteToConfig(configPath, client);

            var root = JObject.Parse(File.ReadAllText(configPath));
            var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
            Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
            Assert.AreEqual("bar", (string)unity["env"]!["FOO"], "Existing env should be preserved");
            Assert.AreEqual(true, (bool)unity["disabled"], "Existing disabled value should be preserved");
        }

        // --- Helpers ---

        private static void TryChmodX(string path)
        {
            try
            {
                var psi = new ProcessStartInfo
                {
                    FileName = "/bin/chmod",
                    Arguments = "+x \"" + path + "\"",
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    CreateNoWindow = true
                };
                using var p = Process.Start(psi);
                p?.WaitForExit(2000);
            }
            catch { /* best-effort on non-Unix */ }
        }

        private static void WriteInitialConfig(string configPath, bool isVSCode, string command, string directory)
        {
            Directory.CreateDirectory(Path.GetDirectoryName(configPath)!);
            JObject root;
            if (isVSCode)
            {
                root = new JObject
                {
                    ["servers"] = new JObject
                    {
                        ["unityMCP"] = new JObject
                        {
                            ["command"] = command,
                            ["args"] = new JArray("run", "--directory", directory, "server.py"),
                            ["type"] = "stdio"
                        }
                    }
                };
            }
            else
            {
                root = new JObject
                {
                    ["mcpServers"] = new JObject
                    {
                        ["unityMCP"] = new JObject
                        {
                            ["command"] = command,
                            ["args"] = new JArray("run", "--directory", directory, "server.py")
                        }
                    }
                };
            }
            File.WriteAllText(configPath, root.ToString());
        }

        private static void InvokeWriteToConfig(string configPath, McpClient client)
        {
            var result = McpConfigurationHelper.WriteMcpConfiguration(
                pythonDir: string.Empty,
                configPath: configPath,
                mcpClient: client
            );

            Assert.AreEqual("Configured successfully", result, "WriteMcpConfiguration should return success");
        }
    }
}

```

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

```csharp
using System.Runtime.InteropServices;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Models;

namespace MCPForUnity.Editor.Windows
{
    public class VSCodeManualSetupWindow : ManualConfigEditorWindow
    {
        public static void ShowWindow(string configPath, string configJson)
        {
            var window = GetWindow<VSCodeManualSetupWindow>("VSCode GitHub Copilot Setup");
            window.configPath = configPath;
            window.configJson = configJson;
            window.minSize = new Vector2(550, 500);

            // Create a McpClient for VSCode
            window.mcpClient = new McpClient
            {
                name = "VSCode GitHub Copilot",
                mcpType = McpTypes.VSCode
            };

            window.Show();
        }

        protected override void OnGUI()
        {
            scrollPos = EditorGUILayout.BeginScrollView(scrollPos);

            // Header with improved styling
            EditorGUILayout.Space(10);
            Rect titleRect = EditorGUILayout.GetControlRect(false, 30);
            EditorGUI.DrawRect(
                new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height),
                new Color(0.2f, 0.2f, 0.2f, 0.1f)
            );
            GUI.Label(
                new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height),
                "VSCode GitHub Copilot MCP Setup",
                EditorStyles.boldLabel
            );
            EditorGUILayout.Space(10);

            // Instructions with improved styling
            EditorGUILayout.BeginVertical(EditorStyles.helpBox);

            Rect headerRect = EditorGUILayout.GetControlRect(false, 24);
            EditorGUI.DrawRect(
                new Rect(headerRect.x, headerRect.y, headerRect.width, headerRect.height),
                new Color(0.1f, 0.1f, 0.1f, 0.2f)
            );
            GUI.Label(
                new Rect(
                    headerRect.x + 8,
                    headerRect.y + 4,
                    headerRect.width - 16,
                    headerRect.height
                ),
                "Setting up GitHub Copilot in VSCode with MCP for Unity",
                EditorStyles.boldLabel
            );
            EditorGUILayout.Space(10);

            GUIStyle instructionStyle = new(EditorStyles.wordWrappedLabel)
            {
                margin = new RectOffset(10, 10, 5, 5),
            };

            EditorGUILayout.LabelField(
                "1. Prerequisites",
                EditorStyles.boldLabel
            );
            EditorGUILayout.LabelField(
                "• Ensure you have VSCode installed",
                instructionStyle
            );
            EditorGUILayout.LabelField(
                "• Ensure you have GitHub Copilot extension installed in VSCode",
                instructionStyle
            );
            EditorGUILayout.LabelField(
                "• Ensure you have a valid GitHub Copilot subscription",
                instructionStyle
            );
            EditorGUILayout.Space(5);

            EditorGUILayout.LabelField(
                "2. Steps to Configure",
                EditorStyles.boldLabel
            );
            EditorGUILayout.LabelField(
                "a) Open or create your VSCode MCP config file (mcp.json) at the path below",
                instructionStyle
            );
            EditorGUILayout.LabelField(
                "b) Paste the JSON shown below into mcp.json",
                instructionStyle
            );
            EditorGUILayout.LabelField(
                "c) Save the file and restart VSCode",
                instructionStyle
            );
            EditorGUILayout.Space(5);

            EditorGUILayout.LabelField(
                "3. VSCode mcp.json location:",
                EditorStyles.boldLabel
            );

            // Path section with improved styling
            EditorGUILayout.BeginVertical(EditorStyles.helpBox);
            string displayPath;
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                displayPath = System.IO.Path.Combine(
                    System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData),
                    "Code",
                    "User",
                    "mcp.json"
                );
            }
            else
            {
                displayPath = System.IO.Path.Combine(
                    System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile),
                    "Library",
                    "Application Support",
                    "Code",
                    "User",
                    "mcp.json"
                );
            }

            // Store the path in the base class config path
            if (string.IsNullOrEmpty(configPath))
            {
                configPath = displayPath;
            }

            // Prevent text overflow by allowing the text field to wrap
            GUIStyle pathStyle = new(EditorStyles.textField) { wordWrap = true };

            EditorGUILayout.TextField(
                displayPath,
                pathStyle,
                GUILayout.Height(EditorGUIUtility.singleLineHeight)
            );

            // Copy button with improved styling
            EditorGUILayout.BeginHorizontal();
            GUILayout.FlexibleSpace();
            GUIStyle copyButtonStyle = new(GUI.skin.button)
            {
                padding = new RectOffset(15, 15, 5, 5),
                margin = new RectOffset(10, 10, 5, 5),
            };

            if (
                GUILayout.Button(
                    "Copy Path",
                    copyButtonStyle,
                    GUILayout.Height(25),
                    GUILayout.Width(100)
                )
            )
            {
                EditorGUIUtility.systemCopyBuffer = displayPath;
                pathCopied = true;
                copyFeedbackTimer = 2f;
            }

            if (
                GUILayout.Button(
                    "Open File",
                    copyButtonStyle,
                    GUILayout.Height(25),
                    GUILayout.Width(100)
                )
            )
            {
                // Open the file using the system's default application
                System.Diagnostics.Process.Start(
                    new System.Diagnostics.ProcessStartInfo
                    {
                        FileName = displayPath,
                        UseShellExecute = true,
                    }
                );
            }

            if (pathCopied)
            {
                GUIStyle feedbackStyle = new(EditorStyles.label);
                feedbackStyle.normal.textColor = Color.green;
                EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60));
            }

            EditorGUILayout.EndHorizontal();
            EditorGUILayout.EndVertical();
            EditorGUILayout.Space(10);

            EditorGUILayout.LabelField(
                "4. Add this configuration to your mcp.json:",
                EditorStyles.boldLabel
            );

            // JSON section with improved styling
            EditorGUILayout.BeginVertical(EditorStyles.helpBox);

            // Improved text area for JSON with syntax highlighting colors
            GUIStyle jsonStyle = new(EditorStyles.textArea)
            {
                font = EditorStyles.boldFont,
                wordWrap = true,
            };
            jsonStyle.normal.textColor = new Color(0.3f, 0.6f, 0.9f); // Syntax highlighting blue

            // Draw the JSON in a text area with a taller height for better readability
            EditorGUILayout.TextArea(configJson, jsonStyle, GUILayout.Height(200));

            // Copy JSON button with improved styling
            EditorGUILayout.BeginHorizontal();
            GUILayout.FlexibleSpace();

            if (
                GUILayout.Button(
                    "Copy JSON",
                    copyButtonStyle,
                    GUILayout.Height(25),
                    GUILayout.Width(100)
                )
            )
            {
                EditorGUIUtility.systemCopyBuffer = configJson;
                jsonCopied = true;
                copyFeedbackTimer = 2f;
            }

            if (jsonCopied)
            {
                GUIStyle feedbackStyle = new(EditorStyles.label);
                feedbackStyle.normal.textColor = Color.green;
                EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60));
            }

            EditorGUILayout.EndHorizontal();
            EditorGUILayout.EndVertical();

            EditorGUILayout.Space(10);
            EditorGUILayout.LabelField(
                "5. After configuration:",
                EditorStyles.boldLabel
            );
            EditorGUILayout.LabelField(
                "• Restart VSCode",
                instructionStyle
            );
            EditorGUILayout.LabelField(
                "• GitHub Copilot will now be able to interact with your Unity project through the MCP protocol",
                instructionStyle
            );
            EditorGUILayout.LabelField(
                "• Remember to have the MCP for Unity Bridge running in Unity Editor",
                instructionStyle
            );

            EditorGUILayout.EndVertical();

            EditorGUILayout.Space(10);

            // Close button at the bottom
            EditorGUILayout.BeginHorizontal();
            GUILayout.FlexibleSpace();
            if (GUILayout.Button("Close", GUILayout.Height(30), GUILayout.Width(100)))
            {
                Close();
            }
            GUILayout.FlexibleSpace();
            EditorGUILayout.EndHorizontal();

            EditorGUILayout.EndScrollView();
        }

        protected override void Update()
        {
            // Call the base implementation which handles the copy feedback timer
            base.Update();
        }
    }
}

```

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

```csharp
using System.Runtime.InteropServices;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Models;

namespace MCPForUnity.Editor.Windows
{
    public class VSCodeManualSetupWindow : ManualConfigEditorWindow
    {
        public static void ShowWindow(string configPath, string configJson)
        {
            var window = GetWindow<VSCodeManualSetupWindow>("VSCode GitHub Copilot Setup");
            window.configPath = configPath;
            window.configJson = configJson;
            window.minSize = new Vector2(550, 500);

            // Create a McpClient for VSCode
            window.mcpClient = new McpClient
            {
                name = "VSCode GitHub Copilot",
                mcpType = McpTypes.VSCode
            };

            window.Show();
        }

        protected override void OnGUI()
        {
            scrollPos = EditorGUILayout.BeginScrollView(scrollPos);

            // Header with improved styling
            EditorGUILayout.Space(10);
            Rect titleRect = EditorGUILayout.GetControlRect(false, 30);
            EditorGUI.DrawRect(
                new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height),
                new Color(0.2f, 0.2f, 0.2f, 0.1f)
            );
            GUI.Label(
                new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height),
                "VSCode GitHub Copilot MCP Setup",
                EditorStyles.boldLabel
            );
            EditorGUILayout.Space(10);

            // Instructions with improved styling
            EditorGUILayout.BeginVertical(EditorStyles.helpBox);

            Rect headerRect = EditorGUILayout.GetControlRect(false, 24);
            EditorGUI.DrawRect(
                new Rect(headerRect.x, headerRect.y, headerRect.width, headerRect.height),
                new Color(0.1f, 0.1f, 0.1f, 0.2f)
            );
            GUI.Label(
                new Rect(
                    headerRect.x + 8,
                    headerRect.y + 4,
                    headerRect.width - 16,
                    headerRect.height
                ),
                "Setting up GitHub Copilot in VSCode with MCP for Unity",
                EditorStyles.boldLabel
            );
            EditorGUILayout.Space(10);

            GUIStyle instructionStyle = new(EditorStyles.wordWrappedLabel)
            {
                margin = new RectOffset(10, 10, 5, 5),
            };

            EditorGUILayout.LabelField(
                "1. Prerequisites",
                EditorStyles.boldLabel
            );
            EditorGUILayout.LabelField(
                "• Ensure you have VSCode installed",
                instructionStyle
            );
            EditorGUILayout.LabelField(
                "• Ensure you have GitHub Copilot extension installed in VSCode",
                instructionStyle
            );
            EditorGUILayout.LabelField(
                "• Ensure you have a valid GitHub Copilot subscription",
                instructionStyle
            );
            EditorGUILayout.Space(5);

            EditorGUILayout.LabelField(
                "2. Steps to Configure",
                EditorStyles.boldLabel
            );
            EditorGUILayout.LabelField(
                "a) Open or create your VSCode MCP config file (mcp.json) at the path below",
                instructionStyle
            );
            EditorGUILayout.LabelField(
                "b) Paste the JSON shown below into mcp.json",
                instructionStyle
            );
            EditorGUILayout.LabelField(
                "c) Save the file and restart VSCode",
                instructionStyle
            );
            EditorGUILayout.Space(5);

            EditorGUILayout.LabelField(
                "3. VSCode mcp.json location:",
                EditorStyles.boldLabel
            );

            // Path section with improved styling
            EditorGUILayout.BeginVertical(EditorStyles.helpBox);
            string displayPath;
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                displayPath = System.IO.Path.Combine(
                    System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData),
                    "Code",
                    "User",
                    "mcp.json"
                );
            }
            else
            {
                displayPath = System.IO.Path.Combine(
                    System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile),
                    "Library",
                    "Application Support",
                    "Code",
                    "User",
                    "mcp.json"
                );
            }

            // Store the path in the base class config path
            if (string.IsNullOrEmpty(configPath))
            {
                configPath = displayPath;
            }

            // Prevent text overflow by allowing the text field to wrap
            GUIStyle pathStyle = new(EditorStyles.textField) { wordWrap = true };

            EditorGUILayout.TextField(
                displayPath,
                pathStyle,
                GUILayout.Height(EditorGUIUtility.singleLineHeight)
            );

            // Copy button with improved styling
            EditorGUILayout.BeginHorizontal();
            GUILayout.FlexibleSpace();
            GUIStyle copyButtonStyle = new(GUI.skin.button)
            {
                padding = new RectOffset(15, 15, 5, 5),
                margin = new RectOffset(10, 10, 5, 5),
            };

            if (
                GUILayout.Button(
                    "Copy Path",
                    copyButtonStyle,
                    GUILayout.Height(25),
                    GUILayout.Width(100)
                )
            )
            {
                EditorGUIUtility.systemCopyBuffer = displayPath;
                pathCopied = true;
                copyFeedbackTimer = 2f;
            }

            if (
                GUILayout.Button(
                    "Open File",
                    copyButtonStyle,
                    GUILayout.Height(25),
                    GUILayout.Width(100)
                )
            )
            {
                // Open the file using the system's default application
                System.Diagnostics.Process.Start(
                    new System.Diagnostics.ProcessStartInfo
                    {
                        FileName = displayPath,
                        UseShellExecute = true,
                    }
                );
            }

            if (pathCopied)
            {
                GUIStyle feedbackStyle = new(EditorStyles.label);
                feedbackStyle.normal.textColor = Color.green;
                EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60));
            }

            EditorGUILayout.EndHorizontal();
            EditorGUILayout.EndVertical();
            EditorGUILayout.Space(10);

            EditorGUILayout.LabelField(
                "4. Add this configuration to your mcp.json:",
                EditorStyles.boldLabel
            );

            // JSON section with improved styling
            EditorGUILayout.BeginVertical(EditorStyles.helpBox);

            // Improved text area for JSON with syntax highlighting colors
            GUIStyle jsonStyle = new(EditorStyles.textArea)
            {
                font = EditorStyles.boldFont,
                wordWrap = true,
            };
            jsonStyle.normal.textColor = new Color(0.3f, 0.6f, 0.9f); // Syntax highlighting blue

            // Draw the JSON in a text area with a taller height for better readability
            EditorGUILayout.TextArea(configJson, jsonStyle, GUILayout.Height(200));

            // Copy JSON button with improved styling
            EditorGUILayout.BeginHorizontal();
            GUILayout.FlexibleSpace();

            if (
                GUILayout.Button(
                    "Copy JSON",
                    copyButtonStyle,
                    GUILayout.Height(25),
                    GUILayout.Width(100)
                )
            )
            {
                EditorGUIUtility.systemCopyBuffer = configJson;
                jsonCopied = true;
                copyFeedbackTimer = 2f;
            }

            if (jsonCopied)
            {
                GUIStyle feedbackStyle = new(EditorStyles.label);
                feedbackStyle.normal.textColor = Color.green;
                EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60));
            }

            EditorGUILayout.EndHorizontal();
            EditorGUILayout.EndVertical();

            EditorGUILayout.Space(10);
            EditorGUILayout.LabelField(
                "5. After configuration:",
                EditorStyles.boldLabel
            );
            EditorGUILayout.LabelField(
                "• Restart VSCode",
                instructionStyle
            );
            EditorGUILayout.LabelField(
                "• GitHub Copilot will now be able to interact with your Unity project through the MCP protocol",
                instructionStyle
            );
            EditorGUILayout.LabelField(
                "• Remember to have the MCP for Unity Bridge running in Unity Editor",
                instructionStyle
            );

            EditorGUILayout.EndVertical();

            EditorGUILayout.Space(10);

            // Close button at the bottom
            EditorGUILayout.BeginHorizontal();
            GUILayout.FlexibleSpace();
            if (GUILayout.Button("Close", GUILayout.Height(30), GUILayout.Width(100)))
            {
                Close();
            }
            GUILayout.FlexibleSpace();
            EditorGUILayout.EndHorizontal();

            EditorGUILayout.EndScrollView();
        }

        protected override void Update()
        {
            // Call the base implementation which handles the copy feedback timer
            base.Update();
        }
    }
}

```

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

```markdown
# MCP for Unity Development Tools

| [English](README-DEV.md) | [简体中文](README-DEV-zh.md) |
|---------------------------|------------------------------|

Welcome to the MCP for Unity development environment! This directory contains tools and utilities to streamline MCP for Unity core development.

## 🚀 Available Development Features

### ✅ Development Deployment Scripts
Quick deployment and testing tools for MCP for Unity core changes.

### 🔄 Coming Soon
- **Development Mode Toggle**: Built-in Unity editor development features
- **Hot Reload System**: Real-time code updates without Unity restarts  
- **Plugin Development Kit**: Tools for creating custom MCP for Unity extensions
- **Automated Testing Suite**: Comprehensive testing framework for contributions
- **Debug Dashboard**: Advanced debugging and monitoring tools

---

## Switching MCP package sources quickly

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:

**Usage:**
```bash
python mcp_source.py [--manifest /path/to/manifest.json] [--repo /path/to/unity-mcp] [--choice 1|2|3]
```

**Options:**
- **1** Upstream main (CoplayDev/unity-mcp)
- **2** Remote current branch (origin + branch)
- **3** Local workspace (file: MCPForUnity)

After switching, open Package Manager and Refresh to re-resolve packages.

## Development Deployment Scripts

These deployment scripts help you quickly test changes to MCP for Unity core code.

## Scripts

### `deploy-dev.bat`
Deploys your development code to the actual installation locations for testing.

**What it does:**
1. Backs up original files to a timestamped folder
2. Copies Unity Bridge code to Unity's package cache
3. Copies Python Server code to the MCP installation folder

**Usage:**
1. Run `deploy-dev.bat`
2. Enter Unity package cache path (example provided)
3. Enter server path (or use default: `%LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer\src`)
4. Enter backup location (or use default: `%USERPROFILE%\Desktop\unity-mcp-backup`)

**Note:** Dev deploy skips `.venv`, `__pycache__`, `.pytest_cache`, `.mypy_cache`, `.git`; reduces churn and avoids copying virtualenvs.

### `restore-dev.bat`
Restores original files from backup.

**What it does:**
1. Lists available backups with timestamps
2. Allows you to select which backup to restore
3. Restores both Unity Bridge and Python Server files

### `prune_tool_results.py`
Compacts large `tool_result` blobs in conversation JSON into concise one-line summaries.

**Usage:**
```bash
python3 prune_tool_results.py < reports/claude-execution-output.json > reports/claude-execution-output.pruned.json
```

The script reads a conversation from `stdin` and writes the pruned version to `stdout`, making logs much easier to inspect or archive.

These defaults dramatically cut token usage without affecting essential information.

## Finding Unity Package Cache Path

Unity stores Git packages under a version-or-hash folder. Expect something like:
```
X:\UnityProject\Library\PackageCache\com.coplaydev.unity-mcp@<version-or-hash>
```
Example (hash):
```
X:\UnityProject\Library\PackageCache\com.coplaydev.unity-mcp@272123cfd97e

```

To find it reliably:
1. Open Unity Package Manager
2. Select "MCP for Unity" package
3. Right click the package and choose "Show in Explorer"
4. That opens the exact cache folder Unity is using for your project

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.

## MCP Bridge Stress Test

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).

### Script
- `tools/stress_mcp.py`

### What it does
- Starts N TCP clients against the MCP for Unity bridge (default port auto-discovered from `~/.unity-mcp/unity-mcp-status-*.json`).
- Sends lightweight framed `ping` keepalives to maintain concurrency.
- In parallel, appends a unique marker comment to a target C# file using `manage_script.apply_text_edits` with:
  - `options.refresh = "immediate"` to force an import/compile immediately (triggers domain reload), and
  - `precondition_sha256` computed from the current file contents to avoid drift.
- Uses EOF insertion to avoid header/`using`-guard edits.

### Usage (local)
```bash
# Recommended: use the included large script in the test project
python3 tools/stress_mcp.py \
  --duration 60 \
  --clients 8 \
  --unity-file "TestProjects/UnityMCPTests/Assets/Scripts/LongUnityScriptClaudeTest.cs"
```

Flags:
- `--project` Unity project path (auto-detected to the included test project by default)
- `--unity-file` C# file to edit (defaults to the long test script)
- `--clients` number of concurrent clients (default 10)
- `--duration` seconds to run (default 60)

### Expected outcome
- No Unity Editor crashes during reload churn
- Immediate reloads after each applied edit (no `Assets/Refresh` menu calls)
- Some transient disconnects or a few failed calls may occur during domain reload; the tool retries and continues
- JSON summary printed at the end, e.g.:
  - `{"port": 6400, "stats": {"pings": 28566, "applies": 69, "disconnects": 0, "errors": 0}}`

### Notes and troubleshooting
- Immediate vs debounced:
  - 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.
- Precondition required:
  - `apply_text_edits` requires `precondition_sha256` on larger files. The tool reads the file first to compute the SHA.
- Edit location:
  - To avoid header guards or complex ranges, the tool appends a one-line marker at EOF each cycle.
- Read API:
  - The bridge currently supports `manage_script.read` for file reads. You may see a deprecation warning; it's harmless for this internal tool.
- Transient failures:
  - Occasional `apply_errors` often indicate the connection reloaded mid-reply. Edits still typically apply; the loop continues on the next iteration.

### CI guidance
- Keep this out of default PR CI due to Unity/editor requirements and runtime variability.
- Optionally run it as a manual workflow or nightly job on a Unity-capable runner.

## CI Test Workflow (GitHub Actions)

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.

***To run it***
 - Trigger: In GitHun "Actions" for the repo, trigger `workflow dispatch` (`Claude NL/T Full Suite (Unity live)`).
 - Image: `UNITY_IMAGE` (UnityCI) pulled by tag; the job resolves a digest at runtime. Logs are sanitized.
 - 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.
 - Reports: JUnit at `reports/junit-nl-suite.xml`, Markdown at `reports/junit-nl-suite.md`.
 - Publishing: JUnit is normalized to `reports/junit-for-actions.xml` and published; artifacts upload all files under `reports/`.

### Test target script
- The repo includes a long, standalone C# script used to exercise larger edits and windows:
  - `TestProjects/UnityMCPTests/Assets/Scripts/LongUnityScriptClaudeTest.cs`
  Use this file locally and in CI to validate multi-edit batches, anchor inserts, and windowed reads on a sizable script.

### Adjust tests / prompts
- 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.
- Keep edits minimal and reversible; include concise evidence.

### Run the suite
1) Push your branch, then manually run the workflow from the Actions tab.
2) The job writes reports into `reports/` and uploads artifacts.
3) The “JUnit Test Report” check summarizes results; open the Job Summary for full markdown.

### View results
- Job Summary: inline markdown summary of the run on the Actions tab in GitHub
- Check: “JUnit Test Report” on the PR/commit.
- Artifacts: `claude-nl-suite-artifacts` includes XML and MD.

### MCP Connection Debugging
- *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:
  - bridge startup/port, client connections, strict framing negotiation, and parsed frames
  - auto-config path detection (Windows/macOS/Linux), uv/claude resolution, and surfaced errors
- In CI, the job tails Unity logs (redacted for serial/license/password/token) and prints socket/status JSON diagnostics if startup fails.
## Workflow

1. **Make changes** to your source code in this directory
2. **Deploy** using `deploy-dev.bat`
3. **Test** in Unity (restart Unity Editor first)
4. **Iterate** - repeat steps 1-3 as needed
5. **Restore** original files when done using `restore-dev.bat`

## Troubleshooting

### "Path not found" errors running the .bat file
- Verify Unity package cache path is correct
- Check that MCP for Unity package is actually installed
- Ensure server is installed via MCP client

### "Permission denied" errors
- Run cmd as Administrator
- Close Unity Editor before deploying
- Close any MCP clients before deploying

### "Backup not found" errors
- Run `deploy-dev.bat` first to create initial backup
- Check backup directory permissions
- Verify backup directory path is correct

### Windows uv path issues
- 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
using System;
using System.IO;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace MCPForUnity.Editor.Tools.Prefabs
{
    [McpForUnityTool("manage_prefabs")]
    public static class ManagePrefabs
    {
        private const string SupportedActions = "open_stage, close_stage, save_open_stage, create_from_gameobject";

        public static object HandleCommand(JObject @params)
        {
            if (@params == null)
            {
                return Response.Error("Parameters cannot be null.");
            }

            string action = @params["action"]?.ToString()?.ToLowerInvariant();
            if (string.IsNullOrEmpty(action))
            {
                return Response.Error($"Action parameter is required. Valid actions are: {SupportedActions}.");
            }

            try
            {
                switch (action)
                {
                    case "open_stage":
                        return OpenStage(@params);
                    case "close_stage":
                        return CloseStage(@params);
                    case "save_open_stage":
                        return SaveOpenStage();
                    case "create_from_gameobject":
                        return CreatePrefabFromGameObject(@params);
                    default:
                        return Response.Error($"Unknown action: '{action}'. Valid actions are: {SupportedActions}.");
                }
            }
            catch (Exception e)
            {
                McpLog.Error($"[ManagePrefabs] Action '{action}' failed: {e}");
                return Response.Error($"Internal error: {e.Message}");
            }
        }

        private static object OpenStage(JObject @params)
        {
            string prefabPath = @params["prefabPath"]?.ToString();
            if (string.IsNullOrEmpty(prefabPath))
            {
                return Response.Error("'prefabPath' parameter is required for open_stage.");
            }

            string sanitizedPath = AssetPathUtility.SanitizeAssetPath(prefabPath);
            GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(sanitizedPath);
            if (prefabAsset == null)
            {
                return Response.Error($"No prefab asset found at path '{sanitizedPath}'.");
            }

            string modeValue = @params["mode"]?.ToString();
            if (!string.IsNullOrEmpty(modeValue) && !modeValue.Equals(PrefabStage.Mode.InIsolation.ToString(), StringComparison.OrdinalIgnoreCase))
            {
                return Response.Error("Only PrefabStage mode 'InIsolation' is supported at this time.");
            }

            PrefabStage stage = PrefabStageUtility.OpenPrefab(sanitizedPath);
            if (stage == null)
            {
                return Response.Error($"Failed to open prefab stage for '{sanitizedPath}'.");
            }

            return Response.Success($"Opened prefab stage for '{sanitizedPath}'.", SerializeStage(stage));
        }

        private static object CloseStage(JObject @params)
        {
            PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
            if (stage == null)
            {
                return Response.Success("No prefab stage was open.");
            }

            bool saveBeforeClose = @params["saveBeforeClose"]?.ToObject<bool>() ?? false;
            if (saveBeforeClose && stage.scene.isDirty)
            {
                SaveStagePrefab(stage);
                AssetDatabase.SaveAssets();
            }

            StageUtility.GoToMainStage();
            return Response.Success($"Closed prefab stage for '{stage.assetPath}'.");
        }

        private static object SaveOpenStage()
        {
            PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
            if (stage == null)
            {
                return Response.Error("No prefab stage is currently open.");
            }

            SaveStagePrefab(stage);
            AssetDatabase.SaveAssets();
            return Response.Success($"Saved prefab stage for '{stage.assetPath}'.", SerializeStage(stage));
        }

        private static void SaveStagePrefab(PrefabStage stage)
        {
            if (stage?.prefabContentsRoot == null)
            {
                throw new InvalidOperationException("Cannot save prefab stage without a prefab root.");
            }

            bool saved = PrefabUtility.SaveAsPrefabAsset(stage.prefabContentsRoot, stage.assetPath);
            if (!saved)
            {
                throw new InvalidOperationException($"Failed to save prefab asset at '{stage.assetPath}'.");
            }
        }

        private static object CreatePrefabFromGameObject(JObject @params)
        {
            string targetName = @params["target"]?.ToString() ?? @params["name"]?.ToString();
            if (string.IsNullOrEmpty(targetName))
            {
                return Response.Error("'target' parameter is required for create_from_gameobject.");
            }

            bool includeInactive = @params["searchInactive"]?.ToObject<bool>() ?? false;
            GameObject sourceObject = FindSceneObjectByName(targetName, includeInactive);
            if (sourceObject == null)
            {
                return Response.Error($"GameObject '{targetName}' not found in the active scene.");
            }

            if (PrefabUtility.IsPartOfPrefabAsset(sourceObject))
            {
                return Response.Error(
                    $"GameObject '{sourceObject.name}' is part of a prefab asset. Open the prefab stage to save changes instead."
                );
            }

            PrefabInstanceStatus status = PrefabUtility.GetPrefabInstanceStatus(sourceObject);
            if (status != PrefabInstanceStatus.NotAPrefab)
            {
                return Response.Error(
                    $"GameObject '{sourceObject.name}' is already linked to an existing prefab instance."
                );
            }

            string requestedPath = @params["prefabPath"]?.ToString();
            if (string.IsNullOrWhiteSpace(requestedPath))
            {
                return Response.Error("'prefabPath' parameter is required for create_from_gameobject.");
            }

            string sanitizedPath = AssetPathUtility.SanitizeAssetPath(requestedPath);
            if (!sanitizedPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
            {
                sanitizedPath += ".prefab";
            }

            bool allowOverwrite = @params["allowOverwrite"]?.ToObject<bool>() ?? false;
            string finalPath = sanitizedPath;

            if (!allowOverwrite && AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(finalPath) != null)
            {
                finalPath = AssetDatabase.GenerateUniqueAssetPath(finalPath);
            }

            EnsureAssetDirectoryExists(finalPath);

            try
            {
                GameObject connectedInstance = PrefabUtility.SaveAsPrefabAssetAndConnect(
                    sourceObject,
                    finalPath,
                    InteractionMode.AutomatedAction
                );

                if (connectedInstance == null)
                {
                    return Response.Error($"Failed to save prefab asset at '{finalPath}'.");
                }

                Selection.activeGameObject = connectedInstance;

                return Response.Success(
                    $"Prefab created at '{finalPath}' and instance linked.",
                    new
                    {
                        prefabPath = finalPath,
                        instanceId = connectedInstance.GetInstanceID()
                    }
                );
            }
            catch (Exception e)
            {
                return Response.Error($"Error saving prefab asset at '{finalPath}': {e.Message}");
            }
        }

        private static void EnsureAssetDirectoryExists(string assetPath)
        {
            string directory = Path.GetDirectoryName(assetPath);
            if (string.IsNullOrEmpty(directory))
            {
                return;
            }

            string fullDirectory = Path.Combine(Directory.GetCurrentDirectory(), directory);
            if (!Directory.Exists(fullDirectory))
            {
                Directory.CreateDirectory(fullDirectory);
                AssetDatabase.Refresh();
            }
        }

        private static GameObject FindSceneObjectByName(string name, bool includeInactive)
        {
            PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
            if (stage?.prefabContentsRoot != null)
            {
                foreach (Transform transform in stage.prefabContentsRoot.GetComponentsInChildren<Transform>(includeInactive))
                {
                    if (transform.name == name)
                    {
                        return transform.gameObject;
                    }
                }
            }

            Scene activeScene = SceneManager.GetActiveScene();
            foreach (GameObject root in activeScene.GetRootGameObjects())
            {
                foreach (Transform transform in root.GetComponentsInChildren<Transform>(includeInactive))
                {
                    GameObject candidate = transform.gameObject;
                    if (candidate.name == name)
                    {
                        return candidate;
                    }
                }
            }

            return null;
        }

        private static object SerializeStage(PrefabStage stage)
        {
            if (stage == null)
            {
                return new { isOpen = false };
            }

            return new
            {
                isOpen = true,
                assetPath = stage.assetPath,
                prefabRootName = stage.prefabContentsRoot != null ? stage.prefabContentsRoot.name : null,
                mode = stage.mode.ToString(),
                isDirty = stage.scene.isDirty
            };
        }

    }
}

```

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

```csharp
using System;
using System.IO;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace MCPForUnity.Editor.Tools.Prefabs
{
    [McpForUnityTool("manage_prefabs")]
    public static class ManagePrefabs
    {
        private const string SupportedActions = "open_stage, close_stage, save_open_stage, create_from_gameobject";

        public static object HandleCommand(JObject @params)
        {
            if (@params == null)
            {
                return Response.Error("Parameters cannot be null.");
            }

            string action = @params["action"]?.ToString()?.ToLowerInvariant();
            if (string.IsNullOrEmpty(action))
            {
                return Response.Error($"Action parameter is required. Valid actions are: {SupportedActions}.");
            }

            try
            {
                switch (action)
                {
                    case "open_stage":
                        return OpenStage(@params);
                    case "close_stage":
                        return CloseStage(@params);
                    case "save_open_stage":
                        return SaveOpenStage();
                    case "create_from_gameobject":
                        return CreatePrefabFromGameObject(@params);
                    default:
                        return Response.Error($"Unknown action: '{action}'. Valid actions are: {SupportedActions}.");
                }
            }
            catch (Exception e)
            {
                McpLog.Error($"[ManagePrefabs] Action '{action}' failed: {e}");
                return Response.Error($"Internal error: {e.Message}");
            }
        }

        private static object OpenStage(JObject @params)
        {
            string prefabPath = @params["prefabPath"]?.ToString();
            if (string.IsNullOrEmpty(prefabPath))
            {
                return Response.Error("'prefabPath' parameter is required for open_stage.");
            }

            string sanitizedPath = AssetPathUtility.SanitizeAssetPath(prefabPath);
            GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(sanitizedPath);
            if (prefabAsset == null)
            {
                return Response.Error($"No prefab asset found at path '{sanitizedPath}'.");
            }

            string modeValue = @params["mode"]?.ToString();
            if (!string.IsNullOrEmpty(modeValue) && !modeValue.Equals(PrefabStage.Mode.InIsolation.ToString(), StringComparison.OrdinalIgnoreCase))
            {
                return Response.Error("Only PrefabStage mode 'InIsolation' is supported at this time.");
            }

            PrefabStage stage = PrefabStageUtility.OpenPrefab(sanitizedPath);
            if (stage == null)
            {
                return Response.Error($"Failed to open prefab stage for '{sanitizedPath}'.");
            }

            return Response.Success($"Opened prefab stage for '{sanitizedPath}'.", SerializeStage(stage));
        }

        private static object CloseStage(JObject @params)
        {
            PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
            if (stage == null)
            {
                return Response.Success("No prefab stage was open.");
            }

            bool saveBeforeClose = @params["saveBeforeClose"]?.ToObject<bool>() ?? false;
            if (saveBeforeClose && stage.scene.isDirty)
            {
                SaveStagePrefab(stage);
                AssetDatabase.SaveAssets();
            }

            StageUtility.GoToMainStage();
            return Response.Success($"Closed prefab stage for '{stage.assetPath}'.");
        }

        private static object SaveOpenStage()
        {
            PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
            if (stage == null)
            {
                return Response.Error("No prefab stage is currently open.");
            }

            SaveStagePrefab(stage);
            AssetDatabase.SaveAssets();
            return Response.Success($"Saved prefab stage for '{stage.assetPath}'.", SerializeStage(stage));
        }

        private static void SaveStagePrefab(PrefabStage stage)
        {
            if (stage?.prefabContentsRoot == null)
            {
                throw new InvalidOperationException("Cannot save prefab stage without a prefab root.");
            }

            bool saved = PrefabUtility.SaveAsPrefabAsset(stage.prefabContentsRoot, stage.assetPath);
            if (!saved)
            {
                throw new InvalidOperationException($"Failed to save prefab asset at '{stage.assetPath}'.");
            }
        }

        private static object CreatePrefabFromGameObject(JObject @params)
        {
            string targetName = @params["target"]?.ToString() ?? @params["name"]?.ToString();
            if (string.IsNullOrEmpty(targetName))
            {
                return Response.Error("'target' parameter is required for create_from_gameobject.");
            }

            bool includeInactive = @params["searchInactive"]?.ToObject<bool>() ?? false;
            GameObject sourceObject = FindSceneObjectByName(targetName, includeInactive);
            if (sourceObject == null)
            {
                return Response.Error($"GameObject '{targetName}' not found in the active scene.");
            }

            if (PrefabUtility.IsPartOfPrefabAsset(sourceObject))
            {
                return Response.Error(
                    $"GameObject '{sourceObject.name}' is part of a prefab asset. Open the prefab stage to save changes instead."
                );
            }

            PrefabInstanceStatus status = PrefabUtility.GetPrefabInstanceStatus(sourceObject);
            if (status != PrefabInstanceStatus.NotAPrefab)
            {
                return Response.Error(
                    $"GameObject '{sourceObject.name}' is already linked to an existing prefab instance."
                );
            }

            string requestedPath = @params["prefabPath"]?.ToString();
            if (string.IsNullOrWhiteSpace(requestedPath))
            {
                return Response.Error("'prefabPath' parameter is required for create_from_gameobject.");
            }

            string sanitizedPath = AssetPathUtility.SanitizeAssetPath(requestedPath);
            if (!sanitizedPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
            {
                sanitizedPath += ".prefab";
            }

            bool allowOverwrite = @params["allowOverwrite"]?.ToObject<bool>() ?? false;
            string finalPath = sanitizedPath;

            if (!allowOverwrite && AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(finalPath) != null)
            {
                finalPath = AssetDatabase.GenerateUniqueAssetPath(finalPath);
            }

            EnsureAssetDirectoryExists(finalPath);

            try
            {
                GameObject connectedInstance = PrefabUtility.SaveAsPrefabAssetAndConnect(
                    sourceObject,
                    finalPath,
                    InteractionMode.AutomatedAction
                );

                if (connectedInstance == null)
                {
                    return Response.Error($"Failed to save prefab asset at '{finalPath}'.");
                }

                Selection.activeGameObject = connectedInstance;

                return Response.Success(
                    $"Prefab created at '{finalPath}' and instance linked.",
                    new
                    {
                        prefabPath = finalPath,
                        instanceId = connectedInstance.GetInstanceID()
                    }
                );
            }
            catch (Exception e)
            {
                return Response.Error($"Error saving prefab asset at '{finalPath}': {e.Message}");
            }
        }

        private static void EnsureAssetDirectoryExists(string assetPath)
        {
            string directory = Path.GetDirectoryName(assetPath);
            if (string.IsNullOrEmpty(directory))
            {
                return;
            }

            string fullDirectory = Path.Combine(Directory.GetCurrentDirectory(), directory);
            if (!Directory.Exists(fullDirectory))
            {
                Directory.CreateDirectory(fullDirectory);
                AssetDatabase.Refresh();
            }
        }

        private static GameObject FindSceneObjectByName(string name, bool includeInactive)
        {
            PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
            if (stage?.prefabContentsRoot != null)
            {
                foreach (Transform transform in stage.prefabContentsRoot.GetComponentsInChildren<Transform>(includeInactive))
                {
                    if (transform.name == name)
                    {
                        return transform.gameObject;
                    }
                }
            }

            Scene activeScene = SceneManager.GetActiveScene();
            foreach (GameObject root in activeScene.GetRootGameObjects())
            {
                foreach (Transform transform in root.GetComponentsInChildren<Transform>(includeInactive))
                {
                    GameObject candidate = transform.gameObject;
                    if (candidate.name == name)
                    {
                        return candidate;
                    }
                }
            }

            return null;
        }

        private static object SerializeStage(PrefabStage stage)
        {
            if (stage == null)
            {
                return new { isOpen = false };
            }

            return new
            {
                isOpen = true,
                assetPath = stage.assetPath,
                prefabRootName = stage.prefabContentsRoot != null ? stage.prefabContentsRoot.name : null,
                mode = stage.mode.ToString(),
                isDirty = stage.scene.isDirty
            };
        }

    }
}

```

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

```markdown
# MCP for Unity v6 - New Editor Window

> **UI Toolkit-based window with service-oriented architecture**

![New MCP Editor Window Dark](./screenshots/v6_new_ui_dark.png)
*Dark theme*

![New MCP Editor Window Light](./screenshots/v6_new_ui_light.png)
*Light theme*

---

## Overview

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.

**Quick Access:** `Cmd/Ctrl+Shift+M` or `Window > MCP For Unity > Open MCP Window`

**Key Improvements:**
- 🎨 Modern UI that doesn't hide info as the window size changes
- 🏗️ Service layer separates business logic from UI
- 🔧 Explicit path overrides for troubleshooting
- 📦 Asset Store support with server download capability
- ⚡ Keyboard shortcut for quick access

---

## Key Differences at a Glance

| Feature | Old Window | New Window | Notes |
|---------|-----------|------------|-------|
| **Architecture** | Monolithic | Service-based | Better testability & reusability |
| **UI Framework** | IMGUI | UI Toolkit (UXML/USS) | Modern, responsive, themeable |
| **Auto-Setup** | ✅ Automatic | ❌ Manual | Users have explicit control |
| **Path Overrides** | ⚠️ Python only | ✅ Python + UV + Claude CLI | Advanced troubleshooting |
| **Bridge Health** | ⚠️ Hidden | ✅ Visible with test button | Separate from connection status |
| **Configure All** | ❌ None | ✅ Batch with summary | Configure all clients at once |
| **Manual Config** | ✅ Popup windows | ✅ Inline foldout | Less window clutter |
| **Server Download** | ❌ None | ✅ Asset Store support | Download server from GitHub |
| **Keyboard Shortcut** | ❌ None | ✅ Cmd/Ctrl+Shift+M | Quick access |

## What's New

### UI Enhancements
- **Advanced Settings Foldout** - Collapsible section for path overrides (MCP server, UV, Claude CLI)
- **Visual Path Validation** - Green/red indicators show whether override paths are valid
- **Bridge Health Indicator** - Separate from connection status, shows handshake and ping/pong results
- **Manual Connection Test Button** - Verify bridge health on demand without reconnecting
- **Inline Manual Configuration** - Copy config path and JSON without opening separate windows

### Functional Improvements
- **Configure All Detected Clients** - One-click batch configuration with summary dialog
- **Keyboard Shortcut** - `Cmd/Ctrl+Shift+M` opens the window quickly

### Asset Store Support
- **Server Download Button** - Asset Store users can download the server from GitHub releases
- **Dynamic UI** - Shows appropriate button based on installation type

![Asset Store Version](./screenshots/v6_new_ui_asset_store_version.png)
*Asset Store version showing the "Download & Install Server" button*

---

## Features Not Supported (By Design)

The new window intentionally removes implicit behaviors and complex edge-case handling to provide a cleaner, more predictable UX.

### ❌ Auto-Setup on First Run
- **Old:** Automatically configured clients on first window open
- **Why Removed:** Users should explicitly choose which clients to configure
- **Alternative:** Use "Configure All Detected Clients" button

### ❌ Python Detection Warning
- **Old:** Warning banner if Python not detected on system
- **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
- **Alternative:** Run Setup Wizard via `Window > MCP For Unity > Setup Wizard`

### ❌ Separate Manual Setup Windows
- **Old:** `VSCodeManualSetupWindow`, `ManualConfigEditorWindow` popup dialogs
- **Why Removed:** Looks neater, less visual clutter
- **Alternative:** Inline "Manual Configuration" foldout with copy buttons

### ❌ Server Installation Status Panel
- **Old:** Dedicated panel showing server install status with color indicators
- **Why Removed:** Simplified to focus on active configuration and the connection status, we now have a setup wizard for this
- **Alternative:** Server path override in Advanced Settings + Rebuild button

---

## Service Locator Architecture

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.

### MCPServiceLocator

**Purpose:** Central access point for MCP services

**Usage:**
```csharp
// Access bridge service
MCPServiceLocator.Bridge.Start();

// Access client configuration service
MCPServiceLocator.Client.ConfigureAllDetectedClients();

// Access path resolver service
string mcpServerPath = MCPServiceLocator.Paths.GetMcpServerPath();
```

**Benefits:**
- No constructor dependencies (easy to use anywhere)
- Lazy initialization (services created only when needed)
- Testable (supports custom implementations via `Register()`)

---

### IBridgeControlService

**Purpose:** Manages MCP for Unity Bridge lifecycle and health verification

**Key Methods:**
- `Start()` / `Stop()` - Bridge lifecycle management
- `Verify(port)` - Health check with handshake + ping/pong validation
- `IsRunning` - Current bridge status
- `CurrentPort` - Active port number

**Implementation:** `BridgeControlService`

**Usage Example:**
```csharp
var bridge = MCPServiceLocator.Bridge;
bridge.Start();

var result = bridge.Verify(bridge.CurrentPort);
if (result.Success && result.PingSucceeded)
{
    Debug.Log("Bridge is healthy");
}
```

---

### IClientConfigurationService

**Purpose:** Handles MCP client configuration and registration

**Key Methods:**
- `ConfigureClient(client)` - Configure a single client
- `ConfigureAllDetectedClients()` - Batch configure with summary
- `CheckClientStatus(client)` - Verify client status + auto-rewrite paths
- `RegisterClaudeCode()` / `UnregisterClaudeCode()` - Claude Code management
- `GenerateConfigJson(client)` - Get JSON for manual configuration

**Implementation:** `ClientConfigurationService`

**Usage Example:**
```csharp
var clientService = MCPServiceLocator.Client;
var summary = clientService.ConfigureAllDetectedClients();
Debug.Log($"Configured: {summary.SuccessCount}, Failed: {summary.FailureCount}");
```

---

### IPathResolverService

**Purpose:** Resolves paths to required tools with override support

**Key Methods:**
- `GetMcpServerPath()` - MCP server directory
- `GetUvPath()` - UV executable path
- `GetClaudeCliPath()` - Claude CLI path
- `SetMcpServerOverride(path)` / `ClearMcpServerOverride()` - Manage MCP server overrides
- `SetUvPathOverride(path)` / `ClearUvPathOverride()` - Manage UV overrides
- `SetClaudeCliPathOverride(path)` / `ClearClaudeCliPathOverride()` - Manage Claude CLI overrides
- `IsPythonDetected()` / `IsUvDetected()` - Detection checks

**Implementation:** `PathResolverService`

**Usage Example:**
```csharp
var paths = MCPServiceLocator.Paths;

// Check if UV is detected
if (!paths.IsUvDetected())
{
    Debug.LogWarning("UV not found");
}

// Set an override
paths.SetUvPathOverride("/custom/path/to/uv");
```

## Technical Details

### Files Created

**Services:**
```text
MCPForUnity/Editor/Services/
├── IBridgeControlService.cs          # Bridge lifecycle interface
├── BridgeControlService.cs           # Bridge lifecycle implementation
├── IClientConfigurationService.cs    # Client config interface
├── ClientConfigurationService.cs     # Client config implementation
├── IPathResolverService.cs          # Path resolution interface
├── PathResolverService.cs           # Path resolution implementation
└── MCPServiceLocator.cs             # Service locator pattern
```

**Helpers:**
```text
MCPForUnity/Editor/Helpers/
└── AssetPathUtility.cs              # Package path detection & package.json parsing
```

**UI:**
```text
MCPForUnity/Editor/Windows/
├── MCPForUnityEditorWindowNew.cs    # Main window (~850 lines)
├── MCPForUnityEditorWindowNew.uxml  # UI Toolkit layout
└── MCPForUnityEditorWindowNew.uss   # UI Toolkit styles
```

**CI/CD:**
```text
.github/workflows/
└── bump-version.yml                 # Server upload to releases
```

### Key Files Modified

- `ServerInstaller.cs` - Added download/install logic for Asset Store
- `SetupWizard.cs` - Integration with new service locator
- `PackageDetector.cs` - Uses `AssetPathUtility` for version detection

---

## Migration Notes

### For Users

**Immediate Changes (v6.x):**
- Both old and new windows are available
- New window accessible via `Cmd/Ctrl+Shift+M` or menu
- Settings and overrides are shared between windows (same EditorPrefs keys)
- Services can be used by both windows

**Upcoming Changes (v8.x):**
- ⚠️ **Old window will be removed in v8.0**
- All users will automatically use the new window
- EditorPrefs keys remain the same (no migration needed)
- Custom scripts using old window APIs will need updates

### For Developers

**Using the Services:**
```csharp
// Accessing services from any editor script
var bridge = MCPServiceLocator.Bridge;
var client = MCPServiceLocator.Client;
var paths = MCPServiceLocator.Paths;

// Services are lazily initialized on first access
// No need to check for null
```

**Testing with Custom Implementations:**
```csharp
// In test setup
var mockBridge = new MockBridgeService();
MCPServiceLocator.Register(mockBridge);

// Services are now testable without Unity dependencies
```

**Reusing Service Logic:**
The service layer is designed to be reused by other parts of the codebase. For example:
- Build scripts can use `IClientConfigurationService` to auto-configure clients
- CI/CD can use `IBridgeControlService` to verify bridge health
- Tools can use `IPathResolverService` for consistent path resolution

**Notes:**
- A lot of Helpers will gradually be moved to the service layer
- 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

---

## Pull Request Reference

**PR #313:** [feat: New UI with service architecture](https://github.com/CoplayDev/unity-mcp/pull/313)

**Key Commits:**
- Service layer implementation
- UI Toolkit window rebuild
- Asset Store server download support
- CI/CD server upload automation

---

**Last Updated:** 2025-10-10  
**Unity Versions:** Unity 2021.3+ through Unity 6.x  
**Architecture:** Service Locator + UI Toolkit  
**Status:** Active (Old window deprecated in v8.0)

```

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

```csharp
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor; // Required for AssetDatabase and EditorUtility
#endif

namespace MCPForUnity.Runtime.Serialization
{
    public class Vector3Converter : JsonConverter<Vector3>
    {
        public override void WriteJson(JsonWriter writer, Vector3 value, JsonSerializer serializer)
        {
            writer.WriteStartObject();
            writer.WritePropertyName("x");
            writer.WriteValue(value.x);
            writer.WritePropertyName("y");
            writer.WriteValue(value.y);
            writer.WritePropertyName("z");
            writer.WriteValue(value.z);
            writer.WriteEndObject();
        }

        public override Vector3 ReadJson(JsonReader reader, Type objectType, Vector3 existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            JObject jo = JObject.Load(reader);
            return new Vector3(
                (float)jo["x"],
                (float)jo["y"],
                (float)jo["z"]
            );
        }
    }

    public class Vector2Converter : JsonConverter<Vector2>
    {
        public override void WriteJson(JsonWriter writer, Vector2 value, JsonSerializer serializer)
        {
            writer.WriteStartObject();
            writer.WritePropertyName("x");
            writer.WriteValue(value.x);
            writer.WritePropertyName("y");
            writer.WriteValue(value.y);
            writer.WriteEndObject();
        }

        public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            JObject jo = JObject.Load(reader);
            return new Vector2(
                (float)jo["x"],
                (float)jo["y"]
            );
        }
    }

    public class QuaternionConverter : JsonConverter<Quaternion>
    {
        public override void WriteJson(JsonWriter writer, Quaternion value, JsonSerializer serializer)
        {
            writer.WriteStartObject();
            writer.WritePropertyName("x");
            writer.WriteValue(value.x);
            writer.WritePropertyName("y");
            writer.WriteValue(value.y);
            writer.WritePropertyName("z");
            writer.WriteValue(value.z);
            writer.WritePropertyName("w");
            writer.WriteValue(value.w);
            writer.WriteEndObject();
        }

        public override Quaternion ReadJson(JsonReader reader, Type objectType, Quaternion existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            JObject jo = JObject.Load(reader);
            return new Quaternion(
                (float)jo["x"],
                (float)jo["y"],
                (float)jo["z"],
                (float)jo["w"]
            );
        }
    }

    public class ColorConverter : JsonConverter<Color>
    {
        public override void WriteJson(JsonWriter writer, Color value, JsonSerializer serializer)
        {
            writer.WriteStartObject();
            writer.WritePropertyName("r");
            writer.WriteValue(value.r);
            writer.WritePropertyName("g");
            writer.WriteValue(value.g);
            writer.WritePropertyName("b");
            writer.WriteValue(value.b);
            writer.WritePropertyName("a");
            writer.WriteValue(value.a);
            writer.WriteEndObject();
        }

        public override Color ReadJson(JsonReader reader, Type objectType, Color existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            JObject jo = JObject.Load(reader);
            return new Color(
                (float)jo["r"],
                (float)jo["g"],
                (float)jo["b"],
                (float)jo["a"]
            );
        }
    }

    public class RectConverter : JsonConverter<Rect>
    {
        public override void WriteJson(JsonWriter writer, Rect value, JsonSerializer serializer)
        {
            writer.WriteStartObject();
            writer.WritePropertyName("x");
            writer.WriteValue(value.x);
            writer.WritePropertyName("y");
            writer.WriteValue(value.y);
            writer.WritePropertyName("width");
            writer.WriteValue(value.width);
            writer.WritePropertyName("height");
            writer.WriteValue(value.height);
            writer.WriteEndObject();
        }

        public override Rect ReadJson(JsonReader reader, Type objectType, Rect existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            JObject jo = JObject.Load(reader);
            return new Rect(
                (float)jo["x"],
                (float)jo["y"],
                (float)jo["width"],
                (float)jo["height"]
            );
        }
    }

    public class BoundsConverter : JsonConverter<Bounds>
    {
        public override void WriteJson(JsonWriter writer, Bounds value, JsonSerializer serializer)
        {
            writer.WriteStartObject();
            writer.WritePropertyName("center");
            serializer.Serialize(writer, value.center); // Use serializer to handle nested Vector3
            writer.WritePropertyName("size");
            serializer.Serialize(writer, value.size);   // Use serializer to handle nested Vector3
            writer.WriteEndObject();
        }

        public override Bounds ReadJson(JsonReader reader, Type objectType, Bounds existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            JObject jo = JObject.Load(reader);
            Vector3 center = jo["center"].ToObject<Vector3>(serializer); // Use serializer to handle nested Vector3
            Vector3 size = jo["size"].ToObject<Vector3>(serializer);     // Use serializer to handle nested Vector3
            return new Bounds(center, size);
        }
    }

    // Converter for UnityEngine.Object references (GameObjects, Components, Materials, Textures, etc.)
    public class UnityEngineObjectConverter : JsonConverter<UnityEngine.Object>
    {
        public override bool CanRead => true; // We need to implement ReadJson
        public override bool CanWrite => true;

        public override void WriteJson(JsonWriter writer, UnityEngine.Object value, JsonSerializer serializer)
        {
            if (value == null)
            {
                writer.WriteNull();
                return;
            }

#if UNITY_EDITOR // AssetDatabase and EditorUtility are Editor-only
            if (UnityEditor.AssetDatabase.Contains(value))
            {
                // It's an asset (Material, Texture, Prefab, etc.)
                string path = UnityEditor.AssetDatabase.GetAssetPath(value);
                if (!string.IsNullOrEmpty(path))
                {
                    writer.WriteValue(path);
                }
                else
                {
                    // Asset exists but path couldn't be found? Write minimal info.
                    writer.WriteStartObject();
                    writer.WritePropertyName("name");
                    writer.WriteValue(value.name);
                    writer.WritePropertyName("instanceID");
                    writer.WriteValue(value.GetInstanceID());
                    writer.WritePropertyName("isAssetWithoutPath");
                    writer.WriteValue(true);
                    writer.WriteEndObject();
                }
            }
            else
            {
                // It's a scene object (GameObject, Component, etc.)
                writer.WriteStartObject();
                writer.WritePropertyName("name");
                writer.WriteValue(value.name);
                writer.WritePropertyName("instanceID");
                writer.WriteValue(value.GetInstanceID());
                writer.WriteEndObject();
            }
#else
            // Runtime fallback: Write basic info without AssetDatabase
            writer.WriteStartObject();
            writer.WritePropertyName("name");
            writer.WriteValue(value.name);
            writer.WritePropertyName("instanceID");
            writer.WriteValue(value.GetInstanceID());
             writer.WritePropertyName("warning");
            writer.WriteValue("UnityEngineObjectConverter running in non-Editor mode, asset path unavailable.");
            writer.WriteEndObject();
#endif
        }

        public override UnityEngine.Object ReadJson(JsonReader reader, Type objectType, UnityEngine.Object existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            if (reader.TokenType == JsonToken.Null)
            {
                return null;
            }

#if UNITY_EDITOR
            if (reader.TokenType == JsonToken.String)
            {
                // Assume it's an asset path
                string path = reader.Value.ToString();
                return UnityEditor.AssetDatabase.LoadAssetAtPath(path, objectType);
            }

            if (reader.TokenType == JsonToken.StartObject)
            {
                JObject jo = JObject.Load(reader);
                if (jo.TryGetValue("instanceID", out JToken idToken) && idToken.Type == JTokenType.Integer)
                {
                    int instanceId = idToken.ToObject<int>();
                    UnityEngine.Object obj = UnityEditor.EditorUtility.InstanceIDToObject(instanceId);
                    if (obj != null && objectType.IsAssignableFrom(obj.GetType()))
                    {
                        return obj;
                    }
                }
                // Could potentially try finding by name as a fallback if ID lookup fails/isn't present
                // but that's less reliable.
            }
#else
             // Runtime deserialization is tricky without AssetDatabase/EditorUtility
             // Maybe log a warning and return null or existingValue?
             Debug.LogWarning("UnityEngineObjectConverter cannot deserialize complex objects in non-Editor mode.");
             // Skip the token to avoid breaking the reader
             if (reader.TokenType == JsonToken.StartObject) JObject.Load(reader);
             else if (reader.TokenType == JsonToken.String) reader.ReadAsString(); 
             // Return null or existing value, depending on desired behavior
             return existingValue; 
#endif

            throw new JsonSerializationException($"Unexpected token type '{reader.TokenType}' when deserializing UnityEngine.Object");
        }
    }
}
```

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

```csharp
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor; // Required for AssetDatabase and EditorUtility
#endif

namespace MCPForUnity.Runtime.Serialization
{
    public class Vector3Converter : JsonConverter<Vector3>
    {
        public override void WriteJson(JsonWriter writer, Vector3 value, JsonSerializer serializer)
        {
            writer.WriteStartObject();
            writer.WritePropertyName("x");
            writer.WriteValue(value.x);
            writer.WritePropertyName("y");
            writer.WriteValue(value.y);
            writer.WritePropertyName("z");
            writer.WriteValue(value.z);
            writer.WriteEndObject();
        }

        public override Vector3 ReadJson(JsonReader reader, Type objectType, Vector3 existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            JObject jo = JObject.Load(reader);
            return new Vector3(
                (float)jo["x"],
                (float)jo["y"],
                (float)jo["z"]
            );
        }
    }

    public class Vector2Converter : JsonConverter<Vector2>
    {
        public override void WriteJson(JsonWriter writer, Vector2 value, JsonSerializer serializer)
        {
            writer.WriteStartObject();
            writer.WritePropertyName("x");
            writer.WriteValue(value.x);
            writer.WritePropertyName("y");
            writer.WriteValue(value.y);
            writer.WriteEndObject();
        }

        public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            JObject jo = JObject.Load(reader);
            return new Vector2(
                (float)jo["x"],
                (float)jo["y"]
            );
        }
    }

    public class QuaternionConverter : JsonConverter<Quaternion>
    {
        public override void WriteJson(JsonWriter writer, Quaternion value, JsonSerializer serializer)
        {
            writer.WriteStartObject();
            writer.WritePropertyName("x");
            writer.WriteValue(value.x);
            writer.WritePropertyName("y");
            writer.WriteValue(value.y);
            writer.WritePropertyName("z");
            writer.WriteValue(value.z);
            writer.WritePropertyName("w");
            writer.WriteValue(value.w);
            writer.WriteEndObject();
        }

        public override Quaternion ReadJson(JsonReader reader, Type objectType, Quaternion existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            JObject jo = JObject.Load(reader);
            return new Quaternion(
                (float)jo["x"],
                (float)jo["y"],
                (float)jo["z"],
                (float)jo["w"]
            );
        }
    }

    public class ColorConverter : JsonConverter<Color>
    {
        public override void WriteJson(JsonWriter writer, Color value, JsonSerializer serializer)
        {
            writer.WriteStartObject();
            writer.WritePropertyName("r");
            writer.WriteValue(value.r);
            writer.WritePropertyName("g");
            writer.WriteValue(value.g);
            writer.WritePropertyName("b");
            writer.WriteValue(value.b);
            writer.WritePropertyName("a");
            writer.WriteValue(value.a);
            writer.WriteEndObject();
        }

        public override Color ReadJson(JsonReader reader, Type objectType, Color existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            JObject jo = JObject.Load(reader);
            return new Color(
                (float)jo["r"],
                (float)jo["g"],
                (float)jo["b"],
                (float)jo["a"]
            );
        }
    }

    public class RectConverter : JsonConverter<Rect>
    {
        public override void WriteJson(JsonWriter writer, Rect value, JsonSerializer serializer)
        {
            writer.WriteStartObject();
            writer.WritePropertyName("x");
            writer.WriteValue(value.x);
            writer.WritePropertyName("y");
            writer.WriteValue(value.y);
            writer.WritePropertyName("width");
            writer.WriteValue(value.width);
            writer.WritePropertyName("height");
            writer.WriteValue(value.height);
            writer.WriteEndObject();
        }

        public override Rect ReadJson(JsonReader reader, Type objectType, Rect existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            JObject jo = JObject.Load(reader);
            return new Rect(
                (float)jo["x"],
                (float)jo["y"],
                (float)jo["width"],
                (float)jo["height"]
            );
        }
    }

    public class BoundsConverter : JsonConverter<Bounds>
    {
        public override void WriteJson(JsonWriter writer, Bounds value, JsonSerializer serializer)
        {
            writer.WriteStartObject();
            writer.WritePropertyName("center");
            serializer.Serialize(writer, value.center); // Use serializer to handle nested Vector3
            writer.WritePropertyName("size");
            serializer.Serialize(writer, value.size);   // Use serializer to handle nested Vector3
            writer.WriteEndObject();
        }

        public override Bounds ReadJson(JsonReader reader, Type objectType, Bounds existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            JObject jo = JObject.Load(reader);
            Vector3 center = jo["center"].ToObject<Vector3>(serializer); // Use serializer to handle nested Vector3
            Vector3 size = jo["size"].ToObject<Vector3>(serializer);     // Use serializer to handle nested Vector3
            return new Bounds(center, size);
        }
    }

    // Converter for UnityEngine.Object references (GameObjects, Components, Materials, Textures, etc.)
    public class UnityEngineObjectConverter : JsonConverter<UnityEngine.Object>
    {
        public override bool CanRead => true; // We need to implement ReadJson
        public override bool CanWrite => true;

        public override void WriteJson(JsonWriter writer, UnityEngine.Object value, JsonSerializer serializer)
        {
            if (value == null)
            {
                writer.WriteNull();
                return;
            }

#if UNITY_EDITOR // AssetDatabase and EditorUtility are Editor-only
            if (UnityEditor.AssetDatabase.Contains(value))
            {
                // It's an asset (Material, Texture, Prefab, etc.)
                string path = UnityEditor.AssetDatabase.GetAssetPath(value);
                if (!string.IsNullOrEmpty(path))
                {
                    writer.WriteValue(path);
                }
                else
                {
                    // Asset exists but path couldn't be found? Write minimal info.
                    writer.WriteStartObject();
                    writer.WritePropertyName("name");
                    writer.WriteValue(value.name);
                    writer.WritePropertyName("instanceID");
                    writer.WriteValue(value.GetInstanceID());
                    writer.WritePropertyName("isAssetWithoutPath");
                    writer.WriteValue(true);
                    writer.WriteEndObject();
                }
            }
            else
            {
                // It's a scene object (GameObject, Component, etc.)
                writer.WriteStartObject();
                writer.WritePropertyName("name");
                writer.WriteValue(value.name);
                writer.WritePropertyName("instanceID");
                writer.WriteValue(value.GetInstanceID());
                writer.WriteEndObject();
            }
#else
            // Runtime fallback: Write basic info without AssetDatabase
            writer.WriteStartObject();
            writer.WritePropertyName("name");
            writer.WriteValue(value.name);
            writer.WritePropertyName("instanceID");
            writer.WriteValue(value.GetInstanceID());
             writer.WritePropertyName("warning");
            writer.WriteValue("UnityEngineObjectConverter running in non-Editor mode, asset path unavailable.");
            writer.WriteEndObject();
#endif
        }

        public override UnityEngine.Object ReadJson(JsonReader reader, Type objectType, UnityEngine.Object existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            if (reader.TokenType == JsonToken.Null)
            {
                return null;
            }

#if UNITY_EDITOR
            if (reader.TokenType == JsonToken.String)
            {
                // Assume it's an asset path
                string path = reader.Value.ToString();
                return UnityEditor.AssetDatabase.LoadAssetAtPath(path, objectType);
            }

            if (reader.TokenType == JsonToken.StartObject)
            {
                JObject jo = JObject.Load(reader);
                if (jo.TryGetValue("instanceID", out JToken idToken) && idToken.Type == JTokenType.Integer)
                {
                    int instanceId = idToken.ToObject<int>();
                    UnityEngine.Object obj = UnityEditor.EditorUtility.InstanceIDToObject(instanceId);
                    if (obj != null && objectType.IsAssignableFrom(obj.GetType()))
                    {
                        return obj;
                    }
                }
                // Could potentially try finding by name as a fallback if ID lookup fails/isn't present
                // but that's less reliable.
            }
#else
             // Runtime deserialization is tricky without AssetDatabase/EditorUtility
             // Maybe log a warning and return null or existingValue?
             Debug.LogWarning("UnityEngineObjectConverter cannot deserialize complex objects in non-Editor mode.");
             // Skip the token to avoid breaking the reader
             if (reader.TokenType == JsonToken.StartObject) JObject.Load(reader);
             else if (reader.TokenType == JsonToken.String) reader.ReadAsString(); 
             // Return null or existing value, depending on desired behavior
             return existingValue; 
#endif

            throw new JsonSerializationException($"Unexpected token type '{reader.TokenType}' when deserializing UnityEngine.Object");
        }
    }
}
```

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

```csharp
using System.Runtime.InteropServices;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Models;

namespace MCPForUnity.Editor.Windows
{
    // Editor window to display manual configuration instructions
    public class ManualConfigEditorWindow : EditorWindow
    {
        protected string configPath;
        protected string configJson;
        protected Vector2 scrollPos;
        protected bool pathCopied = false;
        protected bool jsonCopied = false;
        protected float copyFeedbackTimer = 0;
        protected McpClient mcpClient;

        public static void ShowWindow(string configPath, string configJson, McpClient mcpClient)
        {
            var window = GetWindow<ManualConfigEditorWindow>("Manual Configuration");
            window.configPath = configPath;
            window.configJson = configJson;
            window.mcpClient = mcpClient;
            window.minSize = new Vector2(500, 400);
            window.Show();
        }

        protected virtual void OnGUI()
        {
            scrollPos = EditorGUILayout.BeginScrollView(scrollPos);

            // Header with improved styling
            EditorGUILayout.Space(10);
            Rect titleRect = EditorGUILayout.GetControlRect(false, 30);
            EditorGUI.DrawRect(
                new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height),
                new Color(0.2f, 0.2f, 0.2f, 0.1f)
            );
            GUI.Label(
                new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height),
                (mcpClient?.name ?? "Unknown") + " Manual Configuration",
                EditorStyles.boldLabel
            );
            EditorGUILayout.Space(10);

            // Instructions with improved styling
            EditorGUILayout.BeginVertical(EditorStyles.helpBox);

            Rect headerRect = EditorGUILayout.GetControlRect(false, 24);
            EditorGUI.DrawRect(
                new Rect(headerRect.x, headerRect.y, headerRect.width, headerRect.height),
                new Color(0.1f, 0.1f, 0.1f, 0.2f)
            );
            GUI.Label(
                new Rect(
                    headerRect.x + 8,
                    headerRect.y + 4,
                    headerRect.width - 16,
                    headerRect.height
                ),
                "The automatic configuration failed. Please follow these steps:",
                EditorStyles.boldLabel
            );
            EditorGUILayout.Space(10);

            GUIStyle instructionStyle = new(EditorStyles.wordWrappedLabel)
            {
                margin = new RectOffset(10, 10, 5, 5),
            };

            EditorGUILayout.LabelField(
                "1. Open " + (mcpClient?.name ?? "Unknown") + " config file by either:",
                instructionStyle
            );
            if (mcpClient?.mcpType == McpTypes.ClaudeDesktop)
            {
                EditorGUILayout.LabelField(
                    "    a) Going to Settings > Developer > Edit Config",
                    instructionStyle
                );
            }
            else if (mcpClient?.mcpType == McpTypes.Cursor)
            {
                EditorGUILayout.LabelField(
                    "    a) Going to File > Preferences > Cursor Settings > MCP > Add new global MCP server",
                    instructionStyle
                );
            }
            else if (mcpClient?.mcpType == McpTypes.Windsurf)
            {
                EditorGUILayout.LabelField(
                    "    a) Going to File > Preferences > Windsurf Settings > MCP > Manage MCPs -> View raw config",
                    instructionStyle
                );
            }
            else if (mcpClient?.mcpType == McpTypes.Kiro)
            {
                EditorGUILayout.LabelField(
                    "    a) Going to File > Settings > Settings > Search for \"MCP\" > Open Workspace MCP Config",
                    instructionStyle
                );
            }
            else if (mcpClient?.mcpType == McpTypes.Codex)
            {
                EditorGUILayout.LabelField(
                    "    a) Running `codex config edit` in a terminal",
                    instructionStyle
                );
            }
            EditorGUILayout.LabelField("    OR", instructionStyle);
            EditorGUILayout.LabelField(
                "    b) Opening the configuration file at:",
                instructionStyle
            );

            // Path section with improved styling
            EditorGUILayout.BeginVertical(EditorStyles.helpBox);
            string displayPath;
            if (mcpClient != null)
            {
                if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
                {
                    displayPath = mcpClient.windowsConfigPath;
                }
                else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
                {
                    displayPath = string.IsNullOrEmpty(mcpClient.macConfigPath)

                        ? configPath

                        : mcpClient.macConfigPath;
                }
                else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
                {
                    displayPath = mcpClient.linuxConfigPath;
                }
                else
                {
                    displayPath = configPath;
                }
            }
            else
            {
                displayPath = configPath;
            }

            // Prevent text overflow by allowing the text field to wrap
            GUIStyle pathStyle = new(EditorStyles.textField) { wordWrap = true };

            EditorGUILayout.TextField(
                displayPath,
                pathStyle,
                GUILayout.Height(EditorGUIUtility.singleLineHeight)
            );

            // Copy button with improved styling
            EditorGUILayout.BeginHorizontal();
            GUILayout.FlexibleSpace();
            GUIStyle copyButtonStyle = new(GUI.skin.button)
            {
                padding = new RectOffset(15, 15, 5, 5),
                margin = new RectOffset(10, 10, 5, 5),
            };

            if (
                GUILayout.Button(
                    "Copy Path",
                    copyButtonStyle,
                    GUILayout.Height(25),
                    GUILayout.Width(100)
                )
            )
            {
                EditorGUIUtility.systemCopyBuffer = displayPath;
                pathCopied = true;
                copyFeedbackTimer = 2f;
            }

            if (
                GUILayout.Button(
                    "Open File",
                    copyButtonStyle,
                    GUILayout.Height(25),
                    GUILayout.Width(100)
                )
            )
            {
                // Open the file using the system's default application
                System.Diagnostics.Process.Start(
                    new System.Diagnostics.ProcessStartInfo
                    {
                        FileName = displayPath,
                        UseShellExecute = true,
                    }
                );
            }

            if (pathCopied)
            {
                GUIStyle feedbackStyle = new(EditorStyles.label);
                feedbackStyle.normal.textColor = Color.green;
                EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60));
            }

            EditorGUILayout.EndHorizontal();
            EditorGUILayout.EndVertical();

            EditorGUILayout.Space(10);

            string configLabel = mcpClient?.mcpType == McpTypes.Codex
                ? "2. Paste the following TOML configuration:"
                : "2. Paste the following JSON configuration:";
            EditorGUILayout.LabelField(configLabel, instructionStyle);

            // JSON section with improved styling
            EditorGUILayout.BeginVertical(EditorStyles.helpBox);

            // Improved text area for JSON with syntax highlighting colors
            GUIStyle jsonStyle = new(EditorStyles.textArea)
            {
                font = EditorStyles.boldFont,
                wordWrap = true,
            };
            jsonStyle.normal.textColor = new Color(0.3f, 0.6f, 0.9f); // Syntax highlighting blue

            // Draw the JSON in a text area with a taller height for better readability
            EditorGUILayout.TextArea(configJson, jsonStyle, GUILayout.Height(200));

            // Copy JSON button with improved styling
            EditorGUILayout.BeginHorizontal();
            GUILayout.FlexibleSpace();

            if (
                GUILayout.Button(
                    "Copy JSON",
                    copyButtonStyle,
                    GUILayout.Height(25),
                    GUILayout.Width(100)
                )
            )
            {
                EditorGUIUtility.systemCopyBuffer = configJson;
                jsonCopied = true;
                copyFeedbackTimer = 2f;
            }

            if (jsonCopied)
            {
                GUIStyle feedbackStyle = new(EditorStyles.label);
                feedbackStyle.normal.textColor = Color.green;
                EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60));
            }

            EditorGUILayout.EndHorizontal();
            EditorGUILayout.EndVertical();

            EditorGUILayout.Space(10);
            EditorGUILayout.LabelField(
                "3. Save the file and restart " + (mcpClient?.name ?? "Unknown"),
                instructionStyle
            );

            EditorGUILayout.EndVertical();

            EditorGUILayout.Space(10);

            // Close button at the bottom
            EditorGUILayout.BeginHorizontal();
            GUILayout.FlexibleSpace();
            if (GUILayout.Button("Close", GUILayout.Height(30), GUILayout.Width(100)))
            {
                Close();
            }
            GUILayout.FlexibleSpace();
            EditorGUILayout.EndHorizontal();

            EditorGUILayout.EndScrollView();
        }

        protected virtual void Update()
        {
            // Handle the feedback message timer
            if (copyFeedbackTimer > 0)
            {
                copyFeedbackTimer -= Time.deltaTime;
                if (copyFeedbackTimer <= 0)
                {
                    pathCopied = false;
                    jsonCopied = false;
                    Repaint();
                }
            }
        }
    }
}

```

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

```csharp
using System.Runtime.InteropServices;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Models;

namespace MCPForUnity.Editor.Windows
{
    // Editor window to display manual configuration instructions
    public class ManualConfigEditorWindow : EditorWindow
    {
        protected string configPath;
        protected string configJson;
        protected Vector2 scrollPos;
        protected bool pathCopied = false;
        protected bool jsonCopied = false;
        protected float copyFeedbackTimer = 0;
        protected McpClient mcpClient;

        public static void ShowWindow(string configPath, string configJson, McpClient mcpClient)
        {
            var window = GetWindow<ManualConfigEditorWindow>("Manual Configuration");
            window.configPath = configPath;
            window.configJson = configJson;
            window.mcpClient = mcpClient;
            window.minSize = new Vector2(500, 400);
            window.Show();
        }

        protected virtual void OnGUI()
        {
            scrollPos = EditorGUILayout.BeginScrollView(scrollPos);

            // Header with improved styling
            EditorGUILayout.Space(10);
            Rect titleRect = EditorGUILayout.GetControlRect(false, 30);
            EditorGUI.DrawRect(
                new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height),
                new Color(0.2f, 0.2f, 0.2f, 0.1f)
            );
            GUI.Label(
                new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height),
                (mcpClient?.name ?? "Unknown") + " Manual Configuration",
                EditorStyles.boldLabel
            );
            EditorGUILayout.Space(10);

            // Instructions with improved styling
            EditorGUILayout.BeginVertical(EditorStyles.helpBox);

            Rect headerRect = EditorGUILayout.GetControlRect(false, 24);
            EditorGUI.DrawRect(
                new Rect(headerRect.x, headerRect.y, headerRect.width, headerRect.height),
                new Color(0.1f, 0.1f, 0.1f, 0.2f)
            );
            GUI.Label(
                new Rect(
                    headerRect.x + 8,
                    headerRect.y + 4,
                    headerRect.width - 16,
                    headerRect.height
                ),
                "The automatic configuration failed. Please follow these steps:",
                EditorStyles.boldLabel
            );
            EditorGUILayout.Space(10);

            GUIStyle instructionStyle = new(EditorStyles.wordWrappedLabel)
            {
                margin = new RectOffset(10, 10, 5, 5),
            };

            EditorGUILayout.LabelField(
                "1. Open " + (mcpClient?.name ?? "Unknown") + " config file by either:",
                instructionStyle
            );
            if (mcpClient?.mcpType == McpTypes.ClaudeDesktop)
            {
                EditorGUILayout.LabelField(
                    "    a) Going to Settings > Developer > Edit Config",
                    instructionStyle
                );
            }
            else if (mcpClient?.mcpType == McpTypes.Cursor)
            {
                EditorGUILayout.LabelField(
                    "    a) Going to File > Preferences > Cursor Settings > MCP > Add new global MCP server",
                    instructionStyle
                );
            }
            else if (mcpClient?.mcpType == McpTypes.Windsurf)
            {
                EditorGUILayout.LabelField(
                    "    a) Going to File > Preferences > Windsurf Settings > MCP > Manage MCPs -> View raw config",
                    instructionStyle
                );
            }
            else if (mcpClient?.mcpType == McpTypes.Kiro)
            {
                EditorGUILayout.LabelField(
                    "    a) Going to File > Settings > Settings > Search for \"MCP\" > Open Workspace MCP Config",
                    instructionStyle
                );
            }
            else if (mcpClient?.mcpType == McpTypes.Codex)
            {
                EditorGUILayout.LabelField(
                    "    a) Running `codex config edit` in a terminal",
                    instructionStyle
                );
            }
            EditorGUILayout.LabelField("    OR", instructionStyle);
            EditorGUILayout.LabelField(
                "    b) Opening the configuration file at:",
                instructionStyle
            );

            // Path section with improved styling
            EditorGUILayout.BeginVertical(EditorStyles.helpBox);
            string displayPath;
            if (mcpClient != null)
            {
                if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
                {
                    displayPath = mcpClient.windowsConfigPath;
                }
                else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
                {
                    displayPath = string.IsNullOrEmpty(mcpClient.macConfigPath)

                        ? configPath

                        : mcpClient.macConfigPath;
                }
                else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
                {
                    displayPath = mcpClient.linuxConfigPath;
                }
                else
                {
                    displayPath = configPath;
                }
            }
            else
            {
                displayPath = configPath;
            }

            // Prevent text overflow by allowing the text field to wrap
            GUIStyle pathStyle = new(EditorStyles.textField) { wordWrap = true };

            EditorGUILayout.TextField(
                displayPath,
                pathStyle,
                GUILayout.Height(EditorGUIUtility.singleLineHeight)
            );

            // Copy button with improved styling
            EditorGUILayout.BeginHorizontal();
            GUILayout.FlexibleSpace();
            GUIStyle copyButtonStyle = new(GUI.skin.button)
            {
                padding = new RectOffset(15, 15, 5, 5),
                margin = new RectOffset(10, 10, 5, 5),
            };

            if (
                GUILayout.Button(
                    "Copy Path",
                    copyButtonStyle,
                    GUILayout.Height(25),
                    GUILayout.Width(100)
                )
            )
            {
                EditorGUIUtility.systemCopyBuffer = displayPath;
                pathCopied = true;
                copyFeedbackTimer = 2f;
            }

            if (
                GUILayout.Button(
                    "Open File",
                    copyButtonStyle,
                    GUILayout.Height(25),
                    GUILayout.Width(100)
                )
            )
            {
                // Open the file using the system's default application
                System.Diagnostics.Process.Start(
                    new System.Diagnostics.ProcessStartInfo
                    {
                        FileName = displayPath,
                        UseShellExecute = true,
                    }
                );
            }

            if (pathCopied)
            {
                GUIStyle feedbackStyle = new(EditorStyles.label);
                feedbackStyle.normal.textColor = Color.green;
                EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60));
            }

            EditorGUILayout.EndHorizontal();
            EditorGUILayout.EndVertical();

            EditorGUILayout.Space(10);

            string configLabel = mcpClient?.mcpType == McpTypes.Codex
                ? "2. Paste the following TOML configuration:"
                : "2. Paste the following JSON configuration:";
            EditorGUILayout.LabelField(configLabel, instructionStyle);

            // JSON section with improved styling
            EditorGUILayout.BeginVertical(EditorStyles.helpBox);

            // Improved text area for JSON with syntax highlighting colors
            GUIStyle jsonStyle = new(EditorStyles.textArea)
            {
                font = EditorStyles.boldFont,
                wordWrap = true,
            };
            jsonStyle.normal.textColor = new Color(0.3f, 0.6f, 0.9f); // Syntax highlighting blue

            // Draw the JSON in a text area with a taller height for better readability
            EditorGUILayout.TextArea(configJson, jsonStyle, GUILayout.Height(200));

            // Copy JSON button with improved styling
            EditorGUILayout.BeginHorizontal();
            GUILayout.FlexibleSpace();

            if (
                GUILayout.Button(
                    "Copy JSON",
                    copyButtonStyle,
                    GUILayout.Height(25),
                    GUILayout.Width(100)
                )
            )
            {
                EditorGUIUtility.systemCopyBuffer = configJson;
                jsonCopied = true;
                copyFeedbackTimer = 2f;
            }

            if (jsonCopied)
            {
                GUIStyle feedbackStyle = new(EditorStyles.label);
                feedbackStyle.normal.textColor = Color.green;
                EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60));
            }

            EditorGUILayout.EndHorizontal();
            EditorGUILayout.EndVertical();

            EditorGUILayout.Space(10);
            EditorGUILayout.LabelField(
                "3. Save the file and restart " + (mcpClient?.name ?? "Unknown"),
                instructionStyle
            );

            EditorGUILayout.EndVertical();

            EditorGUILayout.Space(10);

            // Close button at the bottom
            EditorGUILayout.BeginHorizontal();
            GUILayout.FlexibleSpace();
            if (GUILayout.Button("Close", GUILayout.Height(30), GUILayout.Width(100)))
            {
                Close();
            }
            GUILayout.FlexibleSpace();
            EditorGUILayout.EndHorizontal();

            EditorGUILayout.EndScrollView();
        }

        protected virtual void Update()
        {
            // Handle the feedback message timer
            if (copyFeedbackTimer > 0)
            {
                copyFeedbackTimer -= Time.deltaTime;
                if (copyFeedbackTimer <= 0)
                {
                    pathCopied = false;
                    jsonCopied = false;
                    Repaint();
                }
            }
        }
    }
}

```

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

```csharp
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Dependencies;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;

namespace MCPForUnity.Editor.Helpers
{
    /// <summary>
    /// Shared helper for MCP client configuration management with sophisticated
    /// logic for preserving existing configs and handling different client types
    /// </summary>
    public static class McpConfigurationHelper
    {
        private const string LOCK_CONFIG_KEY = "MCPForUnity.LockCursorConfig";

        /// <summary>
        /// Writes MCP configuration to the specified path using sophisticated logic
        /// that preserves existing configuration and only writes when necessary
        /// </summary>
        public static string WriteMcpConfiguration(string pythonDir, string configPath, McpClient mcpClient = null)
        {
            // 0) Respect explicit lock (hidden pref or UI toggle)
            try
            {
                if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false))
                    return "Skipped (locked)";
            }
            catch { }

            JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented };

            // Read existing config if it exists
            string existingJson = "{}";
            if (File.Exists(configPath))
            {
                try
                {
                    existingJson = File.ReadAllText(configPath);
                }
                catch (Exception e)
                {
                    Debug.LogWarning($"Error reading existing config: {e.Message}.");
                }
            }

            // Parse the existing JSON while preserving all properties
            dynamic existingConfig;
            try
            {
                if (string.IsNullOrWhiteSpace(existingJson))
                {
                    existingConfig = new JObject();
                }
                else
                {
                    existingConfig = JsonConvert.DeserializeObject(existingJson) ?? new JObject();
                }
            }
            catch
            {
                // If user has partial/invalid JSON (e.g., mid-edit), start from a fresh object
                if (!string.IsNullOrWhiteSpace(existingJson))
                {
                    Debug.LogWarning("UnityMCP: Configuration file could not be parsed; rewriting server block.");
                }
                existingConfig = new JObject();
            }

            // Determine existing entry references (command/args)
            string existingCommand = null;
            string[] existingArgs = null;
            bool isVSCode = (mcpClient?.mcpType == McpTypes.VSCode);
            try
            {
                if (isVSCode)
                {
                    existingCommand = existingConfig?.servers?.unityMCP?.command?.ToString();
                    existingArgs = existingConfig?.servers?.unityMCP?.args?.ToObject<string[]>();
                }
                else
                {
                    existingCommand = existingConfig?.mcpServers?.unityMCP?.command?.ToString();
                    existingArgs = existingConfig?.mcpServers?.unityMCP?.args?.ToObject<string[]>();
                }
            }
            catch { }

            // 1) Start from existing, only fill gaps (prefer trusted resolver)
            string uvPath = ServerInstaller.FindUvPath();
            // Optionally trust existingCommand if it looks like uv/uv.exe
            try
            {
                var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant();
                if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand))
                {
                    uvPath = existingCommand;
                }
            }
            catch { }
            if (uvPath == null) return "UV package manager not found. Please install UV first.";
            string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs);

            // 2) Canonical args order
            var newArgs = new[] { "run", "--directory", serverSrc, "server.py" };

            // 3) Only write if changed
            bool changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal)
                || !ArgsEqual(existingArgs, newArgs);
            if (!changed)
            {
                return "Configured successfully"; // nothing to do
            }

            // 4) Ensure containers exist and write back minimal changes
            JObject existingRoot;
            if (existingConfig is JObject eo)
                existingRoot = eo;
            else
                existingRoot = JObject.FromObject(existingConfig);

            existingRoot = ConfigJsonBuilder.ApplyUnityServerToExistingConfig(existingRoot, uvPath, serverSrc, mcpClient);

            string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings);

            McpConfigFileHelper.WriteAtomicFile(configPath, mergedJson);

            try
            {
                if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath);
                EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc);
            }
            catch { }

            return "Configured successfully";
        }

        /// <summary>
        /// Configures a Codex client with sophisticated TOML handling
        /// </summary>
        public static string ConfigureCodexClient(string pythonDir, string configPath, McpClient mcpClient)
        {
            try
            {
                if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false))
                    return "Skipped (locked)";
            }
            catch { }

            string existingToml = string.Empty;
            if (File.Exists(configPath))
            {
                try
                {
                    existingToml = File.ReadAllText(configPath);
                }
                catch (Exception e)
                {
                    Debug.LogWarning($"UnityMCP: Failed to read Codex config '{configPath}': {e.Message}");
                    existingToml = string.Empty;
                }
            }

            string existingCommand = null;
            string[] existingArgs = null;
            if (!string.IsNullOrWhiteSpace(existingToml))
            {
                CodexConfigHelper.TryParseCodexServer(existingToml, out existingCommand, out existingArgs);
            }

            string uvPath = ServerInstaller.FindUvPath();
            try
            {
                var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant();
                if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand))
                {
                    uvPath = existingCommand;
                }
            }
            catch { }

            if (uvPath == null)
            {
                return "UV package manager not found. Please install UV first.";
            }

            string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs);
            var newArgs = new[] { "run", "--directory", serverSrc, "server.py" };

            bool changed = true;
            if (!string.IsNullOrEmpty(existingCommand) && existingArgs != null)
            {
                changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal)
                    || !ArgsEqual(existingArgs, newArgs);
            }

            if (!changed)
            {
                return "Configured successfully";
            }

            string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, uvPath, serverSrc);

            McpConfigFileHelper.WriteAtomicFile(configPath, updatedToml);

            try
            {
                if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath);
                EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc);
            }
            catch { }

            return "Configured successfully";
        }

        /// <summary>
        /// Validates UV binary by running --version command
        /// </summary>
        private static bool IsValidUvBinary(string path)
        {
            try
            {
                if (!File.Exists(path)) return false;
                var psi = new System.Diagnostics.ProcessStartInfo
                {
                    FileName = path,
                    Arguments = "--version",
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    CreateNoWindow = true
                };
                using var p = System.Diagnostics.Process.Start(psi);
                if (p == null) return false;
                if (!p.WaitForExit(3000)) { try { p.Kill(); } catch { } return false; }
                if (p.ExitCode != 0) return false;
                string output = p.StandardOutput.ReadToEnd().Trim();
                return output.StartsWith("uv ");
            }
            catch { return false; }
        }

        /// <summary>
        /// Compares two string arrays for equality
        /// </summary>
        private static bool ArgsEqual(string[] a, string[] b)
        {
            if (a == null || b == null) return a == b;
            if (a.Length != b.Length) return false;
            for (int i = 0; i < a.Length; i++)
            {
                if (!string.Equals(a[i], b[i], StringComparison.Ordinal)) return false;
            }
            return true;
        }

        /// <summary>
        /// Gets the appropriate config file path for the given MCP client based on OS
        /// </summary>
        public static string GetClientConfigPath(McpClient mcpClient)
        {
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                return mcpClient.windowsConfigPath;
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                return string.IsNullOrEmpty(mcpClient.macConfigPath)
                    ? mcpClient.linuxConfigPath
                    : mcpClient.macConfigPath;
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                return mcpClient.linuxConfigPath;
            }
            else
            {
                return mcpClient.linuxConfigPath; // fallback
            }
        }

        /// <summary>
        /// Creates the directory for the config file if it doesn't exist
        /// </summary>
        public static void EnsureConfigDirectoryExists(string configPath)
        {
            Directory.CreateDirectory(Path.GetDirectoryName(configPath));
        }
    }
}

```

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

```csharp
using System;
using NUnit.Framework;
using UnityEditor;
using MCPForUnity.Editor.Services;

namespace MCPForUnityTests.Editor.Services
{
    public class PackageUpdateServiceTests
    {
        private PackageUpdateService _service;
        private const string TestLastCheckDateKey = "MCPForUnity.LastUpdateCheck";
        private const string TestCachedVersionKey = "MCPForUnity.LatestKnownVersion";

        [SetUp]
        public void SetUp()
        {
            _service = new PackageUpdateService();

            // Clean up any existing test data
            CleanupEditorPrefs();
        }

        [TearDown]
        public void TearDown()
        {
            // Clean up test data
            CleanupEditorPrefs();
        }

        private void CleanupEditorPrefs()
        {
            if (EditorPrefs.HasKey(TestLastCheckDateKey))
            {
                EditorPrefs.DeleteKey(TestLastCheckDateKey);
            }
            if (EditorPrefs.HasKey(TestCachedVersionKey))
            {
                EditorPrefs.DeleteKey(TestCachedVersionKey);
            }
        }

        [Test]
        public void IsNewerVersion_ReturnsTrue_WhenMajorVersionIsNewer()
        {
            bool result = _service.IsNewerVersion("2.0.0", "1.0.0");
            Assert.IsTrue(result, "2.0.0 should be newer than 1.0.0");
        }

        [Test]
        public void IsNewerVersion_ReturnsTrue_WhenMinorVersionIsNewer()
        {
            bool result = _service.IsNewerVersion("1.2.0", "1.1.0");
            Assert.IsTrue(result, "1.2.0 should be newer than 1.1.0");
        }

        [Test]
        public void IsNewerVersion_ReturnsTrue_WhenPatchVersionIsNewer()
        {
            bool result = _service.IsNewerVersion("1.0.2", "1.0.1");
            Assert.IsTrue(result, "1.0.2 should be newer than 1.0.1");
        }

        [Test]
        public void IsNewerVersion_ReturnsFalse_WhenVersionsAreEqual()
        {
            bool result = _service.IsNewerVersion("1.0.0", "1.0.0");
            Assert.IsFalse(result, "Same versions should return false");
        }

        [Test]
        public void IsNewerVersion_ReturnsFalse_WhenVersionIsOlder()
        {
            bool result = _service.IsNewerVersion("1.0.0", "2.0.0");
            Assert.IsFalse(result, "1.0.0 should not be newer than 2.0.0");
        }

        [Test]
        public void IsNewerVersion_HandlesVersionPrefix_v()
        {
            bool result = _service.IsNewerVersion("v2.0.0", "v1.0.0");
            Assert.IsTrue(result, "Should handle 'v' prefix correctly");
        }

        [Test]
        public void IsNewerVersion_HandlesVersionPrefix_V()
        {
            bool result = _service.IsNewerVersion("V2.0.0", "V1.0.0");
            Assert.IsTrue(result, "Should handle 'V' prefix correctly");
        }

        [Test]
        public void IsNewerVersion_HandlesMixedPrefixes()
        {
            bool result = _service.IsNewerVersion("v2.0.0", "1.0.0");
            Assert.IsTrue(result, "Should handle mixed prefixes correctly");
        }

        [Test]
        public void IsNewerVersion_ComparesCorrectly_WhenMajorDiffers()
        {
            bool result1 = _service.IsNewerVersion("10.0.0", "9.0.0");
            bool result2 = _service.IsNewerVersion("2.0.0", "10.0.0");

            Assert.IsTrue(result1, "10.0.0 should be newer than 9.0.0");
            Assert.IsFalse(result2, "2.0.0 should not be newer than 10.0.0");
        }

        [Test]
        public void IsNewerVersion_ReturnsFalse_OnInvalidVersionFormat()
        {
            // Service should handle errors gracefully
            bool result = _service.IsNewerVersion("invalid", "1.0.0");
            Assert.IsFalse(result, "Should return false for invalid version format");
        }

        [Test]
        public void CheckForUpdate_ReturnsCachedVersion_WhenCacheIsValid()
        {
            // Arrange: Set up valid cache
            string today = DateTime.Now.ToString("yyyy-MM-dd");
            string cachedVersion = "5.5.5";
            EditorPrefs.SetString(TestLastCheckDateKey, today);
            EditorPrefs.SetString(TestCachedVersionKey, cachedVersion);

            // Act
            var result = _service.CheckForUpdate("5.0.0");

            // Assert
            Assert.IsTrue(result.CheckSucceeded, "Check should succeed with valid cache");
            Assert.AreEqual(cachedVersion, result.LatestVersion, "Should return cached version");
            Assert.IsTrue(result.UpdateAvailable, "Update should be available (5.5.5 > 5.0.0)");
        }

        [Test]
        public void CheckForUpdate_DetectsUpdateAvailable_WhenNewerVersionCached()
        {
            // Arrange
            string today = DateTime.Now.ToString("yyyy-MM-dd");
            EditorPrefs.SetString(TestLastCheckDateKey, today);
            EditorPrefs.SetString(TestCachedVersionKey, "6.0.0");

            // Act
            var result = _service.CheckForUpdate("5.0.0");

            // Assert
            Assert.IsTrue(result.UpdateAvailable, "Should detect update is available");
            Assert.AreEqual("6.0.0", result.LatestVersion);
        }

        [Test]
        public void CheckForUpdate_DetectsNoUpdate_WhenVersionsMatch()
        {
            // Arrange
            string today = DateTime.Now.ToString("yyyy-MM-dd");
            EditorPrefs.SetString(TestLastCheckDateKey, today);
            EditorPrefs.SetString(TestCachedVersionKey, "5.0.0");

            // Act
            var result = _service.CheckForUpdate("5.0.0");

            // Assert
            Assert.IsFalse(result.UpdateAvailable, "Should detect no update needed");
            Assert.AreEqual("5.0.0", result.LatestVersion);
        }

        [Test]
        public void CheckForUpdate_DetectsNoUpdate_WhenCurrentVersionIsNewer()
        {
            // Arrange
            string today = DateTime.Now.ToString("yyyy-MM-dd");
            EditorPrefs.SetString(TestLastCheckDateKey, today);
            EditorPrefs.SetString(TestCachedVersionKey, "5.0.0");

            // Act
            var result = _service.CheckForUpdate("6.0.0");

            // Assert
            Assert.IsFalse(result.UpdateAvailable, "Should detect no update when current is newer");
            Assert.AreEqual("5.0.0", result.LatestVersion);
        }

        [Test]
        public void CheckForUpdate_IgnoresExpiredCache_AndAttemptsFreshFetch()
        {
            // Arrange: Set cache from yesterday (expired)
            string yesterday = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd");
            string cachedVersion = "4.0.0";
            EditorPrefs.SetString(TestLastCheckDateKey, yesterday);
            EditorPrefs.SetString(TestCachedVersionKey, cachedVersion);

            // Act
            var result = _service.CheckForUpdate("5.0.0");

            // Assert
            Assert.IsNotNull(result, "Should return a result");
            
            // If the check succeeded (network available), verify it didn't use the expired cache
            if (result.CheckSucceeded)
            {
                Assert.AreNotEqual(cachedVersion, result.LatestVersion, 
                    "Should not return expired cached version when fresh fetch succeeds");
                Assert.IsNotNull(result.LatestVersion, "Should have fetched a new version");
            }
            else
            {
                // If offline, check should fail (not succeed with cached data)
                Assert.IsFalse(result.UpdateAvailable, 
                    "Should not report update available when fetch fails and cache is expired");
            }
        }

        [Test]
        public void CheckForUpdate_ReturnsAssetStoreMessage_ForNonGitInstallations()
        {
            // Note: This test verifies the service behavior when IsGitInstallation() returns false.
            // Since the actual result depends on package installation method, we create a mock
            // implementation to test this specific code path.
            
            var mockService = new MockAssetStorePackageUpdateService();
            
            // Act
            var result = mockService.CheckForUpdate("5.0.0");

            // Assert
            Assert.IsFalse(result.CheckSucceeded, "Check should not succeed for Asset Store installs");
            Assert.IsFalse(result.UpdateAvailable, "No update should be reported for Asset Store installs");
            Assert.AreEqual("Asset Store installations are updated via Unity Asset Store", result.Message,
                "Should return Asset Store update message");
            Assert.IsNull(result.LatestVersion, "Latest version should be null for Asset Store installs");
        }

        [Test]
        public void ClearCache_RemovesAllCachedData()
        {
            // Arrange: Set up cache
            EditorPrefs.SetString(TestLastCheckDateKey, DateTime.Now.ToString("yyyy-MM-dd"));
            EditorPrefs.SetString(TestCachedVersionKey, "5.0.0");

            // Verify cache exists
            Assert.IsTrue(EditorPrefs.HasKey(TestLastCheckDateKey), "Cache should exist before clearing");
            Assert.IsTrue(EditorPrefs.HasKey(TestCachedVersionKey), "Cache should exist before clearing");

            // Act
            _service.ClearCache();

            // Assert
            Assert.IsFalse(EditorPrefs.HasKey(TestLastCheckDateKey), "Date cache should be cleared");
            Assert.IsFalse(EditorPrefs.HasKey(TestCachedVersionKey), "Version cache should be cleared");
        }

        [Test]
        public void ClearCache_DoesNotThrow_WhenNoCacheExists()
        {
            // Ensure no cache exists
            CleanupEditorPrefs();

            // Act & Assert - should not throw
            Assert.DoesNotThrow(() => _service.ClearCache(), "Should not throw when clearing non-existent cache");
        }
    }

    /// <summary>
    /// Mock implementation of IPackageUpdateService that simulates Asset Store installation behavior
    /// </summary>
    internal class MockAssetStorePackageUpdateService : IPackageUpdateService
    {
        public UpdateCheckResult CheckForUpdate(string currentVersion)
        {
            // Simulate Asset Store installation (IsGitInstallation returns false)
            return new UpdateCheckResult
            {
                CheckSucceeded = false,
                UpdateAvailable = false,
                Message = "Asset Store installations are updated via Unity Asset Store"
            };
        }

        public bool IsNewerVersion(string version1, string version2)
        {
            // Not used in the Asset Store test, but required by interface
            return false;
        }

        public bool IsGitInstallation()
        {
            // Simulate non-Git installation (Asset Store)
            return false;
        }

        public void ClearCache()
        {
            // Not used in the Asset Store test, but required by interface
        }
    }
}

```

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

```csharp
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Dependencies;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;

namespace MCPForUnity.Editor.Helpers
{
    /// <summary>
    /// Shared helper for MCP client configuration management with sophisticated
    /// logic for preserving existing configs and handling different client types
    /// </summary>
    public static class McpConfigurationHelper
    {
        private const string LOCK_CONFIG_KEY = "MCPForUnity.LockCursorConfig";

        /// <summary>
        /// Writes MCP configuration to the specified path using sophisticated logic
        /// that preserves existing configuration and only writes when necessary
        /// </summary>
        public static string WriteMcpConfiguration(string pythonDir, string configPath, McpClient mcpClient = null)
        {
            // 0) Respect explicit lock (hidden pref or UI toggle)
            try
            {
                if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false))
                    return "Skipped (locked)";
            }
            catch { }

            JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented };

            // Read existing config if it exists
            string existingJson = "{}";
            if (File.Exists(configPath))
            {
                try
                {
                    existingJson = File.ReadAllText(configPath);
                }
                catch (Exception e)
                {
                    Debug.LogWarning($"Error reading existing config: {e.Message}.");
                }
            }

            // Parse the existing JSON while preserving all properties
            dynamic existingConfig;
            try
            {
                if (string.IsNullOrWhiteSpace(existingJson))
                {
                    existingConfig = new JObject();
                }
                else
                {
                    existingConfig = JsonConvert.DeserializeObject(existingJson) ?? new JObject();
                }
            }
            catch
            {
                // If user has partial/invalid JSON (e.g., mid-edit), start from a fresh object
                if (!string.IsNullOrWhiteSpace(existingJson))
                {
                    Debug.LogWarning("UnityMCP: Configuration file could not be parsed; rewriting server block.");
                }
                existingConfig = new JObject();
            }

            // Determine existing entry references (command/args)
            string existingCommand = null;
            string[] existingArgs = null;
            bool isVSCode = (mcpClient?.mcpType == McpTypes.VSCode);
            try
            {
                if (isVSCode)
                {
                    existingCommand = existingConfig?.servers?.unityMCP?.command?.ToString();
                    existingArgs = existingConfig?.servers?.unityMCP?.args?.ToObject<string[]>();
                }
                else
                {
                    existingCommand = existingConfig?.mcpServers?.unityMCP?.command?.ToString();
                    existingArgs = existingConfig?.mcpServers?.unityMCP?.args?.ToObject<string[]>();
                }
            }
            catch { }

            // 1) Start from existing, only fill gaps (prefer trusted resolver)
            string uvPath = ServerInstaller.FindUvPath();
            // Optionally trust existingCommand if it looks like uv/uv.exe
            try
            {
                var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant();
                if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand))
                {
                    uvPath = existingCommand;
                }
            }
            catch { }
            if (uvPath == null) return "UV package manager not found. Please install UV first.";
            string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs);

            // 2) Canonical args order
            var newArgs = new[] { "run", "--directory", serverSrc, "server.py" };

            // 3) Only write if changed
            bool changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal)
                || !ArgsEqual(existingArgs, newArgs);
            if (!changed)
            {
                return "Configured successfully"; // nothing to do
            }

            // 4) Ensure containers exist and write back minimal changes
            JObject existingRoot;
            if (existingConfig is JObject eo)
                existingRoot = eo;
            else
                existingRoot = JObject.FromObject(existingConfig);

            existingRoot = ConfigJsonBuilder.ApplyUnityServerToExistingConfig(existingRoot, uvPath, serverSrc, mcpClient);

            string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings);

            McpConfigFileHelper.WriteAtomicFile(configPath, mergedJson);

            try
            {
                if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath);
                EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc);
            }
            catch { }

            return "Configured successfully";
        }

        /// <summary>
        /// Configures a Codex client with sophisticated TOML handling
        /// </summary>
        public static string ConfigureCodexClient(string pythonDir, string configPath, McpClient mcpClient)
        {
            try
            {
                if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false))
                    return "Skipped (locked)";
            }
            catch { }

            string existingToml = string.Empty;
            if (File.Exists(configPath))
            {
                try
                {
                    existingToml = File.ReadAllText(configPath);
                }
                catch (Exception e)
                {
                    Debug.LogWarning($"UnityMCP: Failed to read Codex config '{configPath}': {e.Message}");
                    existingToml = string.Empty;
                }
            }

            string existingCommand = null;
            string[] existingArgs = null;
            if (!string.IsNullOrWhiteSpace(existingToml))
            {
                CodexConfigHelper.TryParseCodexServer(existingToml, out existingCommand, out existingArgs);
            }

            string uvPath = ServerInstaller.FindUvPath();
            try
            {
                var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant();
                if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand))
                {
                    uvPath = existingCommand;
                }
            }
            catch { }

            if (uvPath == null)
            {
                return "UV package manager not found. Please install UV first.";
            }

            string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs);
            var newArgs = new[] { "run", "--directory", serverSrc, "server.py" };

            bool changed = true;
            if (!string.IsNullOrEmpty(existingCommand) && existingArgs != null)
            {
                changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal)
                    || !ArgsEqual(existingArgs, newArgs);
            }

            if (!changed)
            {
                return "Configured successfully";
            }

            string codexBlock = CodexConfigHelper.BuildCodexServerBlock(uvPath, serverSrc);
            string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, codexBlock);

            McpConfigFileHelper.WriteAtomicFile(configPath, updatedToml);

            try
            {
                if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath);
                EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc);
            }
            catch { }

            return "Configured successfully";
        }

        /// <summary>
        /// Validates UV binary by running --version command
        /// </summary>
        private static bool IsValidUvBinary(string path)
        {
            try
            {
                if (!File.Exists(path)) return false;
                var psi = new System.Diagnostics.ProcessStartInfo
                {
                    FileName = path,
                    Arguments = "--version",
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    CreateNoWindow = true
                };
                using var p = System.Diagnostics.Process.Start(psi);
                if (p == null) return false;
                if (!p.WaitForExit(3000)) { try { p.Kill(); } catch { } return false; }
                if (p.ExitCode != 0) return false;
                string output = p.StandardOutput.ReadToEnd().Trim();
                return output.StartsWith("uv ");
            }
            catch { return false; }
        }

        /// <summary>
        /// Compares two string arrays for equality
        /// </summary>
        private static bool ArgsEqual(string[] a, string[] b)
        {
            if (a == null || b == null) return a == b;
            if (a.Length != b.Length) return false;
            for (int i = 0; i < a.Length; i++)
            {
                if (!string.Equals(a[i], b[i], StringComparison.Ordinal)) return false;
            }
            return true;
        }

        /// <summary>
        /// Gets the appropriate config file path for the given MCP client based on OS
        /// </summary>
        public static string GetClientConfigPath(McpClient mcpClient)
        {
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                return mcpClient.windowsConfigPath;
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                return string.IsNullOrEmpty(mcpClient.macConfigPath)
                    ? mcpClient.linuxConfigPath
                    : mcpClient.macConfigPath;
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                return mcpClient.linuxConfigPath;
            }
            else
            {
                return mcpClient.linuxConfigPath; // fallback
            }
        }

        /// <summary>
        /// Creates the directory for the config file if it doesn't exist
        /// </summary>
        public static void EnsureConfigDirectoryExists(string configPath)
        {
            Directory.CreateDirectory(Path.GetDirectoryName(configPath));
        }
    }
}

```

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

```csharp
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using UnityEditor;

namespace MCPForUnity.Editor.Helpers
{
    internal static class ExecPath
    {
        private const string PrefClaude = "MCPForUnity.ClaudeCliPath";

        // Resolve Claude CLI absolute path. Pref → env → common locations → PATH.
        internal static string ResolveClaude()
        {
            try
            {
                string pref = EditorPrefs.GetString(PrefClaude, string.Empty);
                if (!string.IsNullOrEmpty(pref) && File.Exists(pref)) return pref;
            }
            catch { }

            string env = Environment.GetEnvironmentVariable("CLAUDE_CLI");
            if (!string.IsNullOrEmpty(env) && File.Exists(env)) return env;

            if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
                string[] candidates =
                {
                    "/opt/homebrew/bin/claude",
                    "/usr/local/bin/claude",
                    Path.Combine(home, ".local", "bin", "claude"),
                };
                foreach (string c in candidates) { if (File.Exists(c)) return c; }
                // Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude
                string nvmClaude = ResolveClaudeFromNvm(home);
                if (!string.IsNullOrEmpty(nvmClaude)) return nvmClaude;
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
                return Which("claude", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin");
#else
                return null;
#endif
            }

            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
#if UNITY_EDITOR_WIN
                // Common npm global locations
                string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
                string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
                string[] candidates =
                {
                    // Prefer .cmd (most reliable from non-interactive processes)
                    Path.Combine(appData, "npm", "claude.cmd"),
                    Path.Combine(localAppData, "npm", "claude.cmd"),
                    // Fall back to PowerShell shim if only .ps1 is present
                    Path.Combine(appData, "npm", "claude.ps1"),
                    Path.Combine(localAppData, "npm", "claude.ps1"),
                };
                foreach (string c in candidates) { if (File.Exists(c)) return c; }
                string fromWhere = Where("claude.exe") ?? Where("claude.cmd") ?? Where("claude.ps1") ?? Where("claude");
                if (!string.IsNullOrEmpty(fromWhere)) return fromWhere;
#endif
                return null;
            }

            // Linux
            {
                string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
                string[] candidates =
                {
                    "/usr/local/bin/claude",
                    "/usr/bin/claude",
                    Path.Combine(home, ".local", "bin", "claude"),
                };
                foreach (string c in candidates) { if (File.Exists(c)) return c; }
                // Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude
                string nvmClaude = ResolveClaudeFromNvm(home);
                if (!string.IsNullOrEmpty(nvmClaude)) return nvmClaude;
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
                return Which("claude", "/usr/local/bin:/usr/bin:/bin");
#else
                return null;
#endif
            }
        }

        // Attempt to resolve claude from NVM-managed Node installations, choosing the newest version
        private static string ResolveClaudeFromNvm(string home)
        {
            try
            {
                if (string.IsNullOrEmpty(home)) return null;
                string nvmNodeDir = Path.Combine(home, ".nvm", "versions", "node");
                if (!Directory.Exists(nvmNodeDir)) return null;

                string bestPath = null;
                Version bestVersion = null;
                foreach (string versionDir in Directory.EnumerateDirectories(nvmNodeDir))
                {
                    string name = Path.GetFileName(versionDir);
                    if (string.IsNullOrEmpty(name)) continue;
                    if (name.StartsWith("v", StringComparison.OrdinalIgnoreCase))
                    {
                        // Extract numeric portion: e.g., v18.19.0-nightly -> 18.19.0
                        string versionStr = name.Substring(1);
                        int dashIndex = versionStr.IndexOf('-');
                        if (dashIndex > 0)
                        {
                            versionStr = versionStr.Substring(0, dashIndex);
                        }
                        if (Version.TryParse(versionStr, out Version parsed))
                        {
                            string candidate = Path.Combine(versionDir, "bin", "claude");
                            if (File.Exists(candidate))
                            {
                                if (bestVersion == null || parsed > bestVersion)
                                {
                                    bestVersion = parsed;
                                    bestPath = candidate;
                                }
                            }
                        }
                    }
                }
                return bestPath;
            }
            catch { return null; }
        }

        // Explicitly set the Claude CLI absolute path override in EditorPrefs
        internal static void SetClaudeCliPath(string absolutePath)
        {
            try
            {
                if (!string.IsNullOrEmpty(absolutePath) && File.Exists(absolutePath))
                {
                    EditorPrefs.SetString(PrefClaude, absolutePath);
                }
            }
            catch { }
        }

        // Clear any previously set Claude CLI override path
        internal static void ClearClaudeCliPath()
        {
            try
            {
                if (EditorPrefs.HasKey(PrefClaude))
                {
                    EditorPrefs.DeleteKey(PrefClaude);
                }
            }
            catch { }
        }

        // Use existing UV resolver; returns absolute path or null.
        internal static string ResolveUv()
        {
            return ServerInstaller.FindUvPath();
        }

        internal static bool TryRun(
            string file,
            string args,
            string workingDir,
            out string stdout,
            out string stderr,
            int timeoutMs = 15000,
            string extraPathPrepend = null)
        {
            stdout = string.Empty;
            stderr = string.Empty;
            try
            {
                // Handle PowerShell scripts on Windows by invoking through powershell.exe
                bool isPs1 = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
                             file.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase);

                var psi = new ProcessStartInfo
                {
                    FileName = isPs1 ? "powershell.exe" : file,
                    Arguments = isPs1
                        ? $"-NoProfile -ExecutionPolicy Bypass -File \"{file}\" {args}".Trim()
                        : args,
                    WorkingDirectory = string.IsNullOrEmpty(workingDir) ? Environment.CurrentDirectory : workingDir,
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    CreateNoWindow = true,
                };
                if (!string.IsNullOrEmpty(extraPathPrepend))
                {
                    string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
                    psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath)
                        ? extraPathPrepend
                        : (extraPathPrepend + System.IO.Path.PathSeparator + currentPath);
                }

                using var process = new Process { StartInfo = psi, EnableRaisingEvents = false };

                var so = new StringBuilder();
                var se = new StringBuilder();
                process.OutputDataReceived += (_, e) => { if (e.Data != null) so.AppendLine(e.Data); };
                process.ErrorDataReceived += (_, e) => { if (e.Data != null) se.AppendLine(e.Data); };

                if (!process.Start()) return false;

                process.BeginOutputReadLine();
                process.BeginErrorReadLine();

                if (!process.WaitForExit(timeoutMs))
                {
                    try { process.Kill(); } catch { }
                    return false;
                }

                // Ensure async buffers are flushed
                process.WaitForExit();

                stdout = so.ToString();
                stderr = se.ToString();
                return process.ExitCode == 0;
            }
            catch
            {
                return false;
            }
        }

#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
        private static string Which(string exe, string prependPath)
        {
            try
            {
                var psi = new ProcessStartInfo("/usr/bin/which", exe)
                {
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    CreateNoWindow = true,
                };
                string path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
                psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(path) ? prependPath : (prependPath + Path.PathSeparator + path);
                using var p = Process.Start(psi);
                string output = p?.StandardOutput.ReadToEnd().Trim();
                p?.WaitForExit(1500);
                return (!string.IsNullOrEmpty(output) && File.Exists(output)) ? output : null;
            }
            catch { return null; }
        }
#endif

#if UNITY_EDITOR_WIN
        private static string Where(string exe)
        {
            try
            {
                var psi = new ProcessStartInfo("where", exe)
                {
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    CreateNoWindow = true,
                };
                using var p = Process.Start(psi);
                string first = p?.StandardOutput.ReadToEnd()
                    .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
                    .FirstOrDefault();
                p?.WaitForExit(1500);
                return (!string.IsNullOrEmpty(first) && File.Exists(first)) ? first : null;
            }
            catch { return null; }
        }
#endif
    }
}

```

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

```csharp
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using UnityEditor;

namespace MCPForUnity.Editor.Helpers
{
    internal static class ExecPath
    {
        private const string PrefClaude = "MCPForUnity.ClaudeCliPath";

        // Resolve Claude CLI absolute path. Pref → env → common locations → PATH.
        internal static string ResolveClaude()
        {
            try
            {
                string pref = EditorPrefs.GetString(PrefClaude, string.Empty);
                if (!string.IsNullOrEmpty(pref) && File.Exists(pref)) return pref;
            }
            catch { }

            string env = Environment.GetEnvironmentVariable("CLAUDE_CLI");
            if (!string.IsNullOrEmpty(env) && File.Exists(env)) return env;

            if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
                string[] candidates =
                {
                    "/opt/homebrew/bin/claude",
                    "/usr/local/bin/claude",
                    Path.Combine(home, ".local", "bin", "claude"),
                };
                foreach (string c in candidates) { if (File.Exists(c)) return c; }
                // Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude
                string nvmClaude = ResolveClaudeFromNvm(home);
                if (!string.IsNullOrEmpty(nvmClaude)) return nvmClaude;
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
                return Which("claude", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin");
#else
                return null;
#endif
            }

            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
#if UNITY_EDITOR_WIN
                // Common npm global locations
                string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
                string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
                string[] candidates =
                {
                    // Prefer .cmd (most reliable from non-interactive processes)
                    Path.Combine(appData, "npm", "claude.cmd"),
                    Path.Combine(localAppData, "npm", "claude.cmd"),
                    // Fall back to PowerShell shim if only .ps1 is present
                    Path.Combine(appData, "npm", "claude.ps1"),
                    Path.Combine(localAppData, "npm", "claude.ps1"),
                };
                foreach (string c in candidates) { if (File.Exists(c)) return c; }
                string fromWhere = Where("claude.exe") ?? Where("claude.cmd") ?? Where("claude.ps1") ?? Where("claude");
                if (!string.IsNullOrEmpty(fromWhere)) return fromWhere;
#endif
                return null;
            }

            // Linux
            {
                string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
                string[] candidates =
                {
                    "/usr/local/bin/claude",
                    "/usr/bin/claude",
                    Path.Combine(home, ".local", "bin", "claude"),
                };
                foreach (string c in candidates) { if (File.Exists(c)) return c; }
                // Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude
                string nvmClaude = ResolveClaudeFromNvm(home);
                if (!string.IsNullOrEmpty(nvmClaude)) return nvmClaude;
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
                return Which("claude", "/usr/local/bin:/usr/bin:/bin");
#else
                return null;
#endif
            }
        }

        // Attempt to resolve claude from NVM-managed Node installations, choosing the newest version
        private static string ResolveClaudeFromNvm(string home)
        {
            try
            {
                if (string.IsNullOrEmpty(home)) return null;
                string nvmNodeDir = Path.Combine(home, ".nvm", "versions", "node");
                if (!Directory.Exists(nvmNodeDir)) return null;

                string bestPath = null;
                Version bestVersion = null;
                foreach (string versionDir in Directory.EnumerateDirectories(nvmNodeDir))
                {
                    string name = Path.GetFileName(versionDir);
                    if (string.IsNullOrEmpty(name)) continue;
                    if (name.StartsWith("v", StringComparison.OrdinalIgnoreCase))
                    {
                        // Extract numeric portion: e.g., v18.19.0-nightly -> 18.19.0
                        string versionStr = name.Substring(1);
                        int dashIndex = versionStr.IndexOf('-');
                        if (dashIndex > 0)
                        {
                            versionStr = versionStr.Substring(0, dashIndex);
                        }
                        if (Version.TryParse(versionStr, out Version parsed))
                        {
                            string candidate = Path.Combine(versionDir, "bin", "claude");
                            if (File.Exists(candidate))
                            {
                                if (bestVersion == null || parsed > bestVersion)
                                {
                                    bestVersion = parsed;
                                    bestPath = candidate;
                                }
                            }
                        }
                    }
                }
                return bestPath;
            }
            catch { return null; }
        }

        // Explicitly set the Claude CLI absolute path override in EditorPrefs
        internal static void SetClaudeCliPath(string absolutePath)
        {
            try
            {
                if (!string.IsNullOrEmpty(absolutePath) && File.Exists(absolutePath))
                {
                    EditorPrefs.SetString(PrefClaude, absolutePath);
                }
            }
            catch { }
        }

        // Clear any previously set Claude CLI override path
        internal static void ClearClaudeCliPath()
        {
            try
            {
                if (EditorPrefs.HasKey(PrefClaude))
                {
                    EditorPrefs.DeleteKey(PrefClaude);
                }
            }
            catch { }
        }

        // Use existing UV resolver; returns absolute path or null.
        internal static string ResolveUv()
        {
            return ServerInstaller.FindUvPath();
        }

        internal static bool TryRun(
            string file,
            string args,
            string workingDir,
            out string stdout,
            out string stderr,
            int timeoutMs = 15000,
            string extraPathPrepend = null)
        {
            stdout = string.Empty;
            stderr = string.Empty;
            try
            {
                // Handle PowerShell scripts on Windows by invoking through powershell.exe
                bool isPs1 = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
                             file.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase);

                var psi = new ProcessStartInfo
                {
                    FileName = isPs1 ? "powershell.exe" : file,
                    Arguments = isPs1
                        ? $"-NoProfile -ExecutionPolicy Bypass -File \"{file}\" {args}".Trim()
                        : args,
                    WorkingDirectory = string.IsNullOrEmpty(workingDir) ? Environment.CurrentDirectory : workingDir,
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    CreateNoWindow = true,
                };
                if (!string.IsNullOrEmpty(extraPathPrepend))
                {
                    string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
                    psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath)
                        ? extraPathPrepend
                        : (extraPathPrepend + System.IO.Path.PathSeparator + currentPath);
                }

                using var process = new Process { StartInfo = psi, EnableRaisingEvents = false };

                var so = new StringBuilder();
                var se = new StringBuilder();
                process.OutputDataReceived += (_, e) => { if (e.Data != null) so.AppendLine(e.Data); };
                process.ErrorDataReceived += (_, e) => { if (e.Data != null) se.AppendLine(e.Data); };

                if (!process.Start()) return false;

                process.BeginOutputReadLine();
                process.BeginErrorReadLine();

                if (!process.WaitForExit(timeoutMs))
                {
                    try { process.Kill(); } catch { }
                    return false;
                }

                // Ensure async buffers are flushed
                process.WaitForExit();

                stdout = so.ToString();
                stderr = se.ToString();
                return process.ExitCode == 0;
            }
            catch
            {
                return false;
            }
        }

#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
        private static string Which(string exe, string prependPath)
        {
            try
            {
                var psi = new ProcessStartInfo("/usr/bin/which", exe)
                {
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    CreateNoWindow = true,
                };
                string path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
                psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(path) ? prependPath : (prependPath + Path.PathSeparator + path);
                using var p = Process.Start(psi);
                string output = p?.StandardOutput.ReadToEnd().Trim();
                p?.WaitForExit(1500);
                return (!string.IsNullOrEmpty(output) && File.Exists(output)) ? output : null;
            }
            catch { return null; }
        }
#endif

#if UNITY_EDITOR_WIN
        private static string Where(string exe)
        {
            try
            {
                var psi = new ProcessStartInfo("where", exe)
                {
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    CreateNoWindow = true,
                };
                using var p = Process.Start(psi);
                string first = p?.StandardOutput.ReadToEnd()
                    .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
                    .FirstOrDefault();
                p?.WaitForExit(1500);
                return (!string.IsNullOrEmpty(first) && File.Exists(first)) ? first : null;
            }
            catch { return null; }
        }
#endif
    }
}

```

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

```csharp
using System;
using System.IO;
using UnityEditor;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using Newtonsoft.Json;
using UnityEngine;

namespace MCPForUnity.Editor.Helpers
{
    /// <summary>
    /// Manages dynamic port allocation and persistent storage for MCP for Unity
    /// </summary>
    public static class PortManager
    {
        private static bool IsDebugEnabled()
        {
            try { return EditorPrefs.GetBool("MCPForUnity.DebugLogs", false); }
            catch { return false; }
        }

        private const int DefaultPort = 6400;
        private const int MaxPortAttempts = 100;
        private const string RegistryFileName = "unity-mcp-port.json";

        [Serializable]
        public class PortConfig
        {
            public int unity_port;
            public string created_date;
            public string project_path;
        }

        /// <summary>
        /// Get the port to use - either from storage or discover a new one
        /// Will try stored port first, then fallback to discovering new port
        /// </summary>
        /// <returns>Port number to use</returns>
        public static int GetPortWithFallback()
        {
            // Try to load stored port first, but only if it's from the current project
            var storedConfig = GetStoredPortConfig();
            if (storedConfig != null &&
                storedConfig.unity_port > 0 &&
                string.Equals(storedConfig.project_path ?? string.Empty, Application.dataPath ?? string.Empty, StringComparison.OrdinalIgnoreCase) &&
                IsPortAvailable(storedConfig.unity_port))
            {
                if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Using stored port {storedConfig.unity_port} for current project");
                return storedConfig.unity_port;
            }

            // If stored port exists but is currently busy, wait briefly for release
            if (storedConfig != null && storedConfig.unity_port > 0)
            {
                if (WaitForPortRelease(storedConfig.unity_port, 1500))
                {
                    if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Stored port {storedConfig.unity_port} became available after short wait");
                    return storedConfig.unity_port;
                }
                // Prefer sticking to the same port; let the caller handle bind retries/fallbacks
                return storedConfig.unity_port;
            }

            // If no valid stored port, find a new one and save it
            int newPort = FindAvailablePort();
            SavePort(newPort);
            return newPort;
        }

        /// <summary>
        /// Discover and save a new available port (used by Auto-Connect button)
        /// </summary>
        /// <returns>New available port</returns>
        public static int DiscoverNewPort()
        {
            int newPort = FindAvailablePort();
            SavePort(newPort);
            if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Discovered and saved new port: {newPort}");
            return newPort;
        }

        /// <summary>
        /// Find an available port starting from the default port
        /// </summary>
        /// <returns>Available port number</returns>
        private static int FindAvailablePort()
        {
            // Always try default port first
            if (IsPortAvailable(DefaultPort))
            {
                if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Using default port {DefaultPort}");
                return DefaultPort;
            }

            if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Default port {DefaultPort} is in use, searching for alternative...");

            // Search for alternatives
            for (int port = DefaultPort + 1; port < DefaultPort + MaxPortAttempts; port++)
            {
                if (IsPortAvailable(port))
                {
                    if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Found available port {port}");
                    return port;
                }
            }

            throw new Exception($"No available ports found in range {DefaultPort}-{DefaultPort + MaxPortAttempts}");
        }

        /// <summary>
        /// Check if a specific port is available for binding
        /// </summary>
        /// <param name="port">Port to check</param>
        /// <returns>True if port is available</returns>
        public static bool IsPortAvailable(int port)
        {
            try
            {
                var testListener = new TcpListener(IPAddress.Loopback, port);
                testListener.Start();
                testListener.Stop();
                return true;
            }
            catch (SocketException)
            {
                return false;
            }
        }

        /// <summary>
        /// Check if a port is currently being used by MCP for Unity
        /// This helps avoid unnecessary port changes when Unity itself is using the port
        /// </summary>
        /// <param name="port">Port to check</param>
        /// <returns>True if port appears to be used by MCP for Unity</returns>
        public static bool IsPortUsedByMCPForUnity(int port)
        {
            try
            {
                // Try to make a quick connection to see if it's an MCP for Unity server
                using var client = new TcpClient();
                var connectTask = client.ConnectAsync(IPAddress.Loopback, port);
                if (connectTask.Wait(100)) // 100ms timeout
                {
                    // If connection succeeded, it's likely the MCP for Unity server
                    return client.Connected;
                }
                return false;
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        /// Wait for a port to become available for a limited amount of time.
        /// Used to bridge the gap during domain reload when the old listener
        /// hasn't released the socket yet.
        /// </summary>
        private static bool WaitForPortRelease(int port, int timeoutMs)
        {
            int waited = 0;
            const int step = 100;
            while (waited < timeoutMs)
            {
                if (IsPortAvailable(port))
                {
                    return true;
                }

                // If the port is in use by an MCP instance, continue waiting briefly
                if (!IsPortUsedByMCPForUnity(port))
                {
                    // In use by something else; don't keep waiting
                    return false;
                }

                Thread.Sleep(step);
                waited += step;
            }
            return IsPortAvailable(port);
        }

        /// <summary>
        /// Save port to persistent storage
        /// </summary>
        /// <param name="port">Port to save</param>
        private static void SavePort(int port)
        {
            try
            {
                var portConfig = new PortConfig
                {
                    unity_port = port,
                    created_date = DateTime.UtcNow.ToString("O"),
                    project_path = Application.dataPath
                };

                string registryDir = GetRegistryDirectory();
                Directory.CreateDirectory(registryDir);

                string registryFile = GetRegistryFilePath();
                string json = JsonConvert.SerializeObject(portConfig, Formatting.Indented);
                // Write to hashed, project-scoped file
                File.WriteAllText(registryFile, json, new System.Text.UTF8Encoding(false));
                // Also write to legacy stable filename to avoid hash/case drift across reloads
                string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName);
                File.WriteAllText(legacy, json, new System.Text.UTF8Encoding(false));

                if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Saved port {port} to storage");
            }
            catch (Exception ex)
            {
                Debug.LogWarning($"Could not save port to storage: {ex.Message}");
            }
        }

        /// <summary>
        /// Load port from persistent storage
        /// </summary>
        /// <returns>Stored port number, or 0 if not found</returns>
        private static int LoadStoredPort()
        {
            try
            {
                string registryFile = GetRegistryFilePath();

                if (!File.Exists(registryFile))
                {
                    // Backwards compatibility: try the legacy file name
                    string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName);
                    if (!File.Exists(legacy))
                    {
                        return 0;
                    }
                    registryFile = legacy;
                }

                string json = File.ReadAllText(registryFile);
                var portConfig = JsonConvert.DeserializeObject<PortConfig>(json);

                return portConfig?.unity_port ?? 0;
            }
            catch (Exception ex)
            {
                Debug.LogWarning($"Could not load port from storage: {ex.Message}");
                return 0;
            }
        }

        /// <summary>
        /// Get the current stored port configuration
        /// </summary>
        /// <returns>Port configuration if exists, null otherwise</returns>
        public static PortConfig GetStoredPortConfig()
        {
            try
            {
                string registryFile = GetRegistryFilePath();

                if (!File.Exists(registryFile))
                {
                    // Backwards compatibility: try the legacy file
                    string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName);
                    if (!File.Exists(legacy))
                    {
                        return null;
                    }
                    registryFile = legacy;
                }

                string json = File.ReadAllText(registryFile);
                return JsonConvert.DeserializeObject<PortConfig>(json);
            }
            catch (Exception ex)
            {
                Debug.LogWarning($"Could not load port config: {ex.Message}");
                return null;
            }
        }

        private static string GetRegistryDirectory()
        {
            return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unity-mcp");
        }

        private static string GetRegistryFilePath()
        {
            string dir = GetRegistryDirectory();
            string hash = ComputeProjectHash(Application.dataPath);
            string fileName = $"unity-mcp-port-{hash}.json";
            return Path.Combine(dir, fileName);
        }

        private static string ComputeProjectHash(string input)
        {
            try
            {
                using SHA1 sha1 = SHA1.Create();
                byte[] bytes = Encoding.UTF8.GetBytes(input ?? string.Empty);
                byte[] hashBytes = sha1.ComputeHash(bytes);
                var sb = new StringBuilder();
                foreach (byte b in hashBytes)
                {
                    sb.Append(b.ToString("x2"));
                }
                return sb.ToString()[..8]; // short, sufficient for filenames
            }
            catch
            {
                return "default";
            }
        }
    }
}

```

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

```csharp
using System;
using System.IO;
using UnityEditor;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using Newtonsoft.Json;
using UnityEngine;

namespace MCPForUnity.Editor.Helpers
{
    /// <summary>
    /// Manages dynamic port allocation and persistent storage for MCP for Unity
    /// </summary>
    public static class PortManager
    {
        private static bool IsDebugEnabled()
        {
            try { return EditorPrefs.GetBool("MCPForUnity.DebugLogs", false); }
            catch { return false; }
        }

        private const int DefaultPort = 6400;
        private const int MaxPortAttempts = 100;
        private const string RegistryFileName = "unity-mcp-port.json";

        [Serializable]
        public class PortConfig
        {
            public int unity_port;
            public string created_date;
            public string project_path;
        }

        /// <summary>
        /// Get the port to use - either from storage or discover a new one
        /// Will try stored port first, then fallback to discovering new port
        /// </summary>
        /// <returns>Port number to use</returns>
        public static int GetPortWithFallback()
        {
            // Try to load stored port first, but only if it's from the current project
            var storedConfig = GetStoredPortConfig();
            if (storedConfig != null &&
                storedConfig.unity_port > 0 &&
                string.Equals(storedConfig.project_path ?? string.Empty, Application.dataPath ?? string.Empty, StringComparison.OrdinalIgnoreCase) &&
                IsPortAvailable(storedConfig.unity_port))
            {
                if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Using stored port {storedConfig.unity_port} for current project");
                return storedConfig.unity_port;
            }

            // If stored port exists but is currently busy, wait briefly for release
            if (storedConfig != null && storedConfig.unity_port > 0)
            {
                if (WaitForPortRelease(storedConfig.unity_port, 1500))
                {
                    if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Stored port {storedConfig.unity_port} became available after short wait");
                    return storedConfig.unity_port;
                }
                // Prefer sticking to the same port; let the caller handle bind retries/fallbacks
                return storedConfig.unity_port;
            }

            // If no valid stored port, find a new one and save it
            int newPort = FindAvailablePort();
            SavePort(newPort);
            return newPort;
        }

        /// <summary>
        /// Discover and save a new available port (used by Auto-Connect button)
        /// </summary>
        /// <returns>New available port</returns>
        public static int DiscoverNewPort()
        {
            int newPort = FindAvailablePort();
            SavePort(newPort);
            if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Discovered and saved new port: {newPort}");
            return newPort;
        }

        /// <summary>
        /// Find an available port starting from the default port
        /// </summary>
        /// <returns>Available port number</returns>
        private static int FindAvailablePort()
        {
            // Always try default port first
            if (IsPortAvailable(DefaultPort))
            {
                if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Using default port {DefaultPort}");
                return DefaultPort;
            }

            if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Default port {DefaultPort} is in use, searching for alternative...");

            // Search for alternatives
            for (int port = DefaultPort + 1; port < DefaultPort + MaxPortAttempts; port++)
            {
                if (IsPortAvailable(port))
                {
                    if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Found available port {port}");
                    return port;
                }
            }

            throw new Exception($"No available ports found in range {DefaultPort}-{DefaultPort + MaxPortAttempts}");
        }

        /// <summary>
        /// Check if a specific port is available for binding
        /// </summary>
        /// <param name="port">Port to check</param>
        /// <returns>True if port is available</returns>
        public static bool IsPortAvailable(int port)
        {
            try
            {
                var testListener = new TcpListener(IPAddress.Loopback, port);
                testListener.Start();
                testListener.Stop();
                return true;
            }
            catch (SocketException)
            {
                return false;
            }
        }

        /// <summary>
        /// Check if a port is currently being used by MCP for Unity
        /// This helps avoid unnecessary port changes when Unity itself is using the port
        /// </summary>
        /// <param name="port">Port to check</param>
        /// <returns>True if port appears to be used by MCP for Unity</returns>
        public static bool IsPortUsedByMCPForUnity(int port)
        {
            try
            {
                // Try to make a quick connection to see if it's an MCP for Unity server
                using var client = new TcpClient();
                var connectTask = client.ConnectAsync(IPAddress.Loopback, port);
                if (connectTask.Wait(100)) // 100ms timeout
                {
                    // If connection succeeded, it's likely the MCP for Unity server
                    return client.Connected;
                }
                return false;
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        /// Wait for a port to become available for a limited amount of time.
        /// Used to bridge the gap during domain reload when the old listener
        /// hasn't released the socket yet.
        /// </summary>
        private static bool WaitForPortRelease(int port, int timeoutMs)
        {
            int waited = 0;
            const int step = 100;
            while (waited < timeoutMs)
            {
                if (IsPortAvailable(port))
                {
                    return true;
                }

                // If the port is in use by an MCP instance, continue waiting briefly
                if (!IsPortUsedByMCPForUnity(port))
                {
                    // In use by something else; don't keep waiting
                    return false;
                }

                Thread.Sleep(step);
                waited += step;
            }
            return IsPortAvailable(port);
        }

        /// <summary>
        /// Save port to persistent storage
        /// </summary>
        /// <param name="port">Port to save</param>
        private static void SavePort(int port)
        {
            try
            {
                var portConfig = new PortConfig
                {
                    unity_port = port,
                    created_date = DateTime.UtcNow.ToString("O"),
                    project_path = Application.dataPath
                };

                string registryDir = GetRegistryDirectory();
                Directory.CreateDirectory(registryDir);

                string registryFile = GetRegistryFilePath();
                string json = JsonConvert.SerializeObject(portConfig, Formatting.Indented);
                // Write to hashed, project-scoped file
                File.WriteAllText(registryFile, json, new System.Text.UTF8Encoding(false));
                // Also write to legacy stable filename to avoid hash/case drift across reloads
                string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName);
                File.WriteAllText(legacy, json, new System.Text.UTF8Encoding(false));

                if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Saved port {port} to storage");
            }
            catch (Exception ex)
            {
                Debug.LogWarning($"Could not save port to storage: {ex.Message}");
            }
        }

        /// <summary>
        /// Load port from persistent storage
        /// </summary>
        /// <returns>Stored port number, or 0 if not found</returns>
        private static int LoadStoredPort()
        {
            try
            {
                string registryFile = GetRegistryFilePath();

                if (!File.Exists(registryFile))
                {
                    // Backwards compatibility: try the legacy file name
                    string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName);
                    if (!File.Exists(legacy))
                    {
                        return 0;
                    }
                    registryFile = legacy;
                }

                string json = File.ReadAllText(registryFile);
                var portConfig = JsonConvert.DeserializeObject<PortConfig>(json);

                return portConfig?.unity_port ?? 0;
            }
            catch (Exception ex)
            {
                Debug.LogWarning($"Could not load port from storage: {ex.Message}");
                return 0;
            }
        }

        /// <summary>
        /// Get the current stored port configuration
        /// </summary>
        /// <returns>Port configuration if exists, null otherwise</returns>
        public static PortConfig GetStoredPortConfig()
        {
            try
            {
                string registryFile = GetRegistryFilePath();

                if (!File.Exists(registryFile))
                {
                    // Backwards compatibility: try the legacy file
                    string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName);
                    if (!File.Exists(legacy))
                    {
                        return null;
                    }
                    registryFile = legacy;
                }

                string json = File.ReadAllText(registryFile);
                return JsonConvert.DeserializeObject<PortConfig>(json);
            }
            catch (Exception ex)
            {
                Debug.LogWarning($"Could not load port config: {ex.Message}");
                return null;
            }
        }

        private static string GetRegistryDirectory()
        {
            return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unity-mcp");
        }

        private static string GetRegistryFilePath()
        {
            string dir = GetRegistryDirectory();
            string hash = ComputeProjectHash(Application.dataPath);
            string fileName = $"unity-mcp-port-{hash}.json";
            return Path.Combine(dir, fileName);
        }

        private static string ComputeProjectHash(string input)
        {
            try
            {
                using SHA1 sha1 = SHA1.Create();
                byte[] bytes = Encoding.UTF8.GetBytes(input ?? string.Empty);
                byte[] hashBytes = sha1.ComputeHash(bytes);
                var sb = new StringBuilder();
                foreach (byte b in hashBytes)
                {
                    sb.Append(b.ToString("x2"));
                }
                return sb.ToString()[..8]; // short, sufficient for filenames
            }
            catch
            {
                return "default";
            }
        }
    }
}

```

--------------------------------------------------------------------------------
/MCPForUnity/Editor/Services/TestRunnerService.cs:
--------------------------------------------------------------------------------

```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MCPForUnity.Editor.Helpers;
using UnityEditor;
using UnityEditor.TestTools.TestRunner.Api;
using UnityEngine;

namespace MCPForUnity.Editor.Services
{
    /// <summary>
    /// Concrete implementation of <see cref="ITestRunnerService"/>.
    /// Coordinates Unity Test Runner operations and produces structured results.
    /// </summary>
    internal sealed class TestRunnerService : ITestRunnerService, ICallbacks, IDisposable
    {
        private static readonly TestMode[] AllModes = { TestMode.EditMode, TestMode.PlayMode };

        private readonly TestRunnerApi _testRunnerApi;
        private readonly SemaphoreSlim _operationLock = new SemaphoreSlim(1, 1);
        private readonly List<ITestResultAdaptor> _leafResults = new List<ITestResultAdaptor>();
        private TaskCompletionSource<TestRunResult> _runCompletionSource;

        public TestRunnerService()
        {
            _testRunnerApi = ScriptableObject.CreateInstance<TestRunnerApi>();
            _testRunnerApi.RegisterCallbacks(this);
        }

        public async Task<IReadOnlyList<Dictionary<string, string>>> GetTestsAsync(TestMode? mode)
        {
            await _operationLock.WaitAsync().ConfigureAwait(false);
            try
            {
                var modes = mode.HasValue ? new[] { mode.Value } : AllModes;

                var results = new List<Dictionary<string, string>>();
                var seen = new HashSet<string>(StringComparer.Ordinal);

                foreach (var m in modes)
                {
                    var root = await RetrieveTestRootAsync(m).ConfigureAwait(true);
                    if (root != null)
                    {
                        CollectFromNode(root, m, results, seen, new List<string>());
                    }
                }

                return results;
            }
            finally
            {
                _operationLock.Release();
            }
        }

        public async Task<TestRunResult> RunTestsAsync(TestMode mode)
        {
            await _operationLock.WaitAsync().ConfigureAwait(false);
            Task<TestRunResult> runTask;
            try
            {
                if (_runCompletionSource != null && !_runCompletionSource.Task.IsCompleted)
                {
                    throw new InvalidOperationException("A Unity test run is already in progress.");
                }

                _leafResults.Clear();
                _runCompletionSource = new TaskCompletionSource<TestRunResult>(TaskCreationOptions.RunContinuationsAsynchronously);

                var filter = new Filter { testMode = mode };
                _testRunnerApi.Execute(new ExecutionSettings(filter));

                runTask = _runCompletionSource.Task;
            }
            catch
            {
                _operationLock.Release();
                throw;
            }

            try
            {
                return await runTask.ConfigureAwait(true);
            }
            finally
            {
                _operationLock.Release();
            }
        }

        public void Dispose()
        {
            try
            {
                _testRunnerApi?.UnregisterCallbacks(this);
            }
            catch
            {
                // Ignore cleanup errors
            }

            if (_testRunnerApi != null)
            {
                ScriptableObject.DestroyImmediate(_testRunnerApi);
            }

            _operationLock.Dispose();
        }

        #region TestRunnerApi callbacks

        public void RunStarted(ITestAdaptor testsToRun)
        {
            _leafResults.Clear();
        }

        public void RunFinished(ITestResultAdaptor result)
        {
            if (_runCompletionSource == null)
            {
                return;
            }

            var payload = TestRunResult.Create(result, _leafResults);
            _runCompletionSource.TrySetResult(payload);
            _runCompletionSource = null;
        }

        public void TestStarted(ITestAdaptor test)
        {
            // No-op
        }

        public void TestFinished(ITestResultAdaptor result)
        {
            if (result == null)
            {
                return;
            }

            if (!result.HasChildren)
            {
                _leafResults.Add(result);
            }
        }

        #endregion

        #region Test list helpers

        private async Task<ITestAdaptor> RetrieveTestRootAsync(TestMode mode)
        {
            var tcs = new TaskCompletionSource<ITestAdaptor>(TaskCreationOptions.RunContinuationsAsynchronously);

            _testRunnerApi.RetrieveTestList(mode, root =>
            {
                tcs.TrySetResult(root);
            });

            // Ensure the editor pumps at least one additional update in case the window is unfocused.
            EditorApplication.QueuePlayerLoopUpdate();

            var completed = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromSeconds(30))).ConfigureAwait(true);
            if (completed != tcs.Task)
            {
                McpLog.Warn($"[TestRunnerService] Timeout waiting for test retrieval callback for {mode}");
                return null;
            }

            try
            {
                return await tcs.Task.ConfigureAwait(true);
            }
            catch (Exception ex)
            {
                McpLog.Error($"[TestRunnerService] Error retrieving tests for {mode}: {ex.Message}\n{ex.StackTrace}");
                return null;
            }
        }

        private static void CollectFromNode(
            ITestAdaptor node,
            TestMode mode,
            List<Dictionary<string, string>> output,
            HashSet<string> seen,
            List<string> path)
        {
            if (node == null)
            {
                return;
            }

            bool hasName = !string.IsNullOrEmpty(node.Name);
            if (hasName)
            {
                path.Add(node.Name);
            }

            bool hasChildren = node.HasChildren && node.Children != null;

            if (!hasChildren)
            {
                string fullName = string.IsNullOrEmpty(node.FullName) ? node.Name ?? string.Empty : node.FullName;
                string key = $"{mode}:{fullName}";

                if (!string.IsNullOrEmpty(fullName) && seen.Add(key))
                {
                    string computedPath = path.Count > 0 ? string.Join("/", path) : fullName;
                    output.Add(new Dictionary<string, string>
                    {
                        ["name"] = node.Name ?? fullName,
                        ["full_name"] = fullName,
                        ["path"] = computedPath,
                        ["mode"] = mode.ToString(),
                    });
                }
            }
            else if (node.Children != null)
            {
                foreach (var child in node.Children)
                {
                    CollectFromNode(child, mode, output, seen, path);
                }
            }

            if (hasName && path.Count > 0)
            {
                path.RemoveAt(path.Count - 1);
            }
        }

        #endregion
    }

    /// <summary>
    /// Summary of a Unity test run.
    /// </summary>
    public sealed class TestRunResult
    {
        internal TestRunResult(TestRunSummary summary, IReadOnlyList<TestRunTestResult> results)
        {
            Summary = summary;
            Results = results;
        }

        public TestRunSummary Summary { get; }
        public IReadOnlyList<TestRunTestResult> Results { get; }

        public int Total => Summary.Total;
        public int Passed => Summary.Passed;
        public int Failed => Summary.Failed;
        public int Skipped => Summary.Skipped;

        public object ToSerializable(string mode)
        {
            return new
            {
                mode,
                summary = Summary.ToSerializable(),
                results = Results.Select(r => r.ToSerializable()).ToList(),
            };
        }

        internal static TestRunResult Create(ITestResultAdaptor summary, IReadOnlyList<ITestResultAdaptor> tests)
        {
            var materializedTests = tests.Select(TestRunTestResult.FromAdaptor).ToList();

            int passed = summary?.PassCount
                ?? materializedTests.Count(t => string.Equals(t.State, "Passed", StringComparison.OrdinalIgnoreCase));
            int failed = summary?.FailCount
                ?? materializedTests.Count(t => string.Equals(t.State, "Failed", StringComparison.OrdinalIgnoreCase));
            int skipped = summary?.SkipCount
                ?? materializedTests.Count(t => string.Equals(t.State, "Skipped", StringComparison.OrdinalIgnoreCase));

            double duration = summary?.Duration
                ?? materializedTests.Sum(t => t.DurationSeconds);

            int total = summary != null ? passed + failed + skipped : materializedTests.Count;

            var summaryPayload = new TestRunSummary(
                total,
                passed,
                failed,
                skipped,
                duration,
                summary?.ResultState ?? "Unknown");

            return new TestRunResult(summaryPayload, materializedTests);
        }
    }

    public sealed class TestRunSummary
    {
        internal TestRunSummary(int total, int passed, int failed, int skipped, double durationSeconds, string resultState)
        {
            Total = total;
            Passed = passed;
            Failed = failed;
            Skipped = skipped;
            DurationSeconds = durationSeconds;
            ResultState = resultState;
        }

        public int Total { get; }
        public int Passed { get; }
        public int Failed { get; }
        public int Skipped { get; }
        public double DurationSeconds { get; }
        public string ResultState { get; }

        internal object ToSerializable()
        {
            return new
            {
                total = Total,
                passed = Passed,
                failed = Failed,
                skipped = Skipped,
                durationSeconds = DurationSeconds,
                resultState = ResultState,
            };
        }
    }

    public sealed class TestRunTestResult
    {
        internal TestRunTestResult(
            string name,
            string fullName,
            string state,
            double durationSeconds,
            string message,
            string stackTrace,
            string output)
        {
            Name = name;
            FullName = fullName;
            State = state;
            DurationSeconds = durationSeconds;
            Message = message;
            StackTrace = stackTrace;
            Output = output;
        }

        public string Name { get; }
        public string FullName { get; }
        public string State { get; }
        public double DurationSeconds { get; }
        public string Message { get; }
        public string StackTrace { get; }
        public string Output { get; }

        internal object ToSerializable()
        {
            return new
            {
                name = Name,
                fullName = FullName,
                state = State,
                durationSeconds = DurationSeconds,
                message = Message,
                stackTrace = StackTrace,
                output = Output,
            };
        }

        internal static TestRunTestResult FromAdaptor(ITestResultAdaptor adaptor)
        {
            if (adaptor == null)
            {
                return new TestRunTestResult(string.Empty, string.Empty, "Unknown", 0.0, string.Empty, string.Empty, string.Empty);
            }

            return new TestRunTestResult(
                adaptor.Name,
                adaptor.FullName,
                adaptor.ResultState,
                adaptor.Duration,
                adaptor.Message,
                adaptor.StackTrace,
                adaptor.Output);
        }
    }
}

```

--------------------------------------------------------------------------------
/MCPForUnity/Editor/Tools/ManageShader.cs:
--------------------------------------------------------------------------------

```csharp
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Helpers;

namespace MCPForUnity.Editor.Tools
{
    /// <summary>
    /// Handles CRUD operations for shader files within the Unity project.
    /// </summary>
    [McpForUnityTool("manage_shader")]
    public static class ManageShader
    {
        /// <summary>
        /// Main handler for shader management actions.
        /// </summary>
        public static object HandleCommand(JObject @params)
        {
            // Extract parameters
            string action = @params["action"]?.ToString().ToLower();
            string name = @params["name"]?.ToString();
            string path = @params["path"]?.ToString(); // Relative to Assets/
            string contents = null;

            // Check if we have base64 encoded contents
            bool contentsEncoded = @params["contentsEncoded"]?.ToObject<bool>() ?? false;
            if (contentsEncoded && @params["encodedContents"] != null)
            {
                try
                {
                    contents = DecodeBase64(@params["encodedContents"].ToString());
                }
                catch (Exception e)
                {
                    return Response.Error($"Failed to decode shader contents: {e.Message}");
                }
            }
            else
            {
                contents = @params["contents"]?.ToString();
            }

            // Validate required parameters
            if (string.IsNullOrEmpty(action))
            {
                return Response.Error("Action parameter is required.");
            }
            if (string.IsNullOrEmpty(name))
            {
                return Response.Error("Name parameter is required.");
            }
            // Basic name validation (alphanumeric, underscores, cannot start with number)
            if (!Regex.IsMatch(name, @"^[a-zA-Z_][a-zA-Z0-9_]*$"))
            {
                return Response.Error(
                    $"Invalid shader name: '{name}'. Use only letters, numbers, underscores, and don't start with a number."
                );
            }

            // Ensure path is relative to Assets/, removing any leading "Assets/"
            // Set default directory to "Shaders" if path is not provided
            string relativeDir = path ?? "Shaders"; // Default to "Shaders" if path is null
            if (!string.IsNullOrEmpty(relativeDir))
            {
                relativeDir = relativeDir.Replace('\\', '/').Trim('/');
                if (relativeDir.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
                {
                    relativeDir = relativeDir.Substring("Assets/".Length).TrimStart('/');
                }
            }
            // Handle empty string case explicitly after processing
            if (string.IsNullOrEmpty(relativeDir))
            {
                relativeDir = "Shaders"; // Ensure default if path was provided as "" or only "/" or "Assets/"
            }

            // Construct paths
            string shaderFileName = $"{name}.shader";
            string fullPathDir = Path.Combine(Application.dataPath, relativeDir);
            string fullPath = Path.Combine(fullPathDir, shaderFileName);
            string relativePath = Path.Combine("Assets", relativeDir, shaderFileName)
                .Replace('\\', '/'); // Ensure "Assets/" prefix and forward slashes

            // Ensure the target directory exists for create/update
            if (action == "create" || action == "update")
            {
                try
                {
                    if (!Directory.Exists(fullPathDir))
                    {
                        Directory.CreateDirectory(fullPathDir);
                        // Refresh AssetDatabase to recognize new folders
                        AssetDatabase.Refresh();
                    }
                }
                catch (Exception e)
                {
                    return Response.Error(
                        $"Could not create directory '{fullPathDir}': {e.Message}"
                    );
                }
            }

            // Route to specific action handlers
            switch (action)
            {
                case "create":
                    return CreateShader(fullPath, relativePath, name, contents);
                case "read":
                    return ReadShader(fullPath, relativePath);
                case "update":
                    return UpdateShader(fullPath, relativePath, name, contents);
                case "delete":
                    return DeleteShader(fullPath, relativePath);
                default:
                    return Response.Error(
                        $"Unknown action: '{action}'. Valid actions are: create, read, update, delete."
                    );
            }
        }

        /// <summary>
        /// Decode base64 string to normal text
        /// </summary>
        private static string DecodeBase64(string encoded)
        {
            byte[] data = Convert.FromBase64String(encoded);
            return System.Text.Encoding.UTF8.GetString(data);
        }

        /// <summary>
        /// Encode text to base64 string
        /// </summary>
        private static string EncodeBase64(string text)
        {
            byte[] data = System.Text.Encoding.UTF8.GetBytes(text);
            return Convert.ToBase64String(data);
        }

        private static object CreateShader(
            string fullPath,
            string relativePath,
            string name,
            string contents
        )
        {
            // Check if shader already exists
            if (File.Exists(fullPath))
            {
                return Response.Error(
                    $"Shader already exists at '{relativePath}'. Use 'update' action to modify."
                );
            }

            // Add validation for shader name conflicts in Unity
            if (Shader.Find(name) != null)
            {
                return Response.Error(
                    $"A shader with name '{name}' already exists in the project. Choose a different name."
                );
            }

            // Generate default content if none provided
            if (string.IsNullOrEmpty(contents))
            {
                contents = GenerateDefaultShaderContent(name);
            }

            try
            {
                File.WriteAllText(fullPath, contents, new System.Text.UTF8Encoding(false));
                AssetDatabase.ImportAsset(relativePath);
                AssetDatabase.Refresh(); // Ensure Unity recognizes the new shader
                return Response.Success(
                    $"Shader '{name}.shader' created successfully at '{relativePath}'.",
                    new { path = relativePath }
                );
            }
            catch (Exception e)
            {
                return Response.Error($"Failed to create shader '{relativePath}': {e.Message}");
            }
        }

        private static object ReadShader(string fullPath, string relativePath)
        {
            if (!File.Exists(fullPath))
            {
                return Response.Error($"Shader not found at '{relativePath}'.");
            }

            try
            {
                string contents = File.ReadAllText(fullPath);

                // Return both normal and encoded contents for larger files
                //TODO: Consider a threshold for large files
                bool isLarge = contents.Length > 10000; // If content is large, include encoded version
                var responseData = new
                {
                    path = relativePath,
                    contents = contents,
                    // For large files, also include base64-encoded version
                    encodedContents = isLarge ? EncodeBase64(contents) : null,
                    contentsEncoded = isLarge,
                };

                return Response.Success(
                    $"Shader '{Path.GetFileName(relativePath)}' read successfully.",
                    responseData
                );
            }
            catch (Exception e)
            {
                return Response.Error($"Failed to read shader '{relativePath}': {e.Message}");
            }
        }

        private static object UpdateShader(
            string fullPath,
            string relativePath,
            string name,
            string contents
        )
        {
            if (!File.Exists(fullPath))
            {
                return Response.Error(
                    $"Shader not found at '{relativePath}'. Use 'create' action to add a new shader."
                );
            }
            if (string.IsNullOrEmpty(contents))
            {
                return Response.Error("Content is required for the 'update' action.");
            }

            try
            {
                File.WriteAllText(fullPath, contents, new System.Text.UTF8Encoding(false));
                AssetDatabase.ImportAsset(relativePath);
                AssetDatabase.Refresh();
                return Response.Success(
                    $"Shader '{Path.GetFileName(relativePath)}' updated successfully.",
                    new { path = relativePath }
                );
            }
            catch (Exception e)
            {
                return Response.Error($"Failed to update shader '{relativePath}': {e.Message}");
            }
        }

        private static object DeleteShader(string fullPath, string relativePath)
        {
            if (!File.Exists(fullPath))
            {
                return Response.Error($"Shader not found at '{relativePath}'.");
            }

            try
            {
                // Delete the asset through Unity's AssetDatabase first
                bool success = AssetDatabase.DeleteAsset(relativePath);
                if (!success)
                {
                    return Response.Error($"Failed to delete shader through Unity's AssetDatabase: '{relativePath}'");
                }

                // If the file still exists (rare case), try direct deletion
                if (File.Exists(fullPath))
                {
                    File.Delete(fullPath);
                }

                return Response.Success($"Shader '{Path.GetFileName(relativePath)}' deleted successfully.");
            }
            catch (Exception e)
            {
                return Response.Error($"Failed to delete shader '{relativePath}': {e.Message}");
            }
        }

        //This is a CGProgram template
        //TODO: making a HLSL template as well?
        private static string GenerateDefaultShaderContent(string name)
        {
            return @"Shader """ + name + @"""
        {
            Properties
            {
                _MainTex (""Texture"", 2D) = ""white"" {}
            }
            SubShader
            {
                Tags { ""RenderType""=""Opaque"" }
                LOD 100

                Pass
                {
                    CGPROGRAM
                    #pragma vertex vert
                    #pragma fragment frag
                    #include ""UnityCG.cginc""

                    struct appdata
                    {
                        float4 vertex : POSITION;
                        float2 uv : TEXCOORD0;
                    };

                    struct v2f
                    {
                        float2 uv : TEXCOORD0;
                        float4 vertex : SV_POSITION;
                    };

                    sampler2D _MainTex;
                    float4 _MainTex_ST;

                    v2f vert (appdata v)
                    {
                        v2f o;
                        o.vertex = UnityObjectToClipPos(v.vertex);
                        o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                        return o;
                    }

                    fixed4 frag (v2f i) : SV_Target
                    {
                        fixed4 col = tex2D(_MainTex, i.uv);
                        return col;
                    }
                    ENDCG
                }
            }
        }";
        }
    }
}

```

--------------------------------------------------------------------------------
/UnityMcpBridge/Editor/Tools/ManageShader.cs:
--------------------------------------------------------------------------------

```csharp
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Helpers;

namespace MCPForUnity.Editor.Tools
{
    /// <summary>
    /// Handles CRUD operations for shader files within the Unity project.
    /// </summary>
    [McpForUnityTool("manage_shader")]
    public static class ManageShader
    {
        /// <summary>
        /// Main handler for shader management actions.
        /// </summary>
        public static object HandleCommand(JObject @params)
        {
            // Extract parameters
            string action = @params["action"]?.ToString().ToLower();
            string name = @params["name"]?.ToString();
            string path = @params["path"]?.ToString(); // Relative to Assets/
            string contents = null;

            // Check if we have base64 encoded contents
            bool contentsEncoded = @params["contentsEncoded"]?.ToObject<bool>() ?? false;
            if (contentsEncoded && @params["encodedContents"] != null)
            {
                try
                {
                    contents = DecodeBase64(@params["encodedContents"].ToString());
                }
                catch (Exception e)
                {
                    return Response.Error($"Failed to decode shader contents: {e.Message}");
                }
            }
            else
            {
                contents = @params["contents"]?.ToString();
            }

            // Validate required parameters
            if (string.IsNullOrEmpty(action))
            {
                return Response.Error("Action parameter is required.");
            }
            if (string.IsNullOrEmpty(name))
            {
                return Response.Error("Name parameter is required.");
            }
            // Basic name validation (alphanumeric, underscores, cannot start with number)
            if (!Regex.IsMatch(name, @"^[a-zA-Z_][a-zA-Z0-9_]*$"))
            {
                return Response.Error(
                    $"Invalid shader name: '{name}'. Use only letters, numbers, underscores, and don't start with a number."
                );
            }

            // Ensure path is relative to Assets/, removing any leading "Assets/"
            // Set default directory to "Shaders" if path is not provided
            string relativeDir = path ?? "Shaders"; // Default to "Shaders" if path is null
            if (!string.IsNullOrEmpty(relativeDir))
            {
                relativeDir = relativeDir.Replace('\\', '/').Trim('/');
                if (relativeDir.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
                {
                    relativeDir = relativeDir.Substring("Assets/".Length).TrimStart('/');
                }
            }
            // Handle empty string case explicitly after processing
            if (string.IsNullOrEmpty(relativeDir))
            {
                relativeDir = "Shaders"; // Ensure default if path was provided as "" or only "/" or "Assets/"
            }

            // Construct paths
            string shaderFileName = $"{name}.shader";
            string fullPathDir = Path.Combine(Application.dataPath, relativeDir);
            string fullPath = Path.Combine(fullPathDir, shaderFileName);
            string relativePath = Path.Combine("Assets", relativeDir, shaderFileName)
                .Replace('\\', '/'); // Ensure "Assets/" prefix and forward slashes

            // Ensure the target directory exists for create/update
            if (action == "create" || action == "update")
            {
                try
                {
                    if (!Directory.Exists(fullPathDir))
                    {
                        Directory.CreateDirectory(fullPathDir);
                        // Refresh AssetDatabase to recognize new folders
                        AssetDatabase.Refresh();
                    }
                }
                catch (Exception e)
                {
                    return Response.Error(
                        $"Could not create directory '{fullPathDir}': {e.Message}"
                    );
                }
            }

            // Route to specific action handlers
            switch (action)
            {
                case "create":
                    return CreateShader(fullPath, relativePath, name, contents);
                case "read":
                    return ReadShader(fullPath, relativePath);
                case "update":
                    return UpdateShader(fullPath, relativePath, name, contents);
                case "delete":
                    return DeleteShader(fullPath, relativePath);
                default:
                    return Response.Error(
                        $"Unknown action: '{action}'. Valid actions are: create, read, update, delete."
                    );
            }
        }

        /// <summary>
        /// Decode base64 string to normal text
        /// </summary>
        private static string DecodeBase64(string encoded)
        {
            byte[] data = Convert.FromBase64String(encoded);
            return System.Text.Encoding.UTF8.GetString(data);
        }

        /// <summary>
        /// Encode text to base64 string
        /// </summary>
        private static string EncodeBase64(string text)
        {
            byte[] data = System.Text.Encoding.UTF8.GetBytes(text);
            return Convert.ToBase64String(data);
        }

        private static object CreateShader(
            string fullPath,
            string relativePath,
            string name,
            string contents
        )
        {
            // Check if shader already exists
            if (File.Exists(fullPath))
            {
                return Response.Error(
                    $"Shader already exists at '{relativePath}'. Use 'update' action to modify."
                );
            }

            // Add validation for shader name conflicts in Unity
            if (Shader.Find(name) != null)
            {
                return Response.Error(
                    $"A shader with name '{name}' already exists in the project. Choose a different name."
                );
            }

            // Generate default content if none provided
            if (string.IsNullOrEmpty(contents))
            {
                contents = GenerateDefaultShaderContent(name);
            }

            try
            {
                File.WriteAllText(fullPath, contents, new System.Text.UTF8Encoding(false));
                AssetDatabase.ImportAsset(relativePath);
                AssetDatabase.Refresh(); // Ensure Unity recognizes the new shader
                return Response.Success(
                    $"Shader '{name}.shader' created successfully at '{relativePath}'.",
                    new { path = relativePath }
                );
            }
            catch (Exception e)
            {
                return Response.Error($"Failed to create shader '{relativePath}': {e.Message}");
            }
        }

        private static object ReadShader(string fullPath, string relativePath)
        {
            if (!File.Exists(fullPath))
            {
                return Response.Error($"Shader not found at '{relativePath}'.");
            }

            try
            {
                string contents = File.ReadAllText(fullPath);

                // Return both normal and encoded contents for larger files
                //TODO: Consider a threshold for large files
                bool isLarge = contents.Length > 10000; // If content is large, include encoded version
                var responseData = new
                {
                    path = relativePath,
                    contents = contents,
                    // For large files, also include base64-encoded version
                    encodedContents = isLarge ? EncodeBase64(contents) : null,
                    contentsEncoded = isLarge,
                };

                return Response.Success(
                    $"Shader '{Path.GetFileName(relativePath)}' read successfully.",
                    responseData
                );
            }
            catch (Exception e)
            {
                return Response.Error($"Failed to read shader '{relativePath}': {e.Message}");
            }
        }

        private static object UpdateShader(
            string fullPath,
            string relativePath,
            string name,
            string contents
        )
        {
            if (!File.Exists(fullPath))
            {
                return Response.Error(
                    $"Shader not found at '{relativePath}'. Use 'create' action to add a new shader."
                );
            }
            if (string.IsNullOrEmpty(contents))
            {
                return Response.Error("Content is required for the 'update' action.");
            }

            try
            {
                File.WriteAllText(fullPath, contents, new System.Text.UTF8Encoding(false));
                AssetDatabase.ImportAsset(relativePath);
                AssetDatabase.Refresh();
                return Response.Success(
                    $"Shader '{Path.GetFileName(relativePath)}' updated successfully.",
                    new { path = relativePath }
                );
            }
            catch (Exception e)
            {
                return Response.Error($"Failed to update shader '{relativePath}': {e.Message}");
            }
        }

        private static object DeleteShader(string fullPath, string relativePath)
        {
            if (!File.Exists(fullPath))
            {
                return Response.Error($"Shader not found at '{relativePath}'.");
            }

            try
            {
                // Delete the asset through Unity's AssetDatabase first
                bool success = AssetDatabase.DeleteAsset(relativePath);
                if (!success)
                {
                    return Response.Error($"Failed to delete shader through Unity's AssetDatabase: '{relativePath}'");
                }

                // If the file still exists (rare case), try direct deletion
                if (File.Exists(fullPath))
                {
                    File.Delete(fullPath);
                }

                return Response.Success($"Shader '{Path.GetFileName(relativePath)}' deleted successfully.");
            }
            catch (Exception e)
            {
                return Response.Error($"Failed to delete shader '{relativePath}': {e.Message}");
            }
        }

        //This is a CGProgram template
        //TODO: making a HLSL template as well?
        private static string GenerateDefaultShaderContent(string name)
        {
            return @"Shader """ + name + @"""
        {
            Properties
            {
                _MainTex (""Texture"", 2D) = ""white"" {}
            }
            SubShader
            {
                Tags { ""RenderType""=""Opaque"" }
                LOD 100

                Pass
                {
                    CGPROGRAM
                    #pragma vertex vert
                    #pragma fragment frag
                    #include ""UnityCG.cginc""

                    struct appdata
                    {
                        float4 vertex : POSITION;
                        float2 uv : TEXCOORD0;
                    };

                    struct v2f
                    {
                        float2 uv : TEXCOORD0;
                        float4 vertex : SV_POSITION;
                    };

                    sampler2D _MainTex;
                    float4 _MainTex_ST;

                    v2f vert (appdata v)
                    {
                        v2f o;
                        o.vertex = UnityObjectToClipPos(v.vertex);
                        o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                        return o;
                    }

                    fixed4 frag (v2f i) : SV_Target
                    {
                        fixed4 col = tex2D(_MainTex, i.uv);
                        return col;
                    }
                    ENDCG
                }
            }
        }";
        }
    }
}

```
Page 4/13FirstPrevNextLast