#
tokens: 49006/50000 46/263 files (page 2/13)
lines: off (toggle) GitHub
raw markdown copy
This is page 2 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/Resources/GetMenuItemsTests.cs:
--------------------------------------------------------------------------------

```csharp
using NUnit.Framework;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Resources.MenuItems;
using System;
using System.Linq;

namespace MCPForUnityTests.Editor.Resources.MenuItems
{
    public class GetMenuItemsTests
    {
        private static JObject ToJO(object o) => JObject.FromObject(o);

        [Test]
        public void NoSearch_ReturnsSuccessAndArray()
        {
            var res = GetMenuItems.HandleCommand(new JObject { ["search"] = "", ["refresh"] = false });
            var jo = ToJO(res);
            Assert.IsTrue((bool)jo["success"], "Expected success true");
            Assert.IsNotNull(jo["data"], "Expected data field present");
            Assert.AreEqual(JTokenType.Array, jo["data"].Type, "Expected data to be an array");

            // Validate list is sorted ascending when there are multiple items
            var arr = (JArray)jo["data"];
            if (arr.Count >= 2)
            {
                var original = arr.Select(t => (string)t).ToList();
                var sorted = original.OrderBy(s => s, StringComparer.Ordinal).ToList();
                CollectionAssert.AreEqual(sorted, original, "Expected menu items to be sorted ascending");
            }
        }

        [Test]
        public void SearchNoMatch_ReturnsEmpty()
        {
            var res = GetMenuItems.HandleCommand(new JObject { ["search"] = "___unlikely___term___" });
            var jo = ToJO(res);
            Assert.IsTrue((bool)jo["success"], "Expected success true");
            Assert.AreEqual(JTokenType.Array, jo["data"].Type, "Expected data to be an array");
            Assert.AreEqual(0, jo["data"].Count(), "Expected no results for unlikely search term");
        }

        [Test]
        public void SearchMatchesExistingItem_ReturnsContainingItem()
        {
            // Get the full list first
            var listRes = GetMenuItems.HandleCommand(new JObject { ["search"] = "", ["refresh"] = false });
            var listJo = ToJO(listRes);
            if (listJo["data"] is JArray arr && arr.Count > 0)
            {
                var first = (string)arr[0];
                // Use a mid-substring (case-insensitive) to avoid edge cases
                var term = first.Length > 4 ? first.Substring(1, Math.Min(3, first.Length - 2)) : first;
                term = term.ToLowerInvariant();

                var res = GetMenuItems.HandleCommand(new JObject { ["search"] = term, ["refresh"] = false });
                var jo = ToJO(res);
                Assert.IsTrue((bool)jo["success"], "Expected success true");
                Assert.AreEqual(JTokenType.Array, jo["data"].Type, "Expected data to be an array");
                // Expect at least the original item to be present
                var names = ((JArray)jo["data"]).Select(t => (string)t).ToList();
                CollectionAssert.Contains(names, first, "Expected search results to include the sampled item");
            }
            else
            {
                Assert.Pass("No menu items available to perform a content-based search assertion.");
            }
        }
    }
}

```

--------------------------------------------------------------------------------
/MCPForUnity/Editor/Dependencies/Models/DependencyCheckResult.cs:
--------------------------------------------------------------------------------

```csharp
using System;
using System.Collections.Generic;
using System.Linq;

namespace MCPForUnity.Editor.Dependencies.Models
{
    /// <summary>
    /// Result of a comprehensive dependency check
    /// </summary>
    [Serializable]
    public class DependencyCheckResult
    {
        /// <summary>
        /// List of all dependency statuses checked
        /// </summary>
        public List<DependencyStatus> Dependencies { get; set; }

        /// <summary>
        /// Overall system readiness for MCP operations
        /// </summary>
        public bool IsSystemReady { get; set; }

        /// <summary>
        /// Whether all required dependencies are available
        /// </summary>
        public bool AllRequiredAvailable => Dependencies?.Where(d => d.IsRequired).All(d => d.IsAvailable) ?? false;

        /// <summary>
        /// Whether any optional dependencies are missing
        /// </summary>
        public bool HasMissingOptional => Dependencies?.Where(d => !d.IsRequired).Any(d => !d.IsAvailable) ?? false;

        /// <summary>
        /// Summary message about the dependency state
        /// </summary>
        public string Summary { get; set; }

        /// <summary>
        /// Recommended next steps for the user
        /// </summary>
        public List<string> RecommendedActions { get; set; }

        /// <summary>
        /// Timestamp when this check was performed
        /// </summary>
        public DateTime CheckedAt { get; set; }

        public DependencyCheckResult()
        {
            Dependencies = new List<DependencyStatus>();
            RecommendedActions = new List<string>();
            CheckedAt = DateTime.UtcNow;
        }

        /// <summary>
        /// Get dependencies by availability status
        /// </summary>
        public List<DependencyStatus> GetMissingDependencies()
        {
            return Dependencies?.Where(d => !d.IsAvailable).ToList() ?? new List<DependencyStatus>();
        }

        /// <summary>
        /// Get missing required dependencies
        /// </summary>
        public List<DependencyStatus> GetMissingRequired()
        {
            return Dependencies?.Where(d => d.IsRequired && !d.IsAvailable).ToList() ?? new List<DependencyStatus>();
        }

        /// <summary>
        /// Generate a user-friendly summary of the dependency state
        /// </summary>
        public void GenerateSummary()
        {
            var missing = GetMissingDependencies();
            var missingRequired = GetMissingRequired();

            if (missing.Count == 0)
            {
                Summary = "All dependencies are available and ready.";
                IsSystemReady = true;
            }
            else if (missingRequired.Count == 0)
            {
                Summary = $"System is ready. {missing.Count} optional dependencies are missing.";
                IsSystemReady = true;
            }
            else
            {
                Summary = $"System is not ready. {missingRequired.Count} required dependencies are missing.";
                IsSystemReady = false;
            }
        }
    }
}

```

--------------------------------------------------------------------------------
/UnityMcpBridge/Editor/Dependencies/Models/DependencyCheckResult.cs:
--------------------------------------------------------------------------------

```csharp
using System;
using System.Collections.Generic;
using System.Linq;

namespace MCPForUnity.Editor.Dependencies.Models
{
    /// <summary>
    /// Result of a comprehensive dependency check
    /// </summary>
    [Serializable]
    public class DependencyCheckResult
    {
        /// <summary>
        /// List of all dependency statuses checked
        /// </summary>
        public List<DependencyStatus> Dependencies { get; set; }

        /// <summary>
        /// Overall system readiness for MCP operations
        /// </summary>
        public bool IsSystemReady { get; set; }

        /// <summary>
        /// Whether all required dependencies are available
        /// </summary>
        public bool AllRequiredAvailable => Dependencies?.Where(d => d.IsRequired).All(d => d.IsAvailable) ?? false;

        /// <summary>
        /// Whether any optional dependencies are missing
        /// </summary>
        public bool HasMissingOptional => Dependencies?.Where(d => !d.IsRequired).Any(d => !d.IsAvailable) ?? false;

        /// <summary>
        /// Summary message about the dependency state
        /// </summary>
        public string Summary { get; set; }

        /// <summary>
        /// Recommended next steps for the user
        /// </summary>
        public List<string> RecommendedActions { get; set; }

        /// <summary>
        /// Timestamp when this check was performed
        /// </summary>
        public DateTime CheckedAt { get; set; }

        public DependencyCheckResult()
        {
            Dependencies = new List<DependencyStatus>();
            RecommendedActions = new List<string>();
            CheckedAt = DateTime.UtcNow;
        }

        /// <summary>
        /// Get dependencies by availability status
        /// </summary>
        public List<DependencyStatus> GetMissingDependencies()
        {
            return Dependencies?.Where(d => !d.IsAvailable).ToList() ?? new List<DependencyStatus>();
        }

        /// <summary>
        /// Get missing required dependencies
        /// </summary>
        public List<DependencyStatus> GetMissingRequired()
        {
            return Dependencies?.Where(d => d.IsRequired && !d.IsAvailable).ToList() ?? new List<DependencyStatus>();
        }

        /// <summary>
        /// Generate a user-friendly summary of the dependency state
        /// </summary>
        public void GenerateSummary()
        {
            var missing = GetMissingDependencies();
            var missingRequired = GetMissingRequired();

            if (missing.Count == 0)
            {
                Summary = "All dependencies are available and ready.";
                IsSystemReady = true;
            }
            else if (missingRequired.Count == 0)
            {
                Summary = $"System is ready. {missing.Count} optional dependencies are missing.";
                IsSystemReady = true;
            }
            else
            {
                Summary = $"System is not ready. {missingRequired.Count} required dependencies are missing.";
                IsSystemReady = false;
            }
        }
    }
}

```

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

```csharp
using MCPForUnity.Editor.Models;

namespace MCPForUnity.Editor.Services
{
    /// <summary>
    /// Service for configuring MCP clients
    /// </summary>
    public interface IClientConfigurationService
    {
        /// <summary>
        /// Configures a specific MCP client
        /// </summary>
        /// <param name="client">The client to configure</param>
        void ConfigureClient(McpClient client);
        
        /// <summary>
        /// Configures all detected/installed MCP clients (skips clients where CLI/tools not found)
        /// </summary>
        /// <returns>Summary of configuration results</returns>
        ClientConfigurationSummary ConfigureAllDetectedClients();
        
        /// <summary>
        /// Checks the configuration status of a client
        /// </summary>
        /// <param name="client">The client to check</param>
        /// <param name="attemptAutoRewrite">If true, attempts to auto-fix mismatched paths</param>
        /// <returns>True if status changed, false otherwise</returns>
        bool CheckClientStatus(McpClient client, bool attemptAutoRewrite = true);
        
        /// <summary>
        /// Registers MCP for Unity with Claude Code CLI
        /// </summary>
        void RegisterClaudeCode();
        
        /// <summary>
        /// Unregisters MCP for Unity from Claude Code CLI
        /// </summary>
        void UnregisterClaudeCode();
        
        /// <summary>
        /// Gets the configuration file path for a client
        /// </summary>
        /// <param name="client">The client</param>
        /// <returns>Platform-specific config path</returns>
        string GetConfigPath(McpClient client);
        
        /// <summary>
        /// Generates the configuration JSON for a client
        /// </summary>
        /// <param name="client">The client</param>
        /// <returns>JSON configuration string</returns>
        string GenerateConfigJson(McpClient client);
        
        /// <summary>
        /// Gets human-readable installation steps for a client
        /// </summary>
        /// <param name="client">The client</param>
        /// <returns>Installation instructions</returns>
        string GetInstallationSteps(McpClient client);
    }
    
    /// <summary>
    /// Summary of configuration results for multiple clients
    /// </summary>
    public class ClientConfigurationSummary
    {
        /// <summary>
        /// Number of clients successfully configured
        /// </summary>
        public int SuccessCount { get; set; }
        
        /// <summary>
        /// Number of clients that failed to configure
        /// </summary>
        public int FailureCount { get; set; }
        
        /// <summary>
        /// Number of clients skipped (already configured or tool not found)
        /// </summary>
        public int SkippedCount { get; set; }
        
        /// <summary>
        /// Detailed messages for each client
        /// </summary>
        public System.Collections.Generic.List<string> Messages { get; set; } = new();
        
        /// <summary>
        /// Gets a human-readable summary message
        /// </summary>
        public string GetSummaryMessage()
        {
            return $"✓ {SuccessCount} configured, ⚠ {FailureCount} failed, ➜ {SkippedCount} skipped";
        }
    }
}

```

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

```csharp
using System;

namespace MCPForUnity.Editor.Services
{
    /// <summary>
    /// Service locator for accessing MCP services without dependency injection
    /// </summary>
    public static class MCPServiceLocator
    {
        private static IBridgeControlService _bridgeService;
        private static IClientConfigurationService _clientService;
        private static IPathResolverService _pathService;
        private static IPythonToolRegistryService _pythonToolRegistryService;
        private static ITestRunnerService _testRunnerService;
        private static IToolSyncService _toolSyncService;
        private static IPackageUpdateService _packageUpdateService;

        public static IBridgeControlService Bridge => _bridgeService ??= new BridgeControlService();
        public static IClientConfigurationService Client => _clientService ??= new ClientConfigurationService();
        public static IPathResolverService Paths => _pathService ??= new PathResolverService();
        public static IPythonToolRegistryService PythonToolRegistry => _pythonToolRegistryService ??= new PythonToolRegistryService();
        public static ITestRunnerService Tests => _testRunnerService ??= new TestRunnerService();
        public static IToolSyncService ToolSync => _toolSyncService ??= new ToolSyncService();
        public static IPackageUpdateService Updates => _packageUpdateService ??= new PackageUpdateService();

        /// <summary>
        /// Registers a custom implementation for a service (useful for testing)
        /// </summary>
        /// <typeparam name="T">The service interface type</typeparam>
        /// <param name="implementation">The implementation to register</param>
        public static void Register<T>(T implementation) where T : class
        {
            if (implementation is IBridgeControlService b)
                _bridgeService = b;
            else if (implementation is IClientConfigurationService c)
                _clientService = c;
            else if (implementation is IPathResolverService p)
                _pathService = p;
            else if (implementation is IPythonToolRegistryService ptr)
                _pythonToolRegistryService = ptr;
            else if (implementation is ITestRunnerService t)
                _testRunnerService = t;
            else if (implementation is IToolSyncService ts)
                _toolSyncService = ts;
            else if (implementation is IPackageUpdateService pu)
                _packageUpdateService = pu;
        }

        /// <summary>
        /// Resets all services to their default implementations (useful for testing)
        /// </summary>
        public static void Reset()
        {
            (_bridgeService as IDisposable)?.Dispose();
            (_clientService as IDisposable)?.Dispose();
            (_pathService as IDisposable)?.Dispose();
            (_pythonToolRegistryService as IDisposable)?.Dispose();
            (_testRunnerService as IDisposable)?.Dispose();
            (_toolSyncService as IDisposable)?.Dispose();
            (_packageUpdateService as IDisposable)?.Dispose();

            _bridgeService = null;
            _clientService = null;
            _pathService = null;
            _pythonToolRegistryService = null;
            _testRunnerService = null;
            _toolSyncService = null;
            _packageUpdateService = null;
        }
    }
}

```

--------------------------------------------------------------------------------
/MCPForUnity/UnityMcpServer~/src/tools/manage_asset.py:
--------------------------------------------------------------------------------

```python
"""
Defines the manage_asset tool for interacting with Unity assets.
"""
import asyncio
from typing import Annotated, Any, Literal

from mcp.server.fastmcp import Context
from registry import mcp_for_unity_tool
from unity_connection import async_send_command_with_retry


@mcp_for_unity_tool(
    description="Performs asset operations (import, create, modify, delete, etc.) in Unity."
)
async def manage_asset(
    ctx: Context,
    action: Annotated[Literal["import", "create", "modify", "delete", "duplicate", "move", "rename", "search", "get_info", "create_folder", "get_components"], "Perform CRUD operations on assets."],
    path: Annotated[str, "Asset path (e.g., 'Materials/MyMaterial.mat') or search scope."],
    asset_type: Annotated[str,
                          "Asset type (e.g., 'Material', 'Folder') - required for 'create'."] | None = None,
    properties: Annotated[dict[str, Any],
                          "Dictionary of properties for 'create'/'modify'."] | None = None,
    destination: Annotated[str,
                           "Target path for 'duplicate'/'move'."] | None = None,
    generate_preview: Annotated[bool,
                                "Generate a preview/thumbnail for the asset when supported."] = False,
    search_pattern: Annotated[str,
                              "Search pattern (e.g., '*.prefab')."] | None = None,
    filter_type: Annotated[str, "Filter type for search"] | None = None,
    filter_date_after: Annotated[str,
                                 "Date after which to filter"] | None = None,
    page_size: Annotated[int, "Page size for pagination"] | None = None,
    page_number: Annotated[int, "Page number for pagination"] | None = None
) -> dict[str, Any]:
    ctx.info(f"Processing manage_asset: {action}")
    # Ensure properties is a dict if None
    if properties is None:
        properties = {}

    # Coerce numeric inputs defensively
    def _coerce_int(value, default=None):
        if value is None:
            return default
        try:
            if isinstance(value, bool):
                return default
            if isinstance(value, int):
                return int(value)
            s = str(value).strip()
            if s.lower() in ("", "none", "null"):
                return default
            return int(float(s))
        except Exception:
            return default

    page_size = _coerce_int(page_size)
    page_number = _coerce_int(page_number)

    # Prepare parameters for the C# handler
    params_dict = {
        "action": action.lower(),
        "path": path,
        "assetType": asset_type,
        "properties": properties,
        "destination": destination,
        "generatePreview": generate_preview,
        "searchPattern": search_pattern,
        "filterType": filter_type,
        "filterDateAfter": filter_date_after,
        "pageSize": page_size,
        "pageNumber": page_number
    }

    # Remove None values to avoid sending unnecessary nulls
    params_dict = {k: v for k, v in params_dict.items() if v is not None}

    # Get the current asyncio event loop
    loop = asyncio.get_running_loop()

    # Use centralized async retry helper to avoid blocking the event loop
    result = await async_send_command_with_retry("manage_asset", params_dict, loop=loop)
    # Return the result obtained from Unity
    return result if isinstance(result, dict) else {"success": False, "message": str(result)}

```

--------------------------------------------------------------------------------
/UnityMcpBridge/UnityMcpServer~/src/tools/manage_asset.py:
--------------------------------------------------------------------------------

```python
"""
Defines the manage_asset tool for interacting with Unity assets.
"""
import asyncio
from typing import Annotated, Any, Literal

from mcp.server.fastmcp import Context
from registry import mcp_for_unity_tool
from unity_connection import async_send_command_with_retry


@mcp_for_unity_tool(
    description="Performs asset operations (import, create, modify, delete, etc.) in Unity."
)
async def manage_asset(
    ctx: Context,
    action: Annotated[Literal["import", "create", "modify", "delete", "duplicate", "move", "rename", "search", "get_info", "create_folder", "get_components"], "Perform CRUD operations on assets."],
    path: Annotated[str, "Asset path (e.g., 'Materials/MyMaterial.mat') or search scope."],
    asset_type: Annotated[str,
                          "Asset type (e.g., 'Material', 'Folder') - required for 'create'."] | None = None,
    properties: Annotated[dict[str, Any],
                          "Dictionary of properties for 'create'/'modify'."] | None = None,
    destination: Annotated[str,
                           "Target path for 'duplicate'/'move'."] | None = None,
    generate_preview: Annotated[bool,
                                "Generate a preview/thumbnail for the asset when supported."] = False,
    search_pattern: Annotated[str,
                              "Search pattern (e.g., '*.prefab')."] | None = None,
    filter_type: Annotated[str, "Filter type for search"] | None = None,
    filter_date_after: Annotated[str,
                                 "Date after which to filter"] | None = None,
    page_size: Annotated[int, "Page size for pagination"] | None = None,
    page_number: Annotated[int, "Page number for pagination"] | None = None
) -> dict[str, Any]:
    ctx.info(f"Processing manage_asset: {action}")
    # Ensure properties is a dict if None
    if properties is None:
        properties = {}

    # Coerce numeric inputs defensively
    def _coerce_int(value, default=None):
        if value is None:
            return default
        try:
            if isinstance(value, bool):
                return default
            if isinstance(value, int):
                return int(value)
            s = str(value).strip()
            if s.lower() in ("", "none", "null"):
                return default
            return int(float(s))
        except Exception:
            return default

    page_size = _coerce_int(page_size)
    page_number = _coerce_int(page_number)

    # Prepare parameters for the C# handler
    params_dict = {
        "action": action.lower(),
        "path": path,
        "assetType": asset_type,
        "properties": properties,
        "destination": destination,
        "generatePreview": generate_preview,
        "searchPattern": search_pattern,
        "filterType": filter_type,
        "filterDateAfter": filter_date_after,
        "pageSize": page_size,
        "pageNumber": page_number
    }

    # Remove None values to avoid sending unnecessary nulls
    params_dict = {k: v for k, v in params_dict.items() if v is not None}

    # Get the current asyncio event loop
    loop = asyncio.get_running_loop()

    # Use centralized async retry helper to avoid blocking the event loop
    result = await async_send_command_with_retry("manage_asset", params_dict, loop=loop)
    # Return the result obtained from Unity
    return result if isinstance(result, dict) else {"success": False, "message": str(result)}

```

--------------------------------------------------------------------------------
/UnityMcpBridge/Editor/Tools/MenuItems/MenuItemsReader.cs:
--------------------------------------------------------------------------------

```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
using UnityEditor;
using MCPForUnity.Editor.Helpers;

namespace MCPForUnity.Editor.Tools.MenuItems
{
    /// <summary>
    /// Provides read/list/exists capabilities for Unity menu items with caching.
    /// </summary>
    public static class MenuItemsReader
    {
        private static List<string> _cached;

        [InitializeOnLoadMethod]
        private static void Build() => Refresh();

        /// <summary>
        /// Returns the cached list, refreshing if necessary.
        /// </summary>
        public static IReadOnlyList<string> AllMenuItems() => _cached ??= Refresh();

        /// <summary>
        /// Rebuilds the cached list from reflection.
        /// </summary>
        private static List<string> Refresh()
        {
            try
            {
                var methods = TypeCache.GetMethodsWithAttribute<MenuItem>();
                _cached = methods
                    // Methods can have multiple [MenuItem] attributes; collect them all
                    .SelectMany(m => m
                        .GetCustomAttributes(typeof(MenuItem), false)
                        .OfType<MenuItem>()
                        .Select(attr => attr.menuItem))
                    .Where(s => !string.IsNullOrEmpty(s))
                    .Distinct(StringComparer.Ordinal) // Ensure no duplicates
                    .OrderBy(s => s, StringComparer.Ordinal) // Ensure consistent ordering
                    .ToList();
                return _cached;
            }
            catch (Exception e)
            {
                McpLog.Error($"[MenuItemsReader] Failed to scan menu items: {e}");
                _cached = _cached ?? new List<string>();
                return _cached;
            }
        }

        /// <summary>
        /// Returns a list of menu items. Optional 'search' param filters results.
        /// </summary>
        public static object List(JObject @params)
        {
            string search = @params["search"]?.ToString();
            bool doRefresh = @params["refresh"]?.ToObject<bool>() ?? false;
            if (doRefresh || _cached == null)
            {
                Refresh();
            }

            IEnumerable<string> result = _cached ?? Enumerable.Empty<string>();
            if (!string.IsNullOrEmpty(search))
            {
                result = result.Where(s => s.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0);
            }

            return Response.Success("Menu items retrieved.", result.ToList());
        }

        /// <summary>
        /// Checks if a given menu path exists in the cache.
        /// </summary>
        public static object Exists(JObject @params)
        {
            string menuPath = @params["menu_path"]?.ToString() ?? @params["menuPath"]?.ToString();
            if (string.IsNullOrWhiteSpace(menuPath))
            {
                return Response.Error("Required parameter 'menu_path' or 'menuPath' is missing or empty.");
            }

            bool doRefresh = @params["refresh"]?.ToObject<bool>() ?? false;
            if (doRefresh || _cached == null)
            {
                Refresh();
            }

            bool exists = (_cached ?? new List<string>()).Contains(menuPath);
            return Response.Success($"Exists check completed for '{menuPath}'.", new { exists });
        }
    }
}

```

--------------------------------------------------------------------------------
/UnityMcpBridge/UnityMcpServer~/src/test_telemetry.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
Test script for Unity MCP Telemetry System
Run this to verify telemetry is working correctly
"""

import os
from pathlib import Path
import sys

# Add src to Python path for imports
sys.path.insert(0, str(Path(__file__).parent))


def test_telemetry_basic():
    """Test basic telemetry functionality"""
    # Avoid stdout noise in tests

    try:
        from telemetry import (
            get_telemetry, record_telemetry, record_milestone,
            RecordType, MilestoneType, is_telemetry_enabled
        )
        pass
    except ImportError as e:
        # Silent failure path for tests
        return False

    # Test telemetry enabled status
    _ = is_telemetry_enabled()

    # Test basic record
    try:
        record_telemetry(RecordType.VERSION, {
            "version": "3.0.2",
            "test_run": True
        })
        pass
    except Exception as e:
        # Silent failure path for tests
        return False

    # Test milestone recording
    try:
        is_first = record_milestone(MilestoneType.FIRST_STARTUP, {
            "test_mode": True
        })
        _ = is_first
    except Exception as e:
        # Silent failure path for tests
        return False

    # Test telemetry collector
    try:
        collector = get_telemetry()
        _ = collector
    except Exception as e:
        # Silent failure path for tests
        return False

    return True


def test_telemetry_disabled():
    """Test telemetry with disabled state"""
    # Silent for tests

    # Set environment variable to disable telemetry
    os.environ["DISABLE_TELEMETRY"] = "true"

    # Re-import to get fresh config
    import importlib
    import telemetry
    importlib.reload(telemetry)

    from telemetry import is_telemetry_enabled, record_telemetry, RecordType

    _ = is_telemetry_enabled()

    if not is_telemetry_enabled():
        pass

        # Test that records are ignored when disabled
        record_telemetry(RecordType.USAGE, {"test": "should_be_ignored"})
        pass

        return True
    else:
        pass
        return False


def test_data_storage():
    """Test data storage functionality"""
    # Silent for tests

    try:
        from telemetry import get_telemetry

        collector = get_telemetry()
        data_dir = collector.config.data_dir

        _ = (data_dir, collector.config.uuid_file,
             collector.config.milestones_file)

        # Check if files exist
        if collector.config.uuid_file.exists():
            pass
        else:
            pass

        if collector.config.milestones_file.exists():
            pass
        else:
            pass

        return True

    except Exception as e:
        # Silent failure path for tests
        return False


def main():
    """Run all telemetry tests"""
    # Silent runner for CI

    tests = [
        test_telemetry_basic,
        test_data_storage,
        test_telemetry_disabled,
    ]

    passed = 0
    failed = 0

    for test in tests:
        try:
            if test():
                passed += 1
                pass
            else:
                failed += 1
                pass
        except Exception as e:
            failed += 1
            pass

    _ = (passed, failed)

    if failed == 0:
        pass
        return True
    else:
        pass
        return False


if __name__ == "__main__":
    success = main()
    sys.exit(0 if success else 1)

```

--------------------------------------------------------------------------------
/MCPForUnity/UnityMcpServer~/src/test_telemetry.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
Test script for MCP for Unity Telemetry System
Run this to verify telemetry is working correctly
"""

import os
from pathlib import Path
import sys

# Add src to Python path for imports
sys.path.insert(0, str(Path(__file__).parent))


def test_telemetry_basic():
    """Test basic telemetry functionality"""
    # Avoid stdout noise in tests

    try:
        from telemetry import (
            get_telemetry, record_telemetry, record_milestone,
            RecordType, MilestoneType, is_telemetry_enabled
        )
        pass
    except ImportError as e:
        # Silent failure path for tests
        return False

    # Test telemetry enabled status
    _ = is_telemetry_enabled()

    # Test basic record
    try:
        record_telemetry(RecordType.VERSION, {
            "version": "3.0.2",
            "test_run": True
        })
        pass
    except Exception as e:
        # Silent failure path for tests
        return False

    # Test milestone recording
    try:
        is_first = record_milestone(MilestoneType.FIRST_STARTUP, {
            "test_mode": True
        })
        _ = is_first
    except Exception as e:
        # Silent failure path for tests
        return False

    # Test telemetry collector
    try:
        collector = get_telemetry()
        _ = collector
    except Exception as e:
        # Silent failure path for tests
        return False

    return True


def test_telemetry_disabled():
    """Test telemetry with disabled state"""
    # Silent for tests

    # Set environment variable to disable telemetry
    os.environ["DISABLE_TELEMETRY"] = "true"

    # Re-import to get fresh config
    import importlib
    import telemetry
    importlib.reload(telemetry)

    from telemetry import is_telemetry_enabled, record_telemetry, RecordType

    _ = is_telemetry_enabled()

    if not is_telemetry_enabled():
        pass

        # Test that records are ignored when disabled
        record_telemetry(RecordType.USAGE, {"test": "should_be_ignored"})
        pass

        return True
    else:
        pass
        return False


def test_data_storage():
    """Test data storage functionality"""
    # Silent for tests

    try:
        from telemetry import get_telemetry

        collector = get_telemetry()
        data_dir = collector.config.data_dir

        _ = (data_dir, collector.config.uuid_file,
             collector.config.milestones_file)

        # Check if files exist
        if collector.config.uuid_file.exists():
            pass
        else:
            pass

        if collector.config.milestones_file.exists():
            pass
        else:
            pass

        return True

    except Exception as e:
        # Silent failure path for tests
        return False


def main():
    """Run all telemetry tests"""
    # Silent runner for CI

    tests = [
        test_telemetry_basic,
        test_data_storage,
        test_telemetry_disabled,
    ]

    passed = 0
    failed = 0

    for test in tests:
        try:
            if test():
                passed += 1
                pass
            else:
                failed += 1
                pass
        except Exception as e:
            failed += 1
            pass

    _ = (passed, failed)

    if failed == 0:
        pass
        return True
    else:
        pass
        return False


if __name__ == "__main__":
    success = main()
    sys.exit(0 if success else 1)

```

--------------------------------------------------------------------------------
/MCPForUnity/UnityMcpServer~/src/tools/read_console.py:
--------------------------------------------------------------------------------

```python
"""
Defines the read_console tool for accessing Unity Editor console messages.
"""
from typing import Annotated, Any, Literal

from mcp.server.fastmcp import Context
from registry import mcp_for_unity_tool
from unity_connection import send_command_with_retry


@mcp_for_unity_tool(
    description="Gets messages from or clears the Unity Editor console."
)
def read_console(
    ctx: Context,
    action: Annotated[Literal['get', 'clear'], "Get or clear the Unity Editor console."],
    types: Annotated[list[Literal['error', 'warning',
                                  'log', 'all']], "Message types to get"] | None = None,
    count: Annotated[int, "Max messages to return"] | None = None,
    filter_text: Annotated[str, "Text filter for messages"] | None = None,
    since_timestamp: Annotated[str,
                               "Get messages after this timestamp (ISO 8601)"] | None = None,
    format: Annotated[Literal['plain', 'detailed',
                              'json'], "Output format"] | None = None,
    include_stacktrace: Annotated[bool,
                                  "Include stack traces in output"] | None = None
) -> dict[str, Any]:
    ctx.info(f"Processing read_console: {action}")
    # Set defaults if values are None
    action = action if action is not None else 'get'
    types = types if types is not None else ['error', 'warning', 'log']
    format = format if format is not None else 'detailed'
    include_stacktrace = include_stacktrace if include_stacktrace is not None else True

    # Normalize action if it's a string
    if isinstance(action, str):
        action = action.lower()

    # Coerce count defensively (string/float -> int)
    def _coerce_int(value, default=None):
        if value is None:
            return default
        try:
            if isinstance(value, bool):
                return default
            if isinstance(value, int):
                return int(value)
            s = str(value).strip()
            if s.lower() in ("", "none", "null"):
                return default
            return int(float(s))
        except Exception:
            return default

    count = _coerce_int(count)

    # Prepare parameters for the C# handler
    params_dict = {
        "action": action,
        "types": types,
        "count": count,
        "filterText": filter_text,
        "sinceTimestamp": since_timestamp,
        "format": format.lower() if isinstance(format, str) else format,
        "includeStacktrace": include_stacktrace
    }

    # Remove None values unless it's 'count' (as None might mean 'all')
    params_dict = {k: v for k, v in params_dict.items()
                   if v is not None or k == 'count'}

    # Add count back if it was None, explicitly sending null might be important for C# logic
    if 'count' not in params_dict:
        params_dict['count'] = None

    # Use centralized retry helper
    resp = send_command_with_retry("read_console", params_dict)
    if isinstance(resp, dict) and resp.get("success") and not include_stacktrace:
        # Strip stacktrace fields from returned lines if present
        try:
            lines = resp.get("data", {}).get("lines", [])
            for line in lines:
                if isinstance(line, dict) and "stacktrace" in line:
                    line.pop("stacktrace", None)
        except Exception:
            pass
    return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}

```

--------------------------------------------------------------------------------
/UnityMcpBridge/UnityMcpServer~/src/tools/read_console.py:
--------------------------------------------------------------------------------

```python
"""
Defines the read_console tool for accessing Unity Editor console messages.
"""
from typing import Annotated, Any, Literal

from mcp.server.fastmcp import Context
from registry import mcp_for_unity_tool
from unity_connection import send_command_with_retry


@mcp_for_unity_tool(
    description="Gets messages from or clears the Unity Editor console."
)
def read_console(
    ctx: Context,
    action: Annotated[Literal['get', 'clear'], "Get or clear the Unity Editor console."],
    types: Annotated[list[Literal['error', 'warning',
                                  'log', 'all']], "Message types to get"] | None = None,
    count: Annotated[int, "Max messages to return"] | None = None,
    filter_text: Annotated[str, "Text filter for messages"] | None = None,
    since_timestamp: Annotated[str,
                               "Get messages after this timestamp (ISO 8601)"] | None = None,
    format: Annotated[Literal['plain', 'detailed',
                              'json'], "Output format"] | None = None,
    include_stacktrace: Annotated[bool,
                                  "Include stack traces in output"] | None = None
) -> dict[str, Any]:
    ctx.info(f"Processing read_console: {action}")
    # Set defaults if values are None
    action = action if action is not None else 'get'
    types = types if types is not None else ['error', 'warning', 'log']
    format = format if format is not None else 'detailed'
    include_stacktrace = include_stacktrace if include_stacktrace is not None else True

    # Normalize action if it's a string
    if isinstance(action, str):
        action = action.lower()

    # Coerce count defensively (string/float -> int)
    def _coerce_int(value, default=None):
        if value is None:
            return default
        try:
            if isinstance(value, bool):
                return default
            if isinstance(value, int):
                return int(value)
            s = str(value).strip()
            if s.lower() in ("", "none", "null"):
                return default
            return int(float(s))
        except Exception:
            return default

    count = _coerce_int(count)

    # Prepare parameters for the C# handler
    params_dict = {
        "action": action,
        "types": types,
        "count": count,
        "filterText": filter_text,
        "sinceTimestamp": since_timestamp,
        "format": format.lower() if isinstance(format, str) else format,
        "includeStacktrace": include_stacktrace
    }

    # Remove None values unless it's 'count' (as None might mean 'all')
    params_dict = {k: v for k, v in params_dict.items()
                   if v is not None or k == 'count'}

    # Add count back if it was None, explicitly sending null might be important for C# logic
    if 'count' not in params_dict:
        params_dict['count'] = None

    # Use centralized retry helper
    resp = send_command_with_retry("read_console", params_dict)
    if isinstance(resp, dict) and resp.get("success") and not include_stacktrace:
        # Strip stacktrace fields from returned lines if present
        try:
            lines = resp.get("data", {}).get("lines", [])
            for line in lines:
                if isinstance(line, dict) and "stacktrace" in line:
                    line.pop("stacktrace", None)
        except Exception:
            pass
    return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}

```

--------------------------------------------------------------------------------
/MCPForUnity/Editor/Resources/Tests/GetTests.cs:
--------------------------------------------------------------------------------

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

namespace MCPForUnity.Editor.Resources.Tests
{
    /// <summary>
    /// Provides access to Unity tests from the Test Framework.
    /// This is a read-only resource that can be queried by MCP clients.
    /// </summary>
    [McpForUnityResource("get_tests")]
    public static class GetTests
    {
        public static async Task<object> HandleCommand(JObject @params)
        {
            McpLog.Info("[GetTests] Retrieving tests for all modes");
            IReadOnlyList<Dictionary<string, string>> result;

            try
            {
                result = await MCPServiceLocator.Tests.GetTestsAsync(mode: null).ConfigureAwait(true);
            }
            catch (Exception ex)
            {
                McpLog.Error($"[GetTests] Error retrieving tests: {ex.Message}\n{ex.StackTrace}");
                return Response.Error("Failed to retrieve tests");
            }

            string message = $"Retrieved {result.Count} tests";

            return Response.Success(message, result);
        }
    }

    /// <summary>
    /// Provides access to Unity tests for a specific mode (EditMode or PlayMode).
    /// This is a read-only resource that can be queried by MCP clients.
    /// </summary>
    [McpForUnityResource("get_tests_for_mode")]
    public static class GetTestsForMode
    {
        public static async Task<object> HandleCommand(JObject @params)
        {
            IReadOnlyList<Dictionary<string, string>> result;
            string modeStr = @params["mode"]?.ToString();
            if (string.IsNullOrEmpty(modeStr))
            {
                return Response.Error("'mode' parameter is required");
            }

            if (!ModeParser.TryParse(modeStr, out var parsedMode, out var parseError))
            {
                return Response.Error(parseError);
            }

            McpLog.Info($"[GetTestsForMode] Retrieving tests for mode: {parsedMode.Value}");

            try
            {
                result = await MCPServiceLocator.Tests.GetTestsAsync(parsedMode).ConfigureAwait(true);
            }
            catch (Exception ex)
            {
                McpLog.Error($"[GetTestsForMode] Error retrieving tests: {ex.Message}\n{ex.StackTrace}");
                return Response.Error("Failed to retrieve tests");
            }

            string message = $"Retrieved {result.Count} {parsedMode.Value} tests";
            return Response.Success(message, result);
        }
    }

    internal static class ModeParser
    {
        internal static bool TryParse(string modeStr, out TestMode? mode, out string error)
        {
            error = null;
            mode = null;

            if (string.IsNullOrWhiteSpace(modeStr))
            {
                error = "'mode' parameter cannot be empty";
                return false;
            }

            if (modeStr.Equals("edit", StringComparison.OrdinalIgnoreCase))
            {
                mode = TestMode.EditMode;
                return true;
            }

            if (modeStr.Equals("play", StringComparison.OrdinalIgnoreCase))
            {
                mode = TestMode.PlayMode;
                return true;
            }

            error = $"Unknown test mode: '{modeStr}'. Use 'edit' or 'play'";
            return false;
        }
    }
}

```

--------------------------------------------------------------------------------
/.github/scripts/mark_skipped.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
Post-processes a JUnit XML so that "expected"/environmental failures
(e.g., permission prompts, empty MCP resources, or schema hiccups)
are converted to <skipped/>. Leaves real failures intact.

Usage:
  python .github/scripts/mark_skipped.py reports/claude-nl-tests.xml
"""

from __future__ import annotations
import sys
import os
import re
import xml.etree.ElementTree as ET

PATTERNS = [
    r"\bpermission\b",
    r"\bpermissions\b",
    r"\bautoApprove\b",
    r"\bapproval\b",
    r"\bdenied\b",
    r"requested\s+permissions",
    r"^MCP resources list is empty$",
    r"No MCP resources detected",
    r"aggregator.*returned\s*\[\s*\]",
    r"Unknown resource:\s*unity://",
    r"Input should be a valid dictionary.*ctx",
    r"validation error .* ctx",
]


def should_skip(msg: str) -> bool:
    if not msg:
        return False
    msg_l = msg.strip()
    for pat in PATTERNS:
        if re.search(pat, msg_l, flags=re.IGNORECASE | re.MULTILINE):
            return True
    return False


def summarize_counts(ts: ET.Element):
    tests = 0
    failures = 0
    errors = 0
    skipped = 0
    for case in ts.findall("testcase"):
        tests += 1
        if case.find("failure") is not None:
            failures += 1
        if case.find("error") is not None:
            errors += 1
        if case.find("skipped") is not None:
            skipped += 1
    return tests, failures, errors, skipped


def main(path: str) -> int:
    if not os.path.exists(path):
        print(f"[mark_skipped] No JUnit at {path}; nothing to do.")
        return 0

    try:
        tree = ET.parse(path)
    except ET.ParseError as e:
        print(f"[mark_skipped] Could not parse {path}: {e}")
        return 0

    root = tree.getroot()
    suites = root.findall("testsuite") if root.tag == "testsuites" else [root]

    changed = False
    for ts in suites:
        for case in list(ts.findall("testcase")):
            nodes = [n for n in list(case) if n.tag in ("failure", "error")]
            if not nodes:
                continue
            # If any node matches skip patterns, convert the whole case to skipped.
            first_match_text = None
            to_skip = False
            for n in nodes:
                msg = (n.get("message") or "") + "\n" + (n.text or "")
                if should_skip(msg):
                    first_match_text = (
                        n.text or "").strip() or first_match_text
                    to_skip = True
            if to_skip:
                for n in nodes:
                    case.remove(n)
                reason = "Marked skipped: environment/permission precondition not met"
                skip = ET.SubElement(case, "skipped")
                skip.set("message", reason)
                skip.text = first_match_text or reason
                changed = True
        # Recompute tallies per testsuite
        tests, failures, errors, skipped = summarize_counts(ts)
        ts.set("tests", str(tests))
        ts.set("failures", str(failures))
        ts.set("errors", str(errors))
        ts.set("skipped", str(skipped))

    if changed:
        tree.write(path, encoding="utf-8", xml_declaration=True)
        print(
            f"[mark_skipped] Updated {path}: converted environmental failures to skipped.")
    else:
        print(f"[mark_skipped] No environmental failures detected in {path}.")

    return 0


if __name__ == "__main__":
    target = (
        sys.argv[1]
        if len(sys.argv) > 1
        else os.environ.get("JUNIT_OUT", "reports/junit-nl-suite.xml")
    )
    raise SystemExit(main(target))

```

--------------------------------------------------------------------------------
/MCPForUnity/Editor/Data/PythonToolsAsset.cs:
--------------------------------------------------------------------------------

```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace MCPForUnity.Editor.Data
{
    /// <summary>
    /// Registry of Python tool files to sync to the MCP server.
    /// Add your Python files here - they can be stored anywhere in your project.
    /// </summary>
    [CreateAssetMenu(fileName = "PythonTools", menuName = "MCP For Unity/Python Tools")]
    public class PythonToolsAsset : ScriptableObject
    {
        [Tooltip("Add Python files (.py) to sync to the MCP server. Files can be located anywhere in your project.")]
        public List<TextAsset> pythonFiles = new List<TextAsset>();

        [Header("Sync Options")]
        [Tooltip("Use content hashing to detect changes (recommended). If false, always copies on startup.")]
        public bool useContentHashing = true;

        [Header("Sync State (Read-only)")]
        [Tooltip("Internal tracking - do not modify")]
        public List<PythonFileState> fileStates = new List<PythonFileState>();

        /// <summary>
        /// Gets all valid Python files (filters out null/missing references)
        /// </summary>
        public IEnumerable<TextAsset> GetValidFiles()
        {
            return pythonFiles.Where(f => f != null);
        }

        /// <summary>
        /// Checks if a file needs syncing
        /// </summary>
        public bool NeedsSync(TextAsset file, string currentHash)
        {
            if (!useContentHashing) return true; // Always sync if hashing disabled

            var state = fileStates.FirstOrDefault(s => s.assetGuid == GetAssetGuid(file));
            return state == null || state.contentHash != currentHash;
        }

        /// <summary>
        /// Records that a file was synced
        /// </summary>
        public void RecordSync(TextAsset file, string hash)
        {
            string guid = GetAssetGuid(file);
            var state = fileStates.FirstOrDefault(s => s.assetGuid == guid);

            if (state == null)
            {
                state = new PythonFileState { assetGuid = guid };
                fileStates.Add(state);
            }

            state.contentHash = hash;
            state.lastSyncTime = DateTime.UtcNow;
            state.fileName = file.name;
        }

        /// <summary>
        /// Removes state entries for files no longer in the list
        /// </summary>
        public void CleanupStaleStates()
        {
            var validGuids = new HashSet<string>(GetValidFiles().Select(GetAssetGuid));
            fileStates.RemoveAll(s => !validGuids.Contains(s.assetGuid));
        }

        private string GetAssetGuid(TextAsset asset)
        {
            return UnityEditor.AssetDatabase.AssetPathToGUID(UnityEditor.AssetDatabase.GetAssetPath(asset));
        }

        /// <summary>
        /// Called when the asset is modified in the Inspector
        /// Triggers sync to handle file additions/removals
        /// </summary>
        private void OnValidate()
        {
            // Cleanup stale states immediately
            CleanupStaleStates();
            
            // Trigger sync after a delay to handle file removals
            // Delay ensures the asset is saved before sync runs
            UnityEditor.EditorApplication.delayCall += () =>
            {
                if (this != null) // Check if asset still exists
                {
                    MCPForUnity.Editor.Helpers.PythonToolSyncProcessor.SyncAllTools();
                }
            };
        }
    }

    [Serializable]
    public class PythonFileState
    {
        public string assetGuid;
        public string fileName;
        public string contentHash;
        public DateTime lastSyncTime;
    }
}
```

--------------------------------------------------------------------------------
/tests/test_manage_script_uri.py:
--------------------------------------------------------------------------------

```python
import tools.manage_script as manage_script  # type: ignore
import sys
import types
from pathlib import Path

import pytest


# Locate server src dynamically to avoid hardcoded layout assumptions (same as other tests)
ROOT = Path(__file__).resolve().parents[1]
candidates = [
    ROOT / "MCPForUnity" / "UnityMcpServer~" / "src",
    ROOT / "UnityMcpServer~" / "src",
]
SRC = next((p for p in candidates if p.exists()), None)
if SRC is None:
    searched = "\n".join(str(p) for p in candidates)
    pytest.skip(
        "MCP for Unity server source not found. Tried:\n" + searched,
        allow_module_level=True,
    )
sys.path.insert(0, str(SRC))

# Stub mcp.server.fastmcp to satisfy imports without full package
mcp_pkg = types.ModuleType("mcp")
server_pkg = types.ModuleType("mcp.server")
fastmcp_pkg = types.ModuleType("mcp.server.fastmcp")


class _Dummy:
    pass


fastmcp_pkg.FastMCP = _Dummy
fastmcp_pkg.Context = _Dummy
server_pkg.fastmcp = fastmcp_pkg
mcp_pkg.server = server_pkg
sys.modules.setdefault("mcp", mcp_pkg)
sys.modules.setdefault("mcp.server", server_pkg)
sys.modules.setdefault("mcp.server.fastmcp", fastmcp_pkg)


# Import target module after path injection


class DummyMCP:
    def __init__(self):
        self.tools = {}

    def tool(self, *args, **kwargs):  # ignore decorator kwargs like description
        def _decorator(fn):
            self.tools[fn.__name__] = fn
            return fn
        return _decorator


class DummyCtx:  # FastMCP Context placeholder
    pass


def _register_tools():
    mcp = DummyMCP()
    manage_script.register_manage_script_tools(mcp)  # populates mcp.tools
    return mcp.tools


def test_split_uri_unity_path(monkeypatch):
    tools = _register_tools()
    captured = {}

    def fake_send(cmd, params):  # capture params and return success
        captured['cmd'] = cmd
        captured['params'] = params
        return {"success": True, "message": "ok"}

    monkeypatch.setattr(manage_script, "send_command_with_retry", fake_send)

    fn = tools['apply_text_edits']
    uri = "unity://path/Assets/Scripts/MyScript.cs"
    fn(DummyCtx(), uri=uri, edits=[], precondition_sha256=None)

    assert captured['cmd'] == 'manage_script'
    assert captured['params']['name'] == 'MyScript'
    assert captured['params']['path'] == 'Assets/Scripts'


@pytest.mark.parametrize(
    "uri, expected_name, expected_path",
    [
        ("file:///Users/alex/Project/Assets/Scripts/Foo%20Bar.cs",
         "Foo Bar", "Assets/Scripts"),
        ("file://localhost/Users/alex/Project/Assets/Hello.cs", "Hello", "Assets"),
        ("file:///C:/Users/Alex/Proj/Assets/Scripts/Hello.cs",
         "Hello", "Assets/Scripts"),
        # outside Assets → fall back to normalized dir
        ("file:///tmp/Other.cs", "Other", "tmp"),
    ],
)
def test_split_uri_file_urls(monkeypatch, uri, expected_name, expected_path):
    tools = _register_tools()
    captured = {}

    def fake_send(cmd, params):
        captured['cmd'] = cmd
        captured['params'] = params
        return {"success": True, "message": "ok"}

    monkeypatch.setattr(manage_script, "send_command_with_retry", fake_send)

    fn = tools['apply_text_edits']
    fn(DummyCtx(), uri=uri, edits=[], precondition_sha256=None)

    assert captured['params']['name'] == expected_name
    assert captured['params']['path'] == expected_path


def test_split_uri_plain_path(monkeypatch):
    tools = _register_tools()
    captured = {}

    def fake_send(cmd, params):
        captured['params'] = params
        return {"success": True, "message": "ok"}

    monkeypatch.setattr(manage_script, "send_command_with_retry", fake_send)

    fn = tools['apply_text_edits']
    fn(DummyCtx(), uri="Assets/Scripts/Thing.cs",
       edits=[], precondition_sha256=None)

    assert captured['params']['name'] == 'Thing'
    assert captured['params']['path'] == 'Assets/Scripts'

```

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

```csharp
using NUnit.Framework;
using MCPForUnity.Editor.Helpers;

namespace MCPForUnityTests.Editor.Helpers
{
    public class CodexConfigHelperTests
    {
        [Test]
        public void TryParseCodexServer_SingleLineArgs_ParsesSuccessfully()
        {
            string toml = string.Join("\n", new[]
            {
                "[mcp_servers.unityMCP]",
                "command = \"uv\"",
                "args = [\"run\", \"--directory\", \"/abs/path\", \"server.py\"]"
            });

            bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);

            Assert.IsTrue(result, "Parser should detect server definition");
            Assert.AreEqual("uv", command);
            CollectionAssert.AreEqual(new[] { "run", "--directory", "/abs/path", "server.py" }, args);
        }

        [Test]
        public void TryParseCodexServer_MultiLineArgsWithTrailingComma_ParsesSuccessfully()
        {
            string toml = string.Join("\n", new[]
            {
                "[mcp_servers.unityMCP]",
                "command = \"uv\"",
                "args = [",
                "  \"run\",",
                "  \"--directory\",",
                "  \"/abs/path\",",
                "  \"server.py\",",
                "]"
            });

            bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);

            Assert.IsTrue(result, "Parser should handle multi-line arrays with trailing comma");
            Assert.AreEqual("uv", command);
            CollectionAssert.AreEqual(new[] { "run", "--directory", "/abs/path", "server.py" }, args);
        }

        [Test]
        public void TryParseCodexServer_MultiLineArgsWithComments_IgnoresComments()
        {
            string toml = string.Join("\n", new[]
            {
                "[mcp_servers.unityMCP]",
                "command = \"uv\"",
                "args = [",
                "  \"run\", # launch command",
                "  \"--directory\",",
                "  \"/abs/path\",",
                "  \"server.py\"",
                "]"
            });

            bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);

            Assert.IsTrue(result, "Parser should tolerate comments within the array block");
            Assert.AreEqual("uv", command);
            CollectionAssert.AreEqual(new[] { "run", "--directory", "/abs/path", "server.py" }, args);
        }

        [Test]
        public void TryParseCodexServer_HeaderWithComment_StillDetected()
        {
            string toml = string.Join("\n", new[]
            {
                "[mcp_servers.unityMCP] # annotated header",
                "command = \"uv\"",
                "args = [\"run\", \"--directory\", \"/abs/path\", \"server.py\"]"
            });

            bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);

            Assert.IsTrue(result, "Parser should recognize section headers even with inline comments");
            Assert.AreEqual("uv", command);
            CollectionAssert.AreEqual(new[] { "run", "--directory", "/abs/path", "server.py" }, args);
        }

        [Test]
        public void TryParseCodexServer_SingleQuotedArgsWithApostrophes_ParsesSuccessfully()
        {
            string toml = string.Join("\n", new[]
            {
                "[mcp_servers.unityMCP]",
                "command = 'uv'",
                "args = ['run', '--directory', '/Users/O''Connor/codex', 'server.py']"
            });

            bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);

            Assert.IsTrue(result, "Parser should accept single-quoted arrays with escaped apostrophes");
            Assert.AreEqual("uv", command);
            CollectionAssert.AreEqual(new[] { "run", "--directory", "/Users/O'Connor/codex", "server.py" }, args);
        }
    }
}

```

--------------------------------------------------------------------------------
/deploy-dev.bat:
--------------------------------------------------------------------------------

```
@echo off
setlocal enabledelayedexpansion

echo ===============================================
echo MCP for Unity Development Deployment Script
echo ===============================================
echo.

:: Configuration
set "SCRIPT_DIR=%~dp0"
set "BRIDGE_SOURCE=%SCRIPT_DIR%MCPForUnity"
set "SERVER_SOURCE=%SCRIPT_DIR%MCPForUnity\UnityMcpServer~\src"
set "DEFAULT_BACKUP_DIR=%USERPROFILE%\Desktop\unity-mcp-backup"
set "DEFAULT_SERVER_PATH=%LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer\src"

:: Get user inputs
echo Please provide the following paths:
echo.

:: Package cache location
echo Unity Package Cache Location:
echo Example: X:\UnityProject\Library\PackageCache\[email protected]
set /p "PACKAGE_CACHE_PATH=Enter Unity package cache path: "

if "%PACKAGE_CACHE_PATH%"=="" (
    echo Error: Package cache path cannot be empty!
    pause
    exit /b 1
)

:: Server installation path (with default)
echo.
echo Server Installation Path:
echo Default: %DEFAULT_SERVER_PATH%
set /p "SERVER_PATH=Enter server path (or press Enter for default): "
if "%SERVER_PATH%"=="" set "SERVER_PATH=%DEFAULT_SERVER_PATH%"

:: Backup location (with default)
echo.
echo Backup Location:
echo Default: %DEFAULT_BACKUP_DIR%
set /p "BACKUP_DIR=Enter backup directory (or press Enter for default): "
if "%BACKUP_DIR%"=="" set "BACKUP_DIR=%DEFAULT_BACKUP_DIR%"

:: Validation
echo.
echo ===============================================
echo Validating paths...
echo ===============================================

if not exist "%BRIDGE_SOURCE%" (
    echo Error: Bridge source not found: %BRIDGE_SOURCE%
    pause
    exit /b 1
)

if not exist "%SERVER_SOURCE%" (
    echo Error: Server source not found: %SERVER_SOURCE%
    pause
    exit /b 1
)

if not exist "%PACKAGE_CACHE_PATH%" (
    echo Error: Package cache path not found: %PACKAGE_CACHE_PATH%
    pause
    exit /b 1
)

if not exist "%SERVER_PATH%" (
    echo Error: Server installation path not found: %SERVER_PATH%
    pause
    exit /b 1
)

:: Create backup directory
if not exist "%BACKUP_DIR%" (
    echo Creating backup directory: %BACKUP_DIR%
    mkdir "%BACKUP_DIR%"
)

:: Create timestamped backup subdirectory
set "TIMESTAMP=%date:~-4,4%%date:~-10,2%%date:~-7,2%_%time:~0,2%%time:~3,2%%time:~6,2%"
set "TIMESTAMP=%TIMESTAMP: =0%"
set "TIMESTAMP=%TIMESTAMP::=-%"
set "TIMESTAMP=%TIMESTAMP:/=-%"
set "BACKUP_SUBDIR=%BACKUP_DIR%\backup_%TIMESTAMP%"
mkdir "%BACKUP_SUBDIR%"

echo.
echo ===============================================
echo Starting deployment...
echo ===============================================

:: Backup original files
echo Creating backup of original files...
if exist "%PACKAGE_CACHE_PATH%\Editor" (
    echo Backing up Unity Bridge files...
    xcopy "%PACKAGE_CACHE_PATH%\Editor" "%BACKUP_SUBDIR%\UnityBridge\Editor\" /E /I /Y > nul
    if !errorlevel! neq 0 (
        echo Error: Failed to backup Unity Bridge files
        pause
        exit /b 1
    )
)

if exist "%SERVER_PATH%" (
    echo Backing up Python Server files...
    xcopy "%SERVER_PATH%\*" "%BACKUP_SUBDIR%\PythonServer\" /E /I /Y > nul
    if !errorlevel! neq 0 (
        echo Error: Failed to backup Python Server files
        pause
        exit /b 1
    )
)

:: Deploy Unity Bridge
echo.
echo Deploying Unity Bridge code...
xcopy "%BRIDGE_SOURCE%\Editor\*" "%PACKAGE_CACHE_PATH%\Editor\" /E /Y > nul
if !errorlevel! neq 0 (
    echo Error: Failed to deploy Unity Bridge code
    pause
    exit /b 1
)

:: Deploy Python Server
echo Deploying Python Server code...
xcopy "%SERVER_SOURCE%\*" "%SERVER_PATH%\" /E /Y > nul
if !errorlevel! neq 0 (
    echo Error: Failed to deploy Python Server code
    pause
    exit /b 1
)

:: Success
echo.
echo ===============================================
echo Deployment completed successfully!
echo ===============================================
echo.
echo Backup created at: %BACKUP_SUBDIR%
echo.
echo Next steps:
echo 1. Restart Unity Editor to load new Bridge code
echo 2. Restart any MCP clients to use new Server code
echo 3. Use restore-dev.bat to rollback if needed
echo.
pause
```

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

```csharp
using UnityEditor;
using UnityEngine;

namespace MCPForUnity.Editor.Helpers
{
    /// <summary>
    /// Auto-runs legacy/older install detection on package load/update (log-only).
    /// Runs once per embedded server version using an EditorPrefs version-scoped key.
    /// </summary>
    [InitializeOnLoad]
    public static class PackageDetector
    {
        private const string DetectOnceFlagKeyPrefix = "MCPForUnity.LegacyDetectLogged:";

        static PackageDetector()
        {
            try
            {
                string pkgVer = ReadPackageVersionOrFallback();
                string key = DetectOnceFlagKeyPrefix + pkgVer;

                // Always force-run if legacy roots exist or canonical install is missing
                bool legacyPresent = LegacyRootsExist();
                bool canonicalMissing = !System.IO.File.Exists(System.IO.Path.Combine(ServerInstaller.GetServerPath(), "server.py"));

                if (!EditorPrefs.GetBool(key, false) || legacyPresent || canonicalMissing)
                {
                    // Marshal the entire flow to the main thread. EnsureServerInstalled may touch Unity APIs.
                    EditorApplication.delayCall += () =>
                    {
                        string error = null;
                        System.Exception capturedEx = null;
                        try
                        {
                            // Ensure any UnityEditor API usage inside runs on the main thread
                            ServerInstaller.EnsureServerInstalled();
                        }
                        catch (System.Exception ex)
                        {
                            error = ex.Message;
                            capturedEx = ex;
                        }

                        // Unity APIs must stay on main thread
                        try { EditorPrefs.SetBool(key, true); } catch { }
                        // Ensure prefs cleanup happens on main thread
                        try { EditorPrefs.DeleteKey("MCPForUnity.ServerSrc"); } catch { }
                        try { EditorPrefs.DeleteKey("MCPForUnity.PythonDirOverride"); } catch { }

                        if (!string.IsNullOrEmpty(error))
                        {
                            McpLog.Info($"Server check: {error}. Download via Window > MCP For Unity if needed.", always: false);
                        }
                    };
                }
            }
            catch { /* ignore */ }
        }

        private static string ReadEmbeddedVersionOrFallback()
        {
            try
            {
                if (ServerPathResolver.TryFindEmbeddedServerSource(out var embeddedSrc))
                {
                    var p = System.IO.Path.Combine(embeddedSrc, "server_version.txt");
                    if (System.IO.File.Exists(p))
                        return (System.IO.File.ReadAllText(p)?.Trim() ?? "unknown");
                }
            }
            catch { }
            return "unknown";
        }

        private static string ReadPackageVersionOrFallback()
        {
            try
            {
                var info = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(PackageDetector).Assembly);
                if (info != null && !string.IsNullOrEmpty(info.version)) return info.version;
            }
            catch { }
            // Fallback to embedded server version if package info unavailable
            return ReadEmbeddedVersionOrFallback();
        }

        private static bool LegacyRootsExist()
        {
            try
            {
                string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile) ?? string.Empty;
                string[] roots =
                {
                    System.IO.Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer", "src"),
                    System.IO.Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer", "src")
                };
                foreach (var r in roots)
                {
                    try { if (System.IO.File.Exists(System.IO.Path.Combine(r, "server.py"))) return true; } catch { }
                }
            }
            catch { }
            return false;
        }
    }
}

```

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

```csharp
using UnityEditor;
using UnityEngine;

namespace MCPForUnity.Editor.Helpers
{
    /// <summary>
    /// Auto-runs legacy/older install detection on package load/update (log-only).
    /// Runs once per embedded server version using an EditorPrefs version-scoped key.
    /// </summary>
    [InitializeOnLoad]
    public static class PackageDetector
    {
        private const string DetectOnceFlagKeyPrefix = "MCPForUnity.LegacyDetectLogged:";

        static PackageDetector()
        {
            try
            {
                string pkgVer = ReadPackageVersionOrFallback();
                string key = DetectOnceFlagKeyPrefix + pkgVer;

                // Always force-run if legacy roots exist or canonical install is missing
                bool legacyPresent = LegacyRootsExist();
                bool canonicalMissing = !System.IO.File.Exists(System.IO.Path.Combine(ServerInstaller.GetServerPath(), "server.py"));

                if (!EditorPrefs.GetBool(key, false) || legacyPresent || canonicalMissing)
                {
                    // Marshal the entire flow to the main thread. EnsureServerInstalled may touch Unity APIs.
                    EditorApplication.delayCall += () =>
                    {
                        string error = null;
                        System.Exception capturedEx = null;
                        try
                        {
                            // Ensure any UnityEditor API usage inside runs on the main thread
                            ServerInstaller.EnsureServerInstalled();
                        }
                        catch (System.Exception ex)
                        {
                            error = ex.Message;
                            capturedEx = ex;
                        }

                        // Unity APIs must stay on main thread
                        try { EditorPrefs.SetBool(key, true); } catch { }
                        // Ensure prefs cleanup happens on main thread
                        try { EditorPrefs.DeleteKey("MCPForUnity.ServerSrc"); } catch { }
                        try { EditorPrefs.DeleteKey("MCPForUnity.PythonDirOverride"); } catch { }

                        if (!string.IsNullOrEmpty(error))
                        {
                            Debug.LogWarning($"MCP for Unity: Auto-detect on load failed: {capturedEx}");
                            // Alternatively: Debug.LogException(capturedEx);
                        }
                    };
                }
            }
            catch { /* ignore */ }
        }

        private static string ReadEmbeddedVersionOrFallback()
        {
            try
            {
                if (ServerPathResolver.TryFindEmbeddedServerSource(out var embeddedSrc))
                {
                    var p = System.IO.Path.Combine(embeddedSrc, "server_version.txt");
                    if (System.IO.File.Exists(p))
                        return (System.IO.File.ReadAllText(p)?.Trim() ?? "unknown");
                }
            }
            catch { }
            return "unknown";
        }

        private static string ReadPackageVersionOrFallback()
        {
            try
            {
                var info = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(PackageDetector).Assembly);
                if (info != null && !string.IsNullOrEmpty(info.version)) return info.version;
            }
            catch { }
            // Fallback to embedded server version if package info unavailable
            return ReadEmbeddedVersionOrFallback();
        }

        private static bool LegacyRootsExist()
        {
            try
            {
                string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile) ?? string.Empty;
                string[] roots =
                {
                    System.IO.Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer", "src"),
                    System.IO.Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer", "src")
                };
                foreach (var r in roots)
                {
                    try { if (System.IO.File.Exists(System.IO.Path.Combine(r, "server.py"))) return true; } catch { }
                }
            }
            catch { }
            return false;
        }
    }
}

```

--------------------------------------------------------------------------------
/.github/workflows/bump-version.yml:
--------------------------------------------------------------------------------

```yaml
name: Bump Version

on:
  workflow_dispatch:
    inputs:
      version_bump:
        description: "Version bump type"
        type: choice
        options:
          - patch
          - minor
          - major
        default: patch
        required: true

jobs:
  bump:
    name: "Bump version and tag"
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Compute new version
        id: compute
        shell: bash
        run: |
          set -euo pipefail
          BUMP="${{ inputs.version_bump }}"
          CURRENT_VERSION=$(jq -r '.version' "MCPForUnity/package.json")
          echo "Current version: $CURRENT_VERSION"

          IFS='.' read -r MA MI PA <<< "$CURRENT_VERSION"
          case "$BUMP" in
            major)
              ((MA+=1)); MI=0; PA=0
              ;;
            minor)
              ((MI+=1)); PA=0
              ;;
            patch)
              ((PA+=1))
              ;;
            *)
              echo "Unknown version_bump: $BUMP" >&2
              exit 1
              ;;
          esac

          NEW_VERSION="$MA.$MI.$PA"
          echo "New version: $NEW_VERSION"
          echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
          echo "current_version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"

      - name: Update files to new version
        env:
          NEW_VERSION: ${{ steps.compute.outputs.new_version }}
        shell: bash
        run: |
          set -euo pipefail

          echo "Updating MCPForUnity/package.json to $NEW_VERSION"
          jq ".version = \"${NEW_VERSION}\"" MCPForUnity/package.json > MCPForUnity/package.json.tmp
          mv MCPForUnity/package.json.tmp MCPForUnity/package.json

          echo "Updating MCPForUnity/UnityMcpServer~/src/pyproject.toml to $NEW_VERSION"
          sed -i '0,/^version = ".*"/s//version = "'"$NEW_VERSION"'"/' "MCPForUnity/UnityMcpServer~/src/pyproject.toml"

          echo "Updating MCPForUnity/UnityMcpServer~/src/server_version.txt to $NEW_VERSION"
          echo "$NEW_VERSION" > "MCPForUnity/UnityMcpServer~/src/server_version.txt"

      - name: Commit and push changes
        env:
          NEW_VERSION: ${{ steps.compute.outputs.new_version }}
        shell: bash
        run: |
          set -euo pipefail
          git config user.name "GitHub Actions"
          git config user.email "[email protected]"
          git add MCPForUnity/package.json "MCPForUnity/UnityMcpServer~/src/pyproject.toml" "MCPForUnity/UnityMcpServer~/src/server_version.txt"
          if git diff --cached --quiet; then
            echo "No version changes to commit."
          else
            git commit -m "chore: bump version to ${NEW_VERSION}"
          fi

          BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
          echo "Pushing to branch: $BRANCH"
          git push origin "$BRANCH"

      - name: Create and push tag
        env:
          NEW_VERSION: ${{ steps.compute.outputs.new_version }}
        shell: bash
        run: |
          set -euo pipefail
          TAG="v${NEW_VERSION}"
          echo "Preparing to create tag $TAG"

          if git ls-remote --tags origin | grep -q "refs/tags/$TAG$"; then
            echo "Tag $TAG already exists on remote. Skipping tag creation."
            exit 0
          fi

          git tag -a "$TAG" -m "Version ${NEW_VERSION}"
          git push origin "$TAG"

      - name: Package server for release
        env:
          NEW_VERSION: ${{ steps.compute.outputs.new_version }}
        shell: bash
        run: |
          set -euo pipefail
          cd MCPForUnity
          zip -r ../mcp-for-unity-server-v${NEW_VERSION}.zip UnityMcpServer~
          cd ..
          ls -lh mcp-for-unity-server-v${NEW_VERSION}.zip
          echo "Server package created: mcp-for-unity-server-v${NEW_VERSION}.zip"

      - name: Create GitHub release with server artifact
        env:
          NEW_VERSION: ${{ steps.compute.outputs.new_version }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        shell: bash
        run: |
          set -euo pipefail
          TAG="v${NEW_VERSION}"

          # Create release
          gh release create "$TAG" \
            --title "v${NEW_VERSION}" \
            --notes "Release v${NEW_VERSION}" \
            "mcp-for-unity-server-v${NEW_VERSION}.zip#MCP Server v${NEW_VERSION}"

```

--------------------------------------------------------------------------------
/UnityMcpBridge/UnityMcpServer~/src/telemetry_decorator.py:
--------------------------------------------------------------------------------

```python
"""
Telemetry decorator for Unity MCP tools
"""

import functools
import inspect
import logging
import time
from typing import Callable, Any

from telemetry import record_tool_usage, record_milestone, MilestoneType

_log = logging.getLogger("unity-mcp-telemetry")
_decorator_log_count = 0


def telemetry_tool(tool_name: str):
    """Decorator to add telemetry tracking to MCP tools"""
    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        def _sync_wrapper(*args, **kwargs) -> Any:
            start_time = time.time()
            success = False
            error = None
            # Extract sub-action (e.g., 'get_hierarchy') from bound args when available
            sub_action = None
            try:
                sig = inspect.signature(func)
                bound = sig.bind_partial(*args, **kwargs)
                bound.apply_defaults()
                sub_action = bound.arguments.get("action")
            except Exception:
                sub_action = None
            try:
                global _decorator_log_count
                if _decorator_log_count < 10:
                    _log.info(f"telemetry_decorator sync: tool={tool_name}")
                    _decorator_log_count += 1
                result = func(*args, **kwargs)
                success = True
                action_val = sub_action or kwargs.get("action")
                try:
                    if tool_name == "manage_script" and action_val == "create":
                        record_milestone(MilestoneType.FIRST_SCRIPT_CREATION)
                    elif tool_name.startswith("manage_scene"):
                        record_milestone(
                            MilestoneType.FIRST_SCENE_MODIFICATION)
                    record_milestone(MilestoneType.FIRST_TOOL_USAGE)
                except Exception:
                    _log.debug("milestone emit failed", exc_info=True)
                return result
            except Exception as e:
                error = str(e)
                raise
            finally:
                duration_ms = (time.time() - start_time) * 1000
                try:
                    record_tool_usage(tool_name, success,
                                      duration_ms, error, sub_action=sub_action)
                except Exception:
                    _log.debug("record_tool_usage failed", exc_info=True)

        @functools.wraps(func)
        async def _async_wrapper(*args, **kwargs) -> Any:
            start_time = time.time()
            success = False
            error = None
            # Extract sub-action (e.g., 'get_hierarchy') from bound args when available
            sub_action = None
            try:
                sig = inspect.signature(func)
                bound = sig.bind_partial(*args, **kwargs)
                bound.apply_defaults()
                sub_action = bound.arguments.get("action")
            except Exception:
                sub_action = None
            try:
                global _decorator_log_count
                if _decorator_log_count < 10:
                    _log.info(f"telemetry_decorator async: tool={tool_name}")
                    _decorator_log_count += 1
                result = await func(*args, **kwargs)
                success = True
                action_val = sub_action or kwargs.get("action")
                try:
                    if tool_name == "manage_script" and action_val == "create":
                        record_milestone(MilestoneType.FIRST_SCRIPT_CREATION)
                    elif tool_name.startswith("manage_scene"):
                        record_milestone(
                            MilestoneType.FIRST_SCENE_MODIFICATION)
                    record_milestone(MilestoneType.FIRST_TOOL_USAGE)
                except Exception:
                    _log.debug("milestone emit failed", exc_info=True)
                return result
            except Exception as e:
                error = str(e)
                raise
            finally:
                duration_ms = (time.time() - start_time) * 1000
                try:
                    record_tool_usage(tool_name, success,
                                      duration_ms, error, sub_action=sub_action)
                except Exception:
                    _log.debug("record_tool_usage failed", exc_info=True)

        return _async_wrapper if inspect.iscoroutinefunction(func) else _sync_wrapper
    return decorator

```

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

```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;

namespace MCPForUnity.Editor.Tools
{
    /// <summary>
    /// Registry for all MCP command handlers via reflection.
    /// </summary>
    public static class CommandRegistry
    {
        private static readonly Dictionary<string, Func<JObject, object>> _handlers = new();
        private static bool _initialized = false;

        /// <summary>
        /// Initialize and auto-discover all tools marked with [McpForUnityTool]
        /// </summary>
        public static void Initialize()
        {
            if (_initialized) return;

            AutoDiscoverTools();
            _initialized = true;
        }

        /// <summary>
        /// Convert PascalCase or camelCase to snake_case
        /// </summary>
        private static string ToSnakeCase(string name)
        {
            if (string.IsNullOrEmpty(name)) return name;

            // Insert underscore before uppercase letters (except first)
            var s1 = Regex.Replace(name, "(.)([A-Z][a-z]+)", "$1_$2");
            var s2 = Regex.Replace(s1, "([a-z0-9])([A-Z])", "$1_$2");
            return s2.ToLower();
        }

        /// <summary>
        /// Auto-discover all types with [McpForUnityTool] attribute
        /// </summary>
        private static void AutoDiscoverTools()
        {
            try
            {
                var toolTypes = AppDomain.CurrentDomain.GetAssemblies()
                    .Where(a => !a.IsDynamic)
                    .SelectMany(a =>
                    {
                        try { return a.GetTypes(); }
                        catch { return new Type[0]; }
                    })
                    .Where(t => t.GetCustomAttribute<McpForUnityToolAttribute>() != null);

                foreach (var type in toolTypes)
                {
                    RegisterToolType(type);
                }

                McpLog.Info($"Auto-discovered {_handlers.Count} tools");
            }
            catch (Exception ex)
            {
                McpLog.Error($"Failed to auto-discover MCP tools: {ex.Message}");
            }
        }

        private static void RegisterToolType(Type type)
        {
            var attr = type.GetCustomAttribute<McpForUnityToolAttribute>();

            // Get command name (explicit or auto-generated)
            string commandName = attr.CommandName;
            if (string.IsNullOrEmpty(commandName))
            {
                commandName = ToSnakeCase(type.Name);
            }

            // Check for duplicate command names
            if (_handlers.ContainsKey(commandName))
            {
                McpLog.Warn(
                    $"Duplicate command name '{commandName}' detected. " +
                    $"Tool {type.Name} will override previously registered handler."
                );
            }

            // Find HandleCommand method
            var method = type.GetMethod(
                "HandleCommand",
                BindingFlags.Public | BindingFlags.Static,
                null,
                new[] { typeof(JObject) },
                null
            );

            if (method == null)
            {
                McpLog.Warn(
                    $"MCP tool {type.Name} is marked with [McpForUnityTool] " +
                    $"but has no public static HandleCommand(JObject) method"
                );
                return;
            }

            try
            {
                var handler = (Func<JObject, object>)Delegate.CreateDelegate(
                    typeof(Func<JObject, object>),
                    method
                );
                _handlers[commandName] = handler;
            }
            catch (Exception ex)
            {
                McpLog.Error($"Failed to register tool {type.Name}: {ex.Message}");
            }
        }

        /// <summary>
        /// Get a command handler by name
        /// </summary>
        public static Func<JObject, object> GetHandler(string commandName)
        {
            if (!_handlers.TryGetValue(commandName, out var handler))
            {
                throw new InvalidOperationException(
                    $"Unknown or unsupported command type: {commandName}"
                );
            }
            return handler;
        }
    }
}

```

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

```csharp
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using UnityEngine;
using MCPForUnity.Editor.Data;
using MCPForUnity.Editor.Services;

namespace MCPForUnityTests.Editor.Services
{
    public class PythonToolRegistryServiceTests
    {
        private PythonToolRegistryService _service;

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

        [Test]
        public void GetAllRegistries_ReturnsEmptyList_WhenNoPythonToolsAssetsExist()
        {
            var registries = _service.GetAllRegistries().ToList();

            // Note: This might find assets in the test project, so we just verify it doesn't throw
            Assert.IsNotNull(registries, "Should return a non-null list");
        }

        [Test]
        public void NeedsSync_ReturnsTrue_WhenHashingDisabled()
        {
            var asset = ScriptableObject.CreateInstance<PythonToolsAsset>();
            asset.useContentHashing = false;

            var textAsset = new TextAsset("print('test')");

            bool needsSync = _service.NeedsSync(asset, textAsset);

            Assert.IsTrue(needsSync, "Should always need sync when hashing is disabled");

            Object.DestroyImmediate(asset);
        }

        [Test]
        public void NeedsSync_ReturnsTrue_WhenFileNotPreviouslySynced()
        {
            var asset = ScriptableObject.CreateInstance<PythonToolsAsset>();
            asset.useContentHashing = true;

            var textAsset = new TextAsset("print('test')");

            bool needsSync = _service.NeedsSync(asset, textAsset);

            Assert.IsTrue(needsSync, "Should need sync for new file");

            Object.DestroyImmediate(asset);
        }

        [Test]
        public void NeedsSync_ReturnsFalse_WhenHashMatches()
        {
            var asset = ScriptableObject.CreateInstance<PythonToolsAsset>();
            asset.useContentHashing = true;

            var textAsset = new TextAsset("print('test')");

            // First sync
            _service.RecordSync(asset, textAsset);

            // Check if needs sync again
            bool needsSync = _service.NeedsSync(asset, textAsset);

            Assert.IsFalse(needsSync, "Should not need sync when hash matches");

            Object.DestroyImmediate(asset);
        }

        [Test]
        public void RecordSync_StoresFileState()
        {
            var asset = ScriptableObject.CreateInstance<PythonToolsAsset>();
            var textAsset = new TextAsset("print('test')");

            _service.RecordSync(asset, textAsset);

            Assert.AreEqual(1, asset.fileStates.Count, "Should have one file state recorded");
            Assert.IsNotNull(asset.fileStates[0].contentHash, "Hash should be stored");
            Assert.IsNotNull(asset.fileStates[0].assetGuid, "GUID should be stored");

            Object.DestroyImmediate(asset);
        }

        [Test]
        public void RecordSync_UpdatesExistingState_WhenFileAlreadyRecorded()
        {
            var asset = ScriptableObject.CreateInstance<PythonToolsAsset>();
            var textAsset = new TextAsset("print('test')");

            // Record twice
            _service.RecordSync(asset, textAsset);
            var firstHash = asset.fileStates[0].contentHash;

            _service.RecordSync(asset, textAsset);

            Assert.AreEqual(1, asset.fileStates.Count, "Should still have only one state");
            Assert.AreEqual(firstHash, asset.fileStates[0].contentHash, "Hash should remain the same");

            Object.DestroyImmediate(asset);
        }

        [Test]
        public void ComputeHash_ReturnsSameHash_ForSameContent()
        {
            var textAsset1 = new TextAsset("print('hello')");
            var textAsset2 = new TextAsset("print('hello')");

            string hash1 = _service.ComputeHash(textAsset1);
            string hash2 = _service.ComputeHash(textAsset2);

            Assert.AreEqual(hash1, hash2, "Same content should produce same hash");
        }

        [Test]
        public void ComputeHash_ReturnsDifferentHash_ForDifferentContent()
        {
            var textAsset1 = new TextAsset("print('hello')");
            var textAsset2 = new TextAsset("print('world')");

            string hash1 = _service.ComputeHash(textAsset1);
            string hash2 = _service.ComputeHash(textAsset2);

            Assert.AreNotEqual(hash1, hash2, "Different content should produce different hash");
        }
    }
}

```

--------------------------------------------------------------------------------
/restore-dev.bat:
--------------------------------------------------------------------------------

```
@echo off
setlocal enabledelayedexpansion

echo ===============================================
echo MCP for Unity Development Restore Script
echo ===============================================
echo.
echo Note: The Python server is bundled under MCPForUnity\UnityMcpServer~ in the package.
echo       This script restores your installed server path from backups, not the repo copy.
echo.

:: Configuration
set "DEFAULT_BACKUP_DIR=%USERPROFILE%\Desktop\unity-mcp-backup"
set "DEFAULT_SERVER_PATH=%LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer\src"

:: Get user inputs
echo Please provide the following paths:
echo.

:: Package cache location
echo Unity Package Cache Location:
echo Example: X:\UnityProject\Library\PackageCache\[email protected]
set /p "PACKAGE_CACHE_PATH=Enter Unity package cache path: "

if "%PACKAGE_CACHE_PATH%"=="" (
    echo Error: Package cache path cannot be empty!
    pause
    exit /b 1
)

:: Server installation path (with default)
echo.
echo Server Installation Path:
echo Default: %DEFAULT_SERVER_PATH%
set /p "SERVER_PATH=Enter server path (or press Enter for default): "
if "%SERVER_PATH%"=="" set "SERVER_PATH=%DEFAULT_SERVER_PATH%"

:: Backup location (with default)
echo.
echo Backup Location:
echo Default: %DEFAULT_BACKUP_DIR%
set /p "BACKUP_DIR=Enter backup directory (or press Enter for default): "
if "%BACKUP_DIR%"=="" set "BACKUP_DIR=%DEFAULT_BACKUP_DIR%"

:: List available backups
echo.
echo ===============================================
echo Available backups:
echo ===============================================
set "counter=0"
for /d %%d in ("%BACKUP_DIR%\backup_*") do (
    set /a counter+=1
    set "backup!counter!=%%d"
    echo !counter!. %%~nxd
)

if %counter%==0 (
    echo No backups found in %BACKUP_DIR%
    pause
    exit /b 1
)

echo.
set /p "choice=Select backup to restore (1-%counter%): "

:: Validate choice
if "%choice%"=="" goto :invalid_choice
if %choice% lss 1 goto :invalid_choice
if %choice% gtr %counter% goto :invalid_choice

set "SELECTED_BACKUP=!backup%choice%!"
echo.
echo Selected backup: %SELECTED_BACKUP%

:: Validation
echo.
echo ===============================================
echo Validating paths...
echo ===============================================

if not exist "%SELECTED_BACKUP%" (
    echo Error: Selected backup not found: %SELECTED_BACKUP%
    pause
    exit /b 1
)

if not exist "%PACKAGE_CACHE_PATH%" (
    echo Error: Package cache path not found: %PACKAGE_CACHE_PATH%
    pause
    exit /b 1
)

if not exist "%SERVER_PATH%" (
    echo Error: Server installation path not found: %SERVER_PATH%
    pause
    exit /b 1
)

:: Confirm restore
echo.
echo ===============================================
echo WARNING: This will overwrite current files!
echo ===============================================
echo Restoring from: %SELECTED_BACKUP%
echo Unity Bridge target: %PACKAGE_CACHE_PATH%\Editor
echo Python Server target: %SERVER_PATH%
echo.
set /p "confirm=Continue with restore? (y/N): "
if /i not "%confirm%"=="y" (
    echo Restore cancelled.
    pause
    exit /b 0
)

echo.
echo ===============================================
echo Starting restore...
echo ===============================================

:: Restore Unity Bridge
if exist "%SELECTED_BACKUP%\UnityBridge\Editor" (
    echo Restoring Unity Bridge files...
    rd /s /q "%PACKAGE_CACHE_PATH%\Editor" 2>nul
    xcopy "%SELECTED_BACKUP%\UnityBridge\Editor\*" "%PACKAGE_CACHE_PATH%\Editor\" /E /I /Y > nul
    if !errorlevel! neq 0 (
        echo Error: Failed to restore Unity Bridge files
        pause
        exit /b 1
    )
) else (
    echo Warning: No Unity Bridge backup found, skipping...
)

:: Restore Python Server
if exist "%SELECTED_BACKUP%\PythonServer" (
    echo Restoring Python Server files...
    rd /s /q "%SERVER_PATH%" 2>nul
    mkdir "%SERVER_PATH%"
    xcopy "%SELECTED_BACKUP%\PythonServer\*" "%SERVER_PATH%\" /E /I /Y > nul
    if !errorlevel! neq 0 (
        echo Error: Failed to restore Python Server files
        pause
        exit /b 1
    )
) else (
    echo Warning: No Python Server backup found, skipping...
)

:: Success
echo.
echo ===============================================
echo Restore completed successfully!
echo ===============================================
echo.
echo Next steps:
echo 1. Restart Unity Editor to load restored Bridge code
echo 2. Restart any MCP clients to use restored Server code
echo.
pause
exit /b 0

:invalid_choice
echo Invalid choice. Please enter a number between 1 and %counter%.
pause
exit /b 1
```

--------------------------------------------------------------------------------
/tests/test_edit_normalization_and_noop.py:
--------------------------------------------------------------------------------

```python
import sys
import pathlib
import importlib.util
import types


ROOT = pathlib.Path(__file__).resolve().parents[1]
SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src"
sys.path.insert(0, str(SRC))

# stub mcp.server.fastmcp
mcp_pkg = types.ModuleType("mcp")
server_pkg = types.ModuleType("mcp.server")
fastmcp_pkg = types.ModuleType("mcp.server.fastmcp")


class _Dummy:
    pass


fastmcp_pkg.FastMCP = _Dummy
fastmcp_pkg.Context = _Dummy
server_pkg.fastmcp = fastmcp_pkg
mcp_pkg.server = server_pkg
sys.modules.setdefault("mcp", mcp_pkg)
sys.modules.setdefault("mcp.server", server_pkg)
sys.modules.setdefault("mcp.server.fastmcp", fastmcp_pkg)


def _load(path: pathlib.Path, name: str):
    spec = importlib.util.spec_from_file_location(name, path)
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


manage_script = _load(SRC / "tools" / "manage_script.py", "manage_script_mod2")
manage_script_edits = _load(
    SRC / "tools" / "manage_script_edits.py", "manage_script_edits_mod2")


class DummyMCP:
    def __init__(self): self.tools = {}

    def tool(self, *args, **kwargs):
        def deco(fn): self.tools[fn.__name__] = fn; return fn
        return deco


def setup_tools():
    mcp = DummyMCP()
    manage_script.register_manage_script_tools(mcp)
    return mcp.tools


def test_normalizes_lsp_and_index_ranges(monkeypatch):
    tools = setup_tools()
    apply = tools["apply_text_edits"]
    calls = []

    def fake_send(cmd, params):
        calls.append(params)
        return {"success": True}

    monkeypatch.setattr(manage_script, "send_command_with_retry", fake_send)

    # LSP-style
    edits = [{
        "range": {"start": {"line": 10, "character": 2}, "end": {"line": 10, "character": 2}},
        "newText": "// lsp\n"
    }]
    apply(None, uri="unity://path/Assets/Scripts/F.cs",
          edits=edits, precondition_sha256="x")
    p = calls[-1]
    e = p["edits"][0]
    assert e["startLine"] == 11 and e["startCol"] == 3

    # Index pair
    calls.clear()
    edits = [{"range": [0, 0], "text": "// idx\n"}]
    # fake read to provide contents length

    def fake_read(cmd, params):
        if params.get("action") == "read":
            return {"success": True, "data": {"contents": "hello\n"}}
        return {"success": True}
    monkeypatch.setattr(manage_script, "send_command_with_retry", fake_read)
    apply(None, uri="unity://path/Assets/Scripts/F.cs",
          edits=edits, precondition_sha256="x")
    # last call is apply_text_edits


def test_noop_evidence_shape(monkeypatch):
    tools = setup_tools()
    apply = tools["apply_text_edits"]
    # Route response from Unity indicating no-op

    def fake_send(cmd, params):
        return {"success": True, "data": {"no_op": True, "evidence": {"reason": "identical_content"}}}
    monkeypatch.setattr(manage_script, "send_command_with_retry", fake_send)

    resp = apply(None, uri="unity://path/Assets/Scripts/F.cs", edits=[
                 {"startLine": 1, "startCol": 1, "endLine": 1, "endCol": 1, "newText": ""}], precondition_sha256="x")
    assert resp["success"] is True
    assert resp.get("data", {}).get("no_op") is True


def test_atomic_multi_span_and_relaxed(monkeypatch):
    tools_text = setup_tools()
    apply_text = tools_text["apply_text_edits"]
    tools_struct = DummyMCP()
    manage_script_edits.register_manage_script_edits_tools(tools_struct)
    # Fake send for read and write; verify atomic applyMode and validate=relaxed passes through
    sent = {}

    def fake_send(cmd, params):
        if params.get("action") == "read":
            return {"success": True, "data": {"contents": "public class C{\nvoid M(){ int x=2; }\n}\n"}}
        sent.setdefault("calls", []).append(params)
        return {"success": True}
    monkeypatch.setattr(manage_script, "send_command_with_retry", fake_send)

    edits = [
        {"startLine": 2, "startCol": 14, "endLine": 2, "endCol": 15, "newText": "3"},
        {"startLine": 3, "startCol": 2, "endLine": 3,
            "endCol": 2, "newText": "// tail\n"}
    ]
    resp = apply_text(None, uri="unity://path/Assets/Scripts/C.cs", edits=edits,
                      precondition_sha256="sha", options={"validate": "relaxed", "applyMode": "atomic"})
    assert resp["success"] is True
    # Last manage_script call should include options with applyMode atomic and validate relaxed
    last = sent["calls"][-1]
    assert last.get("options", {}).get("applyMode") == "atomic"
    assert last.get("options", {}).get("validate") == "relaxed"

```

--------------------------------------------------------------------------------
/docs/CURSOR_HELP.md:
--------------------------------------------------------------------------------

```markdown
### Cursor/VSCode/Windsurf: UV path issue on Windows (diagnosis and fix)

#### The issue
- Some Windows machines have multiple `uv.exe` locations. Our auto-config sometimes picked a less stable path, causing the MCP client to fail to launch the MCP for Unity Server or for the path to be auto-rewritten on repaint/restart.

#### Typical symptoms
- Cursor shows the MCP for Unity server but never connects or reports it “can’t start.”
- Your `%USERPROFILE%\\.cursor\\mcp.json` flips back to a different `command` path when Unity or the MCP for Unity window refreshes.

#### Real-world example
- Wrong/fragile path (auto-picked):
  - `C:\Users\mrken.local\bin\uv.exe` (malformed, not standard)
  - `C:\Users\mrken\AppData\Local\Microsoft\WinGet\Packages\astral-sh.uv_Microsoft.Winget.Source_8wekyb3d8bbwe\uv.exe`
- Correct/stable path (works with Cursor):
  - `C:\Users\mrken\AppData\Local\Microsoft\WinGet\Links\uv.exe`

#### Quick fix (recommended)
1) In MCP for Unity: `Window > MCP for Unity` → select your MCP client (Cursor or Windsurf)
2) If you see “uv Not Found,” click “Choose `uv` Install Location” and browse to:
   - `C:\Users\<YOU>\AppData\Local\Microsoft\WinGet\Links\uv.exe`
3) If uv is already found but wrong, still click “Choose `uv` Install Location” and select the `Links\uv.exe` path above. This saves a persistent override.
4) Click “Auto Configure” (or re-open the client) and restart Cursor.

This sets an override stored in the Editor (key: `MCPForUnity.UvPath`) so MCP for Unity won’t auto-rewrite the config back to a different `uv.exe` later.

#### Verify the fix
- Confirm global Cursor config is at: `%USERPROFILE%\\.cursor\\mcp.json`
- You should see something like:

```json
{
  "mcpServers": {
    "unityMCP": {
      "command": "C:\\Users\\YOU\\AppData\\Local\\Microsoft\\WinGet\\Links\\uv.exe",
      "args": [
        "--directory",
        "C:\\Users\\YOU\\AppData\\Local\\Programs\\UnityMCP\\UnityMcpServer\\src",
        "run",
        "server.py"
      ]
    }
  }
}
```

- Manually run the same command in PowerShell to confirm it launches:

```powershell
"C:\Users\YOU\AppData\Local\Microsoft\WinGet\Links\uv.exe" --directory "C:\Users\YOU\AppData\Local\Programs\UnityMCP\UnityMcpServer\src" run server.py
```

If that runs without error, restart Cursor and it should connect.

#### Why this happens
- On Windows, multiple `uv.exe` can exist (WinGet Packages path, a WinGet Links shim, Python Scripts, etc.). The Links shim is the most stable target for GUI apps to launch.
- Prior versions of the auto-config could pick the first found path and re-write config on refresh. Choosing a path via the MCP window pins a known‑good absolute path and prevents auto-rewrites.

#### Extra notes
- Restart Cursor after changing `mcp.json`; it doesn’t always hot-reload that file.
- If you also have a project-scoped `.cursor\\mcp.json` in your Unity project folder, that file overrides the global one.


### Why pin the WinGet Links shim (and not the Packages path)

- Windows often has multiple `uv.exe` installs and GUI clients (Cursor/Windsurf/VSCode) may launch with a reduced `PATH`. Using an absolute path is safer than `"command": "uv"`.
- WinGet publishes stable launch shims in these locations:
  - User scope: `%LOCALAPPDATA%\Microsoft\WinGet\Links\uv.exe`
  - Machine scope: `C:\Program Files\WinGet\Links\uv.exe`
  These shims survive upgrades and are intended as the portable entrypoints. See the WinGet notes: [discussion](https://github.com/microsoft/winget-pkgs/discussions/184459) • [how to find installs](https://superuser.com/questions/1739292/how-to-know-where-winget-installed-a-program)
- The `Packages` root is where payloads live and can change across updates, so avoid pointing your config at it.

Recommended practice

- Prefer the WinGet Links shim paths above. If present, select one via “Choose `uv` Install Location”.
- If the unity window keeps rewriting to a different `uv.exe`, pick the Links shim again; MCP for Unity saves a pinned override and will stop auto-rewrites.
- If neither Links path exists, a reasonable fallback is `~/.local/bin/uv.exe` (uv tools bin) or a Scoop shim, but Links is preferred for stability.

References

- WinGet portable Links: [GitHub discussion](https://github.com/microsoft/winget-pkgs/discussions/184459)
- WinGet install locations: [Super User](https://superuser.com/questions/1739292/how-to-know-where-winget-installed-a-program)
- GUI client PATH caveats (Cursor): [Cursor community thread](https://forum.cursor.com/t/mcp-feature-client-closed-fix/54651?page=4)
- uv tools install location (`~/.local/bin`): [Astral docs](https://docs.astral.sh/uv/concepts/tools/)



```

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

```csharp
using System;
using MCPForUnity.Editor.Dependencies;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Windows;
using UnityEditor;
using UnityEngine;

namespace MCPForUnity.Editor.Setup
{
    /// <summary>
    /// Handles automatic triggering of the setup wizard
    /// </summary>
    [InitializeOnLoad]
    public static class SetupWizard
    {
        private const string SETUP_COMPLETED_KEY = "MCPForUnity.SetupCompleted";
        private const string SETUP_DISMISSED_KEY = "MCPForUnity.SetupDismissed";
        private static bool _hasCheckedThisSession = false;

        static SetupWizard()
        {
            // Skip in batch mode
            if (Application.isBatchMode)
                return;

            // Show setup wizard on package import
            EditorApplication.delayCall += CheckSetupNeeded;
        }

        /// <summary>
        /// Check if setup wizard should be shown
        /// </summary>
        private static void CheckSetupNeeded()
        {
            if (_hasCheckedThisSession)
                return;

            _hasCheckedThisSession = true;

            try
            {
                // Check if setup was already completed or dismissed in previous sessions
                bool setupCompleted = EditorPrefs.GetBool(SETUP_COMPLETED_KEY, false);
                bool setupDismissed = EditorPrefs.GetBool(SETUP_DISMISSED_KEY, false);

                // Only show setup wizard if it hasn't been completed or dismissed before
                if (!(setupCompleted || setupDismissed))
                {
                    McpLog.Info("Package imported - showing setup wizard", always: false);

                    var dependencyResult = DependencyManager.CheckAllDependencies();
                    EditorApplication.delayCall += () => ShowSetupWizard(dependencyResult);
                }
                else
                {
                    McpLog.Info("Setup wizard skipped - previously completed or dismissed", always: false);
                }
            }
            catch (Exception ex)
            {
                McpLog.Error($"Error checking setup status: {ex.Message}");
            }
        }

        /// <summary>
        /// Show the setup wizard window
        /// </summary>
        public static void ShowSetupWizard(DependencyCheckResult dependencyResult = null)
        {
            try
            {
                dependencyResult ??= DependencyManager.CheckAllDependencies();
                SetupWizardWindow.ShowWindow(dependencyResult);
            }
            catch (Exception ex)
            {
                McpLog.Error($"Error showing setup wizard: {ex.Message}");
            }
        }

        /// <summary>
        /// Mark setup as completed
        /// </summary>
        public static void MarkSetupCompleted()
        {
            EditorPrefs.SetBool(SETUP_COMPLETED_KEY, true);
            McpLog.Info("Setup marked as completed");
        }

        /// <summary>
        /// Mark setup as dismissed
        /// </summary>
        public static void MarkSetupDismissed()
        {
            EditorPrefs.SetBool(SETUP_DISMISSED_KEY, true);
            McpLog.Info("Setup marked as dismissed");
        }

        /// <summary>
        /// Force show setup wizard (for manual invocation)
        /// </summary>
        [MenuItem("Window/MCP For Unity/Setup Wizard", priority = 1)]
        public static void ShowSetupWizardManual()
        {
            ShowSetupWizard();
        }

        /// <summary>
        /// Check dependencies and show status
        /// </summary>
        [MenuItem("Window/MCP For Unity/Check Dependencies", priority = 3)]
        public static void CheckDependencies()
        {
            var result = DependencyManager.CheckAllDependencies();

            if (!result.IsSystemReady)
            {
                bool showWizard = EditorUtility.DisplayDialog(
                    "MCP for Unity - Dependencies",
                    $"System Status: {result.Summary}\n\nWould you like to open the Setup Wizard?",
                    "Open Setup Wizard",
                    "Close"
                );

                if (showWizard)
                {
                    ShowSetupWizard(result);
                }
            }
            else
            {
                EditorUtility.DisplayDialog(
                    "MCP for Unity - Dependencies",
                    "✓ All dependencies are available and ready!\n\nMCP for Unity is ready to use.",
                    "OK"
                );
            }
        }

        /// <summary>
        /// Open MCP Client Configuration window
        /// </summary>
        [MenuItem("Window/MCP For Unity/Open MCP Window", priority = 4)]
        public static void OpenClientConfiguration()
        {
            Windows.MCPForUnityEditorWindow.ShowWindow();
        }
    }
}

```

--------------------------------------------------------------------------------
/tests/test_improved_anchor_matching.py:
--------------------------------------------------------------------------------

```python
"""
Test the improved anchor matching logic.
"""

import sys
import pathlib
import importlib.util
import types

# add server src to path and load modules
ROOT = pathlib.Path(__file__).resolve().parents[1]
SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src"
sys.path.insert(0, str(SRC))

# stub mcp.server.fastmcp
mcp_pkg = types.ModuleType("mcp")
server_pkg = types.ModuleType("mcp.server")
fastmcp_pkg = types.ModuleType("mcp.server.fastmcp")


class _Dummy:
    pass


fastmcp_pkg.FastMCP = _Dummy
fastmcp_pkg.Context = _Dummy
server_pkg.fastmcp = fastmcp_pkg
mcp_pkg.server = server_pkg
sys.modules.setdefault("mcp", mcp_pkg)
sys.modules.setdefault("mcp.server", server_pkg)
sys.modules.setdefault("mcp.server.fastmcp", fastmcp_pkg)


def load_module(path, name):
    spec = importlib.util.spec_from_file_location(name, path)
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


manage_script_edits_module = load_module(
    SRC / "tools" / "manage_script_edits.py", "manage_script_edits_module")


def test_improved_anchor_matching():
    """Test that our improved anchor matching finds the right closing brace."""

    test_code = '''using UnityEngine;

public class TestClass : MonoBehaviour  
{
    void Start()
    {
        Debug.Log("test");
    }
    
    void Update()
    {
        // Update logic
    }
}'''

    import re

    # Test the problematic anchor pattern
    anchor_pattern = r"\s*}\s*$"
    flags = re.MULTILINE

    # Test our improved function
    best_match = manage_script_edits_module._find_best_anchor_match(
        anchor_pattern, test_code, flags, prefer_last=True
    )

    assert best_match is not None, "anchor pattern not found"
    match_pos = best_match.start()
    line_num = test_code[:match_pos].count('\n') + 1
    total_lines = test_code.count('\n') + 1
    assert line_num >= total_lines - \
        2, f"expected match near end (>= {total_lines-2}), got line {line_num}"


def test_old_vs_new_matching():
    """Compare old vs new matching behavior."""

    test_code = '''using UnityEngine;

public class TestClass : MonoBehaviour  
{
    void Start()
    {
        Debug.Log("test");
    }
    
    void Update()
    {
        if (condition)
        {
            DoSomething();
        }
    }
    
    void LateUpdate()
    {
        // More logic
    }
}'''

    import re

    anchor_pattern = r"\s*}\s*$"
    flags = re.MULTILINE

    # Old behavior (first match)
    old_match = re.search(anchor_pattern, test_code, flags)
    old_line = test_code[:old_match.start()].count(
        '\n') + 1 if old_match else None

    # New behavior (improved matching)
    new_match = manage_script_edits_module._find_best_anchor_match(
        anchor_pattern, test_code, flags, prefer_last=True
    )
    new_line = test_code[:new_match.start()].count(
        '\n') + 1 if new_match else None

    assert old_line is not None and new_line is not None, "failed to locate anchors"
    assert new_line > old_line, f"improved matcher should choose a later line (old={old_line}, new={new_line})"
    total_lines = test_code.count('\n') + 1
    assert new_line >= total_lines - \
        2, f"expected class-end match near end (>= {total_lines-2}), got {new_line}"


def test_apply_edits_with_improved_matching():
    """Test that _apply_edits_locally uses improved matching."""

    original_code = '''using UnityEngine;

public class TestClass : MonoBehaviour
{
    public string message = "Hello World";
    
    void Start()
    {
        Debug.Log(message);
    }
}'''

    # Test anchor_insert with the problematic pattern
    edits = [{
        "op": "anchor_insert",
        "anchor": r"\s*}\s*$",  # This should now find the class end
        "position": "before",
        "text": "\n    public void NewMethod() { Debug.Log(\"Added at class end\"); }\n"
    }]

    result = manage_script_edits_module._apply_edits_locally(
        original_code, edits)
    lines = result.split('\n')
    try:
        idx = next(i for i, line in enumerate(lines) if "NewMethod" in line)
    except StopIteration:
        assert False, "NewMethod not found in result"
    total_lines = len(lines)
    assert idx >= total_lines - \
        5, f"method inserted too early (idx={idx}, total_lines={total_lines})"


if __name__ == "__main__":
    print("Testing improved anchor matching...")
    print("="*60)

    success1 = test_improved_anchor_matching()

    print("\n" + "="*60)
    print("Comparing old vs new behavior...")
    success2 = test_old_vs_new_matching()

    print("\n" + "="*60)
    print("Testing _apply_edits_locally with improved matching...")
    success3 = test_apply_edits_with_improved_matching()

    print("\n" + "="*60)
    if success1 and success2 and success3:
        print("🎉 ALL TESTS PASSED! Improved anchor matching is working!")
    else:
        print("💥 Some tests failed. Need more work on anchor matching.")

```

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

```csharp
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using MCPForUnity.Editor.Helpers;
using UnityEditor;

namespace MCPForUnity.Editor.Services
{
    public class ToolSyncService : IToolSyncService
    {
        private readonly IPythonToolRegistryService _registryService;

        public ToolSyncService(IPythonToolRegistryService registryService = null)
        {
            _registryService = registryService ?? MCPServiceLocator.PythonToolRegistry;
        }

        public ToolSyncResult SyncProjectTools(string destToolsDir)
        {
            var result = new ToolSyncResult();

            try
            {
                Directory.CreateDirectory(destToolsDir);

                // Get all PythonToolsAsset instances in the project
                var registries = _registryService.GetAllRegistries().ToList();

                if (!registries.Any())
                {
                    McpLog.Info("No PythonToolsAsset found. Create one via Assets > Create > MCP For Unity > Python Tools");
                    return result;
                }

                var syncedFiles = new HashSet<string>();

                // Batch all asset modifications together to minimize reimports
                AssetDatabase.StartAssetEditing();
                try
                {
                    foreach (var registry in registries)
                    {
                        foreach (var file in registry.GetValidFiles())
                        {
                            try
                            {
                                // Check if needs syncing (hash-based or always)
                                if (_registryService.NeedsSync(registry, file))
                                {
                                    string destPath = Path.Combine(destToolsDir, file.name + ".py");

                                    // Write the Python file content
                                    File.WriteAllText(destPath, file.text);

                                    // Record sync
                                    _registryService.RecordSync(registry, file);

                                    result.CopiedCount++;
                                    syncedFiles.Add(destPath);
                                    McpLog.Info($"Synced Python tool: {file.name}.py");
                                }
                                else
                                {
                                    string destPath = Path.Combine(destToolsDir, file.name + ".py");
                                    syncedFiles.Add(destPath);
                                    result.SkippedCount++;
                                }
                            }
                            catch (Exception ex)
                            {
                                result.ErrorCount++;
                                result.Messages.Add($"Failed to sync {file.name}: {ex.Message}");
                            }
                        }

                        // Cleanup stale states in registry
                        registry.CleanupStaleStates();
                        EditorUtility.SetDirty(registry);
                    }

                    // Cleanup stale Python files in destination
                    CleanupStaleFiles(destToolsDir, syncedFiles);
                }
                finally
                {
                    // End batch editing - this triggers a single asset refresh
                    AssetDatabase.StopAssetEditing();
                }

                // Save all modified registries
                AssetDatabase.SaveAssets();
            }
            catch (Exception ex)
            {
                result.ErrorCount++;
                result.Messages.Add($"Sync failed: {ex.Message}");
            }

            return result;
        }

        private void CleanupStaleFiles(string destToolsDir, HashSet<string> currentFiles)
        {
            try
            {
                if (!Directory.Exists(destToolsDir)) return;

                // Find all .py files in destination that aren't in our current set
                var existingFiles = Directory.GetFiles(destToolsDir, "*.py");

                foreach (var file in existingFiles)
                {
                    if (!currentFiles.Contains(file))
                    {
                        try
                        {
                            File.Delete(file);
                            McpLog.Info($"Cleaned up stale tool: {Path.GetFileName(file)}");
                        }
                        catch (Exception ex)
                        {
                            McpLog.Warn($"Failed to cleanup {file}: {ex.Message}");
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                McpLog.Warn($"Failed to cleanup stale files: {ex.Message}");
            }
        }
    }
}

```

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

```csharp
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Models;

namespace MCPForUnity.Editor.Helpers
{
    public static class ConfigJsonBuilder
    {
        public static string BuildManualConfigJson(string uvPath, string pythonDir, McpClient client)
        {
            var root = new JObject();
            bool isVSCode = client?.mcpType == McpTypes.VSCode;
            JObject container;
            if (isVSCode)
            {
                container = EnsureObject(root, "servers");
            }
            else
            {
                container = EnsureObject(root, "mcpServers");
            }

            var unity = new JObject();
            PopulateUnityNode(unity, uvPath, pythonDir, client, isVSCode);

            container["unityMCP"] = unity;

            return root.ToString(Formatting.Indented);
        }

        public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPath, string serverSrc, McpClient client)
        {
            if (root == null) root = new JObject();
            bool isVSCode = client?.mcpType == McpTypes.VSCode;
            JObject container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers");
            JObject unity = container["unityMCP"] as JObject ?? new JObject();
            PopulateUnityNode(unity, uvPath, serverSrc, client, isVSCode);

            container["unityMCP"] = unity;
            return root;
        }

        /// <summary>
        /// Centralized builder that applies all caveats consistently.
        /// - Sets command/args with provided directory
        /// - Ensures env exists
        /// - Adds type:"stdio" for VSCode
        /// - Adds disabled:false for Windsurf/Kiro only when missing
        /// </summary>
        private static void PopulateUnityNode(JObject unity, string uvPath, string directory, McpClient client, bool isVSCode)
        {
            unity["command"] = uvPath;

            // For Cursor (non-VSCode) on macOS, prefer a no-spaces symlink path to avoid arg parsing issues in some runners
            string effectiveDir = directory;
#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
            bool isCursor = !isVSCode && (client == null || client.mcpType != McpTypes.VSCode);
            if (isCursor && !string.IsNullOrEmpty(directory))
            {
                // Replace canonical path segment with the symlink path if present
                const string canonical = "/Library/Application Support/";
                const string symlinkSeg = "/Library/AppSupport/";
                try
                {
                    // Normalize to full path style
                    if (directory.Contains(canonical))
                    {
                        var candidate = directory.Replace(canonical, symlinkSeg).Replace('\\', '/');
                        if (System.IO.Directory.Exists(candidate))
                        {
                            effectiveDir = candidate;
                        }
                    }
                    else
                    {
                        // If installer returned XDG-style on macOS, map to canonical symlink
                        string norm = directory.Replace('\\', '/');
                        int idx = norm.IndexOf("/.local/share/UnityMCP/", System.StringComparison.Ordinal);
                        if (idx >= 0)
                        {
                            string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal) ?? string.Empty;
                            string suffix = norm.Substring(idx + "/.local/share/".Length); // UnityMCP/...
                            string candidate = System.IO.Path.Combine(home, "Library", "AppSupport", suffix).Replace('\\', '/');
                            if (System.IO.Directory.Exists(candidate))
                            {
                                effectiveDir = candidate;
                            }
                        }
                    }
                }
                catch { /* fallback to original directory on any error */ }
            }
#endif

            unity["args"] = JArray.FromObject(new[] { "run", "--directory", effectiveDir, "server.py" });

            if (isVSCode)
            {
                unity["type"] = "stdio";
            }
            else
            {
                // Remove type if it somehow exists from previous clients
                if (unity["type"] != null) unity.Remove("type");
            }

            if (client != null && (client.mcpType == McpTypes.Windsurf || client.mcpType == McpTypes.Kiro))
            {
                if (unity["env"] == null)
                {
                    unity["env"] = new JObject();
                }

                if (unity["disabled"] == null)
                {
                    unity["disabled"] = false;
                }
            }
        }

        private static JObject EnsureObject(JObject parent, string name)
        {
            if (parent[name] is JObject o) return o;
            var created = new JObject();
            parent[name] = created;
            return created;
        }
    }
}

```

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

```csharp
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Models;

namespace MCPForUnity.Editor.Helpers
{
    public static class ConfigJsonBuilder
    {
        public static string BuildManualConfigJson(string uvPath, string pythonDir, McpClient client)
        {
            var root = new JObject();
            bool isVSCode = client?.mcpType == McpTypes.VSCode;
            JObject container;
            if (isVSCode)
            {
                container = EnsureObject(root, "servers");
            }
            else
            {
                container = EnsureObject(root, "mcpServers");
            }

            var unity = new JObject();
            PopulateUnityNode(unity, uvPath, pythonDir, client, isVSCode);

            container["unityMCP"] = unity;

            return root.ToString(Formatting.Indented);
        }

        public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPath, string serverSrc, McpClient client)
        {
            if (root == null) root = new JObject();
            bool isVSCode = client?.mcpType == McpTypes.VSCode;
            JObject container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers");
            JObject unity = container["unityMCP"] as JObject ?? new JObject();
            PopulateUnityNode(unity, uvPath, serverSrc, client, isVSCode);

            container["unityMCP"] = unity;
            return root;
        }

        /// <summary>
        /// Centralized builder that applies all caveats consistently.
        /// - Sets command/args with provided directory
        /// - Ensures env exists
        /// - Adds type:"stdio" for VSCode
        /// - Adds disabled:false for Windsurf/Kiro only when missing
        /// </summary>
        private static void PopulateUnityNode(JObject unity, string uvPath, string directory, McpClient client, bool isVSCode)
        {
            unity["command"] = uvPath;

            // For Cursor (non-VSCode) on macOS, prefer a no-spaces symlink path to avoid arg parsing issues in some runners
            string effectiveDir = directory;
#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
            bool isCursor = !isVSCode && (client == null || client.mcpType != McpTypes.VSCode);
            if (isCursor && !string.IsNullOrEmpty(directory))
            {
                // Replace canonical path segment with the symlink path if present
                const string canonical = "/Library/Application Support/";
                const string symlinkSeg = "/Library/AppSupport/";
                try
                {
                    // Normalize to full path style
                    if (directory.Contains(canonical))
                    {
                        var candidate = directory.Replace(canonical, symlinkSeg).Replace('\\', '/');
                        if (System.IO.Directory.Exists(candidate))
                        {
                            effectiveDir = candidate;
                        }
                    }
                    else
                    {
                        // If installer returned XDG-style on macOS, map to canonical symlink
                        string norm = directory.Replace('\\', '/');
                        int idx = norm.IndexOf("/.local/share/UnityMCP/", System.StringComparison.Ordinal);
                        if (idx >= 0)
                        {
                            string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal) ?? string.Empty;
                            string suffix = norm.Substring(idx + "/.local/share/".Length); // UnityMCP/...
                            string candidate = System.IO.Path.Combine(home, "Library", "AppSupport", suffix).Replace('\\', '/');
                            if (System.IO.Directory.Exists(candidate))
                            {
                                effectiveDir = candidate;
                            }
                        }
                    }
                }
                catch { /* fallback to original directory on any error */ }
            }
#endif

            unity["args"] = JArray.FromObject(new[] { "run", "--directory", effectiveDir, "server.py" });

            if (isVSCode)
            {
                unity["type"] = "stdio";
            }
            else
            {
                // Remove type if it somehow exists from previous clients
                if (unity["type"] != null) unity.Remove("type");
            }

            if (client != null && (client.mcpType == McpTypes.Windsurf || client.mcpType == McpTypes.Kiro))
            {
                if (unity["env"] == null)
                {
                    unity["env"] = new JObject();
                }

                if (unity["disabled"] == null)
                {
                    unity["disabled"] = false;
                }
            }
        }

        private static JObject EnsureObject(JObject parent, string name)
        {
            if (parent[name] is JObject o) return o;
            var created = new JObject();
            parent[name] = created;
            return created;
        }
    }
}

```

--------------------------------------------------------------------------------
/MCPForUnity/Editor/Dependencies/DependencyManager.cs:
--------------------------------------------------------------------------------

```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Dependencies.PlatformDetectors;
using MCPForUnity.Editor.Helpers;
using UnityEditor;
using UnityEngine;

namespace MCPForUnity.Editor.Dependencies
{
    /// <summary>
    /// Main orchestrator for dependency validation and management
    /// </summary>
    public static class DependencyManager
    {
        private static readonly List<IPlatformDetector> _detectors = new List<IPlatformDetector>
        {
            new WindowsPlatformDetector(),
            new MacOSPlatformDetector(),
            new LinuxPlatformDetector()
        };

        private static IPlatformDetector _currentDetector;

        /// <summary>
        /// Get the platform detector for the current operating system
        /// </summary>
        public static IPlatformDetector GetCurrentPlatformDetector()
        {
            if (_currentDetector == null)
            {
                _currentDetector = _detectors.FirstOrDefault(d => d.CanDetect);
                if (_currentDetector == null)
                {
                    throw new PlatformNotSupportedException($"No detector available for current platform: {RuntimeInformation.OSDescription}");
                }
            }
            return _currentDetector;
        }

        /// <summary>
        /// Perform a comprehensive dependency check
        /// </summary>
        public static DependencyCheckResult CheckAllDependencies()
        {
            var result = new DependencyCheckResult();

            try
            {
                var detector = GetCurrentPlatformDetector();
                McpLog.Info($"Checking dependencies on {detector.PlatformName}...", always: false);

                // Check Python
                var pythonStatus = detector.DetectPython();
                result.Dependencies.Add(pythonStatus);

                // Check UV
                var uvStatus = detector.DetectUV();
                result.Dependencies.Add(uvStatus);

                // Check MCP Server
                var serverStatus = detector.DetectMCPServer();
                result.Dependencies.Add(serverStatus);

                // Generate summary and recommendations
                result.GenerateSummary();
                GenerateRecommendations(result, detector);

                McpLog.Info($"Dependency check completed. System ready: {result.IsSystemReady}", always: false);
            }
            catch (Exception ex)
            {
                McpLog.Error($"Error during dependency check: {ex.Message}");
                result.Summary = $"Dependency check failed: {ex.Message}";
                result.IsSystemReady = false;
            }

            return result;
        }

        /// <summary>
        /// Get installation recommendations for the current platform
        /// </summary>
        public static string GetInstallationRecommendations()
        {
            try
            {
                var detector = GetCurrentPlatformDetector();
                return detector.GetInstallationRecommendations();
            }
            catch (Exception ex)
            {
                return $"Error getting installation recommendations: {ex.Message}";
            }
        }

        /// <summary>
        /// Get platform-specific installation URLs
        /// </summary>
        public static (string pythonUrl, string uvUrl) GetInstallationUrls()
        {
            try
            {
                var detector = GetCurrentPlatformDetector();
                return (detector.GetPythonInstallUrl(), detector.GetUVInstallUrl());
            }
            catch
            {
                return ("https://python.org/downloads/", "https://docs.astral.sh/uv/getting-started/installation/");
            }
        }

        private static void GenerateRecommendations(DependencyCheckResult result, IPlatformDetector detector)
        {
            var missing = result.GetMissingDependencies();

            if (missing.Count == 0)
            {
                result.RecommendedActions.Add("All dependencies are available. You can start using MCP for Unity.");
                return;
            }

            foreach (var dep in missing)
            {
                if (dep.Name == "Python")
                {
                    result.RecommendedActions.Add($"Install Python 3.10+ from: {detector.GetPythonInstallUrl()}");
                }
                else if (dep.Name == "UV Package Manager")
                {
                    result.RecommendedActions.Add($"Install UV package manager from: {detector.GetUVInstallUrl()}");
                }
                else if (dep.Name == "MCP Server")
                {
                    result.RecommendedActions.Add("MCP Server will be installed automatically when needed.");
                }
            }

            if (result.GetMissingRequired().Count > 0)
            {
                result.RecommendedActions.Add("Use the Setup Wizard (Window > MCP for Unity > Setup Wizard) for guided installation.");
            }
        }
    }
}

```

--------------------------------------------------------------------------------
/UnityMcpBridge/Editor/Dependencies/DependencyManager.cs:
--------------------------------------------------------------------------------

```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Dependencies.PlatformDetectors;
using MCPForUnity.Editor.Helpers;
using UnityEditor;
using UnityEngine;

namespace MCPForUnity.Editor.Dependencies
{
    /// <summary>
    /// Main orchestrator for dependency validation and management
    /// </summary>
    public static class DependencyManager
    {
        private static readonly List<IPlatformDetector> _detectors = new List<IPlatformDetector>
        {
            new WindowsPlatformDetector(),
            new MacOSPlatformDetector(),
            new LinuxPlatformDetector()
        };

        private static IPlatformDetector _currentDetector;

        /// <summary>
        /// Get the platform detector for the current operating system
        /// </summary>
        public static IPlatformDetector GetCurrentPlatformDetector()
        {
            if (_currentDetector == null)
            {
                _currentDetector = _detectors.FirstOrDefault(d => d.CanDetect);
                if (_currentDetector == null)
                {
                    throw new PlatformNotSupportedException($"No detector available for current platform: {RuntimeInformation.OSDescription}");
                }
            }
            return _currentDetector;
        }

        /// <summary>
        /// Perform a comprehensive dependency check
        /// </summary>
        public static DependencyCheckResult CheckAllDependencies()
        {
            var result = new DependencyCheckResult();

            try
            {
                var detector = GetCurrentPlatformDetector();
                McpLog.Info($"Checking dependencies on {detector.PlatformName}...", always: false);

                // Check Python
                var pythonStatus = detector.DetectPython();
                result.Dependencies.Add(pythonStatus);

                // Check UV
                var uvStatus = detector.DetectUV();
                result.Dependencies.Add(uvStatus);

                // Check MCP Server
                var serverStatus = detector.DetectMCPServer();
                result.Dependencies.Add(serverStatus);

                // Generate summary and recommendations
                result.GenerateSummary();
                GenerateRecommendations(result, detector);

                McpLog.Info($"Dependency check completed. System ready: {result.IsSystemReady}", always: false);
            }
            catch (Exception ex)
            {
                McpLog.Error($"Error during dependency check: {ex.Message}");
                result.Summary = $"Dependency check failed: {ex.Message}";
                result.IsSystemReady = false;
            }

            return result;
        }

        /// <summary>
        /// Get installation recommendations for the current platform
        /// </summary>
        public static string GetInstallationRecommendations()
        {
            try
            {
                var detector = GetCurrentPlatformDetector();
                return detector.GetInstallationRecommendations();
            }
            catch (Exception ex)
            {
                return $"Error getting installation recommendations: {ex.Message}";
            }
        }

        /// <summary>
        /// Get platform-specific installation URLs
        /// </summary>
        public static (string pythonUrl, string uvUrl) GetInstallationUrls()
        {
            try
            {
                var detector = GetCurrentPlatformDetector();
                return (detector.GetPythonInstallUrl(), detector.GetUVInstallUrl());
            }
            catch
            {
                return ("https://python.org/downloads/", "https://docs.astral.sh/uv/getting-started/installation/");
            }
        }

        private static void GenerateRecommendations(DependencyCheckResult result, IPlatformDetector detector)
        {
            var missing = result.GetMissingDependencies();

            if (missing.Count == 0)
            {
                result.RecommendedActions.Add("All dependencies are available. You can start using MCP for Unity.");
                return;
            }

            foreach (var dep in missing)
            {
                if (dep.Name == "Python")
                {
                    result.RecommendedActions.Add($"Install Python 3.10+ from: {detector.GetPythonInstallUrl()}");
                }
                else if (dep.Name == "UV Package Manager")
                {
                    result.RecommendedActions.Add($"Install UV package manager from: {detector.GetUVInstallUrl()}");
                }
                else if (dep.Name == "MCP Server")
                {
                    result.RecommendedActions.Add("MCP Server will be installed automatically when needed.");
                }
            }

            if (result.GetMissingRequired().Count > 0)
            {
                result.RecommendedActions.Add("Use the Setup Wizard (Window > MCP for Unity > Setup Wizard) for guided installation.");
            }
        }
    }
}

```

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

```csharp
using System;
using MCPForUnity.Editor.Dependencies;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Windows;
using UnityEditor;
using UnityEngine;

namespace MCPForUnity.Editor.Setup
{
    /// <summary>
    /// Handles automatic triggering of the setup wizard
    /// </summary>
    [InitializeOnLoad]
    public static class SetupWizard
    {
        private const string SETUP_COMPLETED_KEY = "MCPForUnity.SetupCompleted";
        private const string SETUP_DISMISSED_KEY = "MCPForUnity.SetupDismissed";
        private static bool _hasCheckedThisSession = false;

        static SetupWizard()
        {
            // Skip in batch mode
            if (Application.isBatchMode)
                return;

            // Show setup wizard on package import
            EditorApplication.delayCall += CheckSetupNeeded;
        }

        /// <summary>
        /// Check if setup wizard should be shown
        /// </summary>
        private static void CheckSetupNeeded()
        {
            if (_hasCheckedThisSession)
                return;

            _hasCheckedThisSession = true;

            try
            {
                // Check if setup was already completed or dismissed in previous sessions
                bool setupCompleted = EditorPrefs.GetBool(SETUP_COMPLETED_KEY, false);
                bool setupDismissed = EditorPrefs.GetBool(SETUP_DISMISSED_KEY, false);

                // Only show setup wizard if it hasn't been completed or dismissed before
                if (!(setupCompleted || setupDismissed))
                {
                    McpLog.Info("Package imported - showing setup wizard", always: false);

                    var dependencyResult = DependencyManager.CheckAllDependencies();
                    EditorApplication.delayCall += () => ShowSetupWizard(dependencyResult);
                }
                else
                {
                    McpLog.Info("Setup wizard skipped - previously completed or dismissed", always: false);
                }
            }
            catch (Exception ex)
            {
                McpLog.Error($"Error checking setup status: {ex.Message}");
            }
        }

        /// <summary>
        /// Show the setup wizard window
        /// </summary>
        public static void ShowSetupWizard(DependencyCheckResult dependencyResult = null)
        {
            try
            {
                dependencyResult ??= DependencyManager.CheckAllDependencies();
                SetupWizardWindow.ShowWindow(dependencyResult);
            }
            catch (Exception ex)
            {
                McpLog.Error($"Error showing setup wizard: {ex.Message}");
            }
        }

        /// <summary>
        /// Mark setup as completed
        /// </summary>
        public static void MarkSetupCompleted()
        {
            EditorPrefs.SetBool(SETUP_COMPLETED_KEY, true);
            McpLog.Info("Setup marked as completed");
        }

        /// <summary>
        /// Mark setup as dismissed
        /// </summary>
        public static void MarkSetupDismissed()
        {
            EditorPrefs.SetBool(SETUP_DISMISSED_KEY, true);
            McpLog.Info("Setup marked as dismissed");
        }

        /// <summary>
        /// Force show setup wizard (for manual invocation)
        /// </summary>
        [MenuItem("Window/MCP For Unity/Setup Wizard", priority = 1)]
        public static void ShowSetupWizardManual()
        {
            ShowSetupWizard();
        }

        /// <summary>
        /// Check dependencies and show status
        /// </summary>
        [MenuItem("Window/MCP For Unity/Check Dependencies", priority = 3)]
        public static void CheckDependencies()
        {
            var result = DependencyManager.CheckAllDependencies();

            if (!result.IsSystemReady)
            {
                bool showWizard = EditorUtility.DisplayDialog(
                    "MCP for Unity - Dependencies",
                    $"System Status: {result.Summary}\n\nWould you like to open the Setup Wizard?",
                    "Open Setup Wizard",
                    "Close"
                );

                if (showWizard)
                {
                    ShowSetupWizard(result);
                }
            }
            else
            {
                EditorUtility.DisplayDialog(
                    "MCP for Unity - Dependencies",
                    "✓ All dependencies are available and ready!\n\nMCP for Unity is ready to use.",
                    "OK"
                );
            }
        }

        /// <summary>
        /// Open MCP Client Configuration window
        /// </summary>
        [MenuItem("Window/MCP For Unity/Open MCP Window %#m", priority = 4)]
        public static void OpenClientConfiguration()
        {
            Windows.MCPForUnityEditorWindowNew.ShowWindow();
        }

        /// <summary>
        /// Open legacy MCP Client Configuration window
        /// </summary>
        [MenuItem("Window/MCP For Unity/Open Legacy MCP Window", priority = 5)]
        public static void OpenLegacyClientConfiguration()
        {
            Windows.MCPForUnityEditorWindow.ShowWindow();
        }
    }
}

```

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

```csharp
using System;
using System.IO;
using UnityEngine;
using UnityEditor;
using MCPForUnity.Editor.Helpers;

namespace MCPForUnity.Editor.Helpers
{
    /// <summary>
    /// Shared helper for resolving MCP server directory paths with support for
    /// development mode, embedded servers, and installed packages
    /// </summary>
    public static class McpPathResolver
    {
        private const string USE_EMBEDDED_SERVER_KEY = "MCPForUnity.UseEmbeddedServer";

        /// <summary>
        /// Resolves the MCP server directory path with comprehensive logic
        /// including development mode support and fallback mechanisms
        /// </summary>
        public static string FindPackagePythonDirectory(bool debugLogsEnabled = false)
        {
            string pythonDir = McpConfigFileHelper.ResolveServerSource();

            try
            {
                // Only check dev paths if we're using a file-based package (development mode)
                bool isDevelopmentMode = IsDevelopmentMode();
                if (isDevelopmentMode)
                {
                    string currentPackagePath = Path.GetDirectoryName(Application.dataPath);
                    string[] devPaths = {
                        Path.Combine(currentPackagePath, "unity-mcp", "UnityMcpServer", "src"),
                        Path.Combine(Path.GetDirectoryName(currentPackagePath), "unity-mcp", "UnityMcpServer", "src"),
                    };

                    foreach (string devPath in devPaths)
                    {
                        if (Directory.Exists(devPath) && File.Exists(Path.Combine(devPath, "server.py")))
                        {
                            if (debugLogsEnabled)
                            {
                                Debug.Log($"Currently in development mode. Package: {devPath}");
                            }
                            return devPath;
                        }
                    }
                }

                // Resolve via shared helper (handles local registry and older fallback) only if dev override on
                if (EditorPrefs.GetBool(USE_EMBEDDED_SERVER_KEY, false))
                {
                    if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded))
                    {
                        return embedded;
                    }
                }

                // Log only if the resolved path does not actually contain server.py
                if (debugLogsEnabled)
                {
                    bool hasServer = false;
                    try { hasServer = File.Exists(Path.Combine(pythonDir, "server.py")); } catch { }
                    if (!hasServer)
                    {
                        Debug.LogWarning("Could not find Python directory with server.py; falling back to installed path");
                    }
                }
            }
            catch (Exception e)
            {
                Debug.LogError($"Error finding package path: {e.Message}");
            }

            return pythonDir;
        }

        /// <summary>
        /// Checks if the current Unity project is in development mode
        /// (i.e., the package is referenced as a local file path in manifest.json)
        /// </summary>
        private static bool IsDevelopmentMode()
        {
            try
            {
                // Only treat as development if manifest explicitly references a local file path for the package
                string manifestPath = Path.Combine(Application.dataPath, "..", "Packages", "manifest.json");
                if (!File.Exists(manifestPath)) return false;

                string manifestContent = File.ReadAllText(manifestPath);
                // Look specifically for our package dependency set to a file: URL
                // This avoids auto-enabling dev mode just because a repo exists elsewhere on disk
                if (manifestContent.IndexOf("\"com.coplaydev.unity-mcp\"", StringComparison.OrdinalIgnoreCase) >= 0)
                {
                    int idx = manifestContent.IndexOf("com.coplaydev.unity-mcp", StringComparison.OrdinalIgnoreCase);
                    // Crude but effective: check for "file:" in the same line/value
                    if (manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase) >= 0
                        && manifestContent.IndexOf("\n", idx, StringComparison.OrdinalIgnoreCase) > manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase))
                    {
                        return true;
                    }
                }
                return false;
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        /// Gets the appropriate PATH prepend for the current platform when running external processes
        /// </summary>
        public static string GetPathPrepend()
        {
            if (Application.platform == RuntimePlatform.OSXEditor)
                return "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
            else if (Application.platform == RuntimePlatform.LinuxEditor)
                return "/usr/local/bin:/usr/bin:/bin";
            return null;
        }
    }
}

```

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

```csharp
using System;
using System.IO;
using UnityEngine;
using UnityEditor;
using MCPForUnity.Editor.Helpers;

namespace MCPForUnity.Editor.Helpers
{
    /// <summary>
    /// Shared helper for resolving Python server directory paths with support for
    /// development mode, embedded servers, and installed packages
    /// </summary>
    public static class McpPathResolver
    {
        private const string USE_EMBEDDED_SERVER_KEY = "MCPForUnity.UseEmbeddedServer";

        /// <summary>
        /// Resolves the Python server directory path with comprehensive logic
        /// including development mode support and fallback mechanisms
        /// </summary>
        public static string FindPackagePythonDirectory(bool debugLogsEnabled = false)
        {
            string pythonDir = McpConfigFileHelper.ResolveServerSource();

            try
            {
                // Only check dev paths if we're using a file-based package (development mode)
                bool isDevelopmentMode = IsDevelopmentMode();
                if (isDevelopmentMode)
                {
                    string currentPackagePath = Path.GetDirectoryName(Application.dataPath);
                    string[] devPaths = {
                        Path.Combine(currentPackagePath, "unity-mcp", "UnityMcpServer", "src"),
                        Path.Combine(Path.GetDirectoryName(currentPackagePath), "unity-mcp", "UnityMcpServer", "src"),
                    };

                    foreach (string devPath in devPaths)
                    {
                        if (Directory.Exists(devPath) && File.Exists(Path.Combine(devPath, "server.py")))
                        {
                            if (debugLogsEnabled)
                            {
                                Debug.Log($"Currently in development mode. Package: {devPath}");
                            }
                            return devPath;
                        }
                    }
                }

                // Resolve via shared helper (handles local registry and older fallback) only if dev override on
                if (EditorPrefs.GetBool(USE_EMBEDDED_SERVER_KEY, false))
                {
                    if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded))
                    {
                        return embedded;
                    }
                }

                // Log only if the resolved path does not actually contain server.py
                if (debugLogsEnabled)
                {
                    bool hasServer = false;
                    try { hasServer = File.Exists(Path.Combine(pythonDir, "server.py")); } catch { }
                    if (!hasServer)
                    {
                        Debug.LogWarning("Could not find Python directory with server.py; falling back to installed path");
                    }
                }
            }
            catch (Exception e)
            {
                Debug.LogError($"Error finding package path: {e.Message}");
            }

            return pythonDir;
        }

        /// <summary>
        /// Checks if the current Unity project is in development mode
        /// (i.e., the package is referenced as a local file path in manifest.json)
        /// </summary>
        private static bool IsDevelopmentMode()
        {
            try
            {
                // Only treat as development if manifest explicitly references a local file path for the package
                string manifestPath = Path.Combine(Application.dataPath, "..", "Packages", "manifest.json");
                if (!File.Exists(manifestPath)) return false;

                string manifestContent = File.ReadAllText(manifestPath);
                // Look specifically for our package dependency set to a file: URL
                // This avoids auto-enabling dev mode just because a repo exists elsewhere on disk
                if (manifestContent.IndexOf("\"com.coplaydev.unity-mcp\"", StringComparison.OrdinalIgnoreCase) >= 0)
                {
                    int idx = manifestContent.IndexOf("com.coplaydev.unity-mcp", StringComparison.OrdinalIgnoreCase);
                    // Crude but effective: check for "file:" in the same line/value
                    if (manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase) >= 0
                        && manifestContent.IndexOf("\n", idx, StringComparison.OrdinalIgnoreCase) > manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase))
                    {
                        return true;
                    }
                }
                return false;
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        /// Gets the appropriate PATH prepend for the current platform when running external processes
        /// </summary>
        public static string GetPathPrepend()
        {
            if (Application.platform == RuntimePlatform.OSXEditor)
                return "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
            else if (Application.platform == RuntimePlatform.LinuxEditor)
                return "/usr/local/bin:/usr/bin:/bin";
            return null;
        }
    }
}

```

--------------------------------------------------------------------------------
/docs/TELEMETRY.md:
--------------------------------------------------------------------------------

```markdown
# MCP for Unity Telemetry

MCP for Unity includes privacy-focused, anonymous telemetry to help us improve the product. This document explains what data is collected, how to opt out, and our privacy practices.

## 🔒 Privacy First

- **Anonymous**: We use randomly generated UUIDs - no personal information
- **Non-blocking**: Telemetry never interferes with your Unity workflow  
- **Easy opt-out**: Simple environment variable or Unity Editor setting
- **Transparent**: All collected data types are documented here

## 📊 What We Collect

### Usage Analytics
- **Tool Usage**: Which MCP tools you use (manage_script, manage_scene, etc.)
- **Performance**: Execution times and success/failure rates
- **System Info**: Unity version, platform (Windows/Mac/Linux), MCP version
- **Milestones**: First-time usage events (first script creation, first tool use, etc.)

### Technical Diagnostics  
- **Connection Events**: Bridge startup/connection success/failures
- **Error Reports**: Anonymized error messages (truncated to 200 chars)
- **Server Health**: Startup time, connection latency

### What We **DON'T** Collect
- ❌ Your code or script contents
- ❌ Project names, file names, or paths
- ❌ Personal information or identifiers
- ❌ Sensitive project data
- ❌ IP addresses (beyond what's needed for HTTP requests)

## 🚫 How to Opt Out

### Method 1: Environment Variable (Recommended)
Set any of these environment variables to `true`:

```bash
# Disable all telemetry
export DISABLE_TELEMETRY=true

# MCP for Unity specific
export UNITY_MCP_DISABLE_TELEMETRY=true

# MCP protocol wide  
export MCP_DISABLE_TELEMETRY=true
```

### Method 2: Unity Editor (Coming Soon)
In Unity Editor: `Window > MCP for Unity > Settings > Disable Telemetry`

### Method 3: Manual Config
Add to your MCP client config:
```json
{
  "env": {
    "DISABLE_TELEMETRY": "true"
  }
}
```

## 🔧 Technical Implementation

### Architecture
- **Python Server**: Core telemetry collection and transmission
- **Unity Bridge**: Local event collection from Unity Editor
- **Anonymous UUIDs**: Generated per-installation for aggregate analytics
- **Thread-safe**: Non-blocking background transmission
- **Fail-safe**: Errors never interrupt your workflow

### Data Storage
Telemetry data is stored locally in:
- **Windows**: `%APPDATA%\UnityMCP\`
- **macOS**: `~/Library/Application Support/UnityMCP/`  
- **Linux**: `~/.local/share/UnityMCP/`

Files created:
- `customer_uuid.txt`: Anonymous identifier
- `milestones.json`: One-time events tracker

### Data Transmission
- **Endpoint**: `https://api-prod.coplay.dev/telemetry/events`
- **Method**: HTTPS POST with JSON payload
- **Retry**: Background thread with graceful failure
- **Timeout**: 10 second timeout, no retries on failure

## 📈 How We Use This Data

### Product Improvement
- **Feature Usage**: Understand which tools are most/least used
- **Performance**: Identify slow operations to optimize
- **Reliability**: Track error rates and connection issues
- **Compatibility**: Ensure Unity version compatibility

### Development Priorities
- **Roadmap**: Focus development on most-used features
- **Bug Fixes**: Prioritize fixes based on error frequency
- **Platform Support**: Allocate resources based on platform usage
- **Documentation**: Improve docs for commonly problematic areas

### What We Don't Do
- ❌ Sell data to third parties
- ❌ Use data for advertising/marketing
- ❌ Track individual developers
- ❌ Store sensitive project information

## 🛠️ For Developers

### Testing Telemetry
```bash
cd MCPForUnity/UnityMcpServer~/src
python test_telemetry.py
```

### Custom Telemetry Events
```python
from telemetry import record_telemetry, RecordType

record_telemetry(RecordType.USAGE, {
    "custom_event": "my_feature_used",
    "metadata": "optional_data"
})
```

### Telemetry Status Check
```python  
from telemetry import is_telemetry_enabled

if is_telemetry_enabled():
    print("Telemetry is active")
else:
    print("Telemetry is disabled")
```

## 📋 Data Retention Policy

- **Aggregated Data**: Retained indefinitely for product insights
- **Raw Events**: Automatically purged after 90 days
- **Personal Data**: None collected, so none to purge
- **Opt-out**: Immediate - no data sent after opting out

## 🤝 Contact & Transparency

- **Questions**: [Discord Community](https://discord.gg/y4p8KfzrN4)
- **Issues**: [GitHub Issues](https://github.com/CoplayDev/unity-mcp/issues)
- **Privacy Concerns**: Create a GitHub issue with "Privacy" label
- **Source Code**: All telemetry code is open source in this repository

## 📊 Example Telemetry Event

Here's what a typical telemetry event looks like:

```json
{
  "record": "tool_execution",
  "timestamp": 1704067200,
  "customer_uuid": "550e8400-e29b-41d4-a716-446655440000", 
  "session_id": "abc123-def456-ghi789",
  "version": "3.0.2",
  "platform": "posix",
  "data": {
    "tool_name": "manage_script",
    "success": true,
    "duration_ms": 42.5
  }
}
```

Notice:
- ✅ Anonymous UUID (randomly generated)
- ✅ Tool performance metrics  
- ✅ Success/failure tracking
- ❌ No code content
- ❌ No project information
- ❌ No personal data

---

*MCP for Unity Telemetry is designed to respect your privacy while helping us build a better tool. Thank you for helping improve MCP for Unity!*

```

--------------------------------------------------------------------------------
/MCPForUnity/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs:
--------------------------------------------------------------------------------

```csharp
using System;
using System.Diagnostics;
using System.IO;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;

namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
{
    /// <summary>
    /// Base class for platform-specific dependency detection
    /// </summary>
    public abstract class PlatformDetectorBase : IPlatformDetector
    {
        public abstract string PlatformName { get; }
        public abstract bool CanDetect { get; }

        public abstract DependencyStatus DetectPython();
        public abstract string GetPythonInstallUrl();
        public abstract string GetUVInstallUrl();
        public abstract string GetInstallationRecommendations();

        public virtual DependencyStatus DetectUV()
        {
            var status = new DependencyStatus("UV Package Manager", isRequired: true)
            {
                InstallationHint = GetUVInstallUrl()
            };

            try
            {
                // Use existing UV detection from ServerInstaller
                string uvPath = ServerInstaller.FindUvPath();
                if (!string.IsNullOrEmpty(uvPath))
                {
                    if (TryValidateUV(uvPath, out string version))
                    {
                        status.IsAvailable = true;
                        status.Version = version;
                        status.Path = uvPath;
                        status.Details = $"Found UV {version} at {uvPath}";
                        return status;
                    }
                }

                status.ErrorMessage = "UV package manager not found. Please install UV.";
                status.Details = "UV is required for managing Python dependencies.";
            }
            catch (Exception ex)
            {
                status.ErrorMessage = $"Error detecting UV: {ex.Message}";
            }

            return status;
        }

        public virtual DependencyStatus DetectMCPServer()
        {
            var status = new DependencyStatus("MCP Server", isRequired: false);

            try
            {
                // Check if server is installed
                string serverPath = ServerInstaller.GetServerPath();
                string serverPy = Path.Combine(serverPath, "server.py");

                if (File.Exists(serverPy))
                {
                    status.IsAvailable = true;
                    status.Path = serverPath;

                    // Try to get version
                    string versionFile = Path.Combine(serverPath, "server_version.txt");
                    if (File.Exists(versionFile))
                    {
                        status.Version = File.ReadAllText(versionFile).Trim();
                    }

                    status.Details = $"MCP Server found at {serverPath}";
                }
                else
                {
                    // Check for embedded server
                    if (ServerPathResolver.TryFindEmbeddedServerSource(out string embeddedPath))
                    {
                        status.IsAvailable = true;
                        status.Path = embeddedPath;
                        status.Details = "MCP Server available (embedded in package)";
                    }
                    else
                    {
                        status.ErrorMessage = "MCP Server not found";
                        status.Details = "Server will be installed automatically when needed";
                    }
                }
            }
            catch (Exception ex)
            {
                status.ErrorMessage = $"Error detecting MCP Server: {ex.Message}";
            }

            return status;
        }

        protected bool TryValidateUV(string uvPath, out string version)
        {
            version = null;

            try
            {
                var psi = new ProcessStartInfo
                {
                    FileName = uvPath,
                    Arguments = "--version",
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    CreateNoWindow = true
                };

                using var process = Process.Start(psi);
                if (process == null) return false;

                string output = process.StandardOutput.ReadToEnd().Trim();
                process.WaitForExit(5000);

                if (process.ExitCode == 0 && output.StartsWith("uv "))
                {
                    version = output.Substring(3); // Remove "uv " prefix
                    return true;
                }
            }
            catch
            {
                // Ignore validation errors
            }

            return false;
        }

        protected bool TryParseVersion(string version, out int major, out int minor)
        {
            major = 0;
            minor = 0;

            try
            {
                var parts = version.Split('.');
                if (parts.Length >= 2)
                {
                    return int.TryParse(parts[0], out major) && int.TryParse(parts[1], out minor);
                }
            }
            catch
            {
                // Ignore parsing errors
            }

            return false;
        }
    }
}

```

--------------------------------------------------------------------------------
/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs:
--------------------------------------------------------------------------------

```csharp
using System;
using System.Diagnostics;
using System.IO;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;

namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
{
    /// <summary>
    /// Base class for platform-specific dependency detection
    /// </summary>
    public abstract class PlatformDetectorBase : IPlatformDetector
    {
        public abstract string PlatformName { get; }
        public abstract bool CanDetect { get; }

        public abstract DependencyStatus DetectPython();
        public abstract string GetPythonInstallUrl();
        public abstract string GetUVInstallUrl();
        public abstract string GetInstallationRecommendations();

        public virtual DependencyStatus DetectUV()
        {
            var status = new DependencyStatus("UV Package Manager", isRequired: true)
            {
                InstallationHint = GetUVInstallUrl()
            };

            try
            {
                // Use existing UV detection from ServerInstaller
                string uvPath = ServerInstaller.FindUvPath();
                if (!string.IsNullOrEmpty(uvPath))
                {
                    if (TryValidateUV(uvPath, out string version))
                    {
                        status.IsAvailable = true;
                        status.Version = version;
                        status.Path = uvPath;
                        status.Details = $"Found UV {version} at {uvPath}";
                        return status;
                    }
                }

                status.ErrorMessage = "UV package manager not found. Please install UV.";
                status.Details = "UV is required for managing Python dependencies.";
            }
            catch (Exception ex)
            {
                status.ErrorMessage = $"Error detecting UV: {ex.Message}";
            }

            return status;
        }

        public virtual DependencyStatus DetectMCPServer()
        {
            var status = new DependencyStatus("MCP Server", isRequired: false);

            try
            {
                // Check if server is installed
                string serverPath = ServerInstaller.GetServerPath();
                string serverPy = Path.Combine(serverPath, "server.py");

                if (File.Exists(serverPy))
                {
                    status.IsAvailable = true;
                    status.Path = serverPath;

                    // Try to get version
                    string versionFile = Path.Combine(serverPath, "server_version.txt");
                    if (File.Exists(versionFile))
                    {
                        status.Version = File.ReadAllText(versionFile).Trim();
                    }

                    status.Details = $"MCP Server found at {serverPath}";
                }
                else
                {
                    // Check for embedded server
                    if (ServerPathResolver.TryFindEmbeddedServerSource(out string embeddedPath))
                    {
                        status.IsAvailable = true;
                        status.Path = embeddedPath;
                        status.Details = "MCP Server available (embedded in package)";
                    }
                    else
                    {
                        status.ErrorMessage = "MCP Server not found";
                        status.Details = "Server will be installed automatically when needed";
                    }
                }
            }
            catch (Exception ex)
            {
                status.ErrorMessage = $"Error detecting MCP Server: {ex.Message}";
            }

            return status;
        }

        protected bool TryValidateUV(string uvPath, out string version)
        {
            version = null;

            try
            {
                var psi = new ProcessStartInfo
                {
                    FileName = uvPath,
                    Arguments = "--version",
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    CreateNoWindow = true
                };

                using var process = Process.Start(psi);
                if (process == null) return false;

                string output = process.StandardOutput.ReadToEnd().Trim();
                process.WaitForExit(5000);

                if (process.ExitCode == 0 && output.StartsWith("uv "))
                {
                    version = output.Substring(3); // Remove "uv " prefix
                    return true;
                }
            }
            catch
            {
                // Ignore validation errors
            }

            return false;
        }

        protected bool TryParseVersion(string version, out int major, out int minor)
        {
            major = 0;
            minor = 0;

            try
            {
                var parts = version.Split('.');
                if (parts.Length >= 2)
                {
                    return int.TryParse(parts[0], out major) && int.TryParse(parts[1], out minor);
                }
            }
            catch
            {
                // Ignore parsing errors
            }

            return false;
        }
    }
}

```

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

```csharp
using System;
using System.IO;
using UnityEditor;
using UnityEngine;

namespace MCPForUnity.Editor.Helpers
{
    public static class ServerPathResolver
    {
        /// <summary>
        /// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package
        /// or common development locations. Returns true if found and sets srcPath to the folder
        /// containing server.py.
        /// </summary>
        public static bool TryFindEmbeddedServerSource(out string srcPath)
        {
            // 1) Repo development layouts commonly used alongside this package
            try
            {
                string projectRoot = Path.GetDirectoryName(Application.dataPath);
                string[] devCandidates =
                {
                    Path.Combine(projectRoot ?? string.Empty, "unity-mcp", "UnityMcpServer", "src"),
                    Path.Combine(projectRoot ?? string.Empty, "..", "unity-mcp", "UnityMcpServer", "src"),
                };
                foreach (string candidate in devCandidates)
                {
                    string full = Path.GetFullPath(candidate);
                    if (Directory.Exists(full) && File.Exists(Path.Combine(full, "server.py")))
                    {
                        srcPath = full;
                        return true;
                    }
                }
            }
            catch { /* ignore */ }

            // 2) Resolve via local package info (no network). Fall back to Client.List on older editors.
            try
            {
#if UNITY_2021_2_OR_NEWER
                // Primary: the package that owns this assembly
                var owner = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(ServerPathResolver).Assembly);
                if (owner != null)
                {
                    if (TryResolveWithinPackage(owner, out srcPath))
                    {
                        return true;
                    }
                }

                // Secondary: scan all registered packages locally
                foreach (var p in UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages())
                {
                    if (TryResolveWithinPackage(p, out srcPath))
                    {
                        return true;
                    }
                }
#else
                // Older Unity versions: use Package Manager Client.List as a fallback
                var list = UnityEditor.PackageManager.Client.List();
                while (!list.IsCompleted) { }
                if (list.Status == UnityEditor.PackageManager.StatusCode.Success)
                {
                    foreach (var pkg in list.Result)
                    {
                        if (TryResolveWithinPackage(pkg, out srcPath))
                        {
                            return true;
                        }
                    }
                }
#endif
            }
            catch { /* ignore */ }

            // 3) Fallback to previous common install locations
            try
            {
                string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
                string[] candidates =
                {
                    Path.Combine(home, "unity-mcp", "UnityMcpServer", "src"),
                    Path.Combine(home, "Applications", "UnityMCP", "UnityMcpServer", "src"),
                };
                foreach (string candidate in candidates)
                {
                    if (Directory.Exists(candidate) && File.Exists(Path.Combine(candidate, "server.py")))
                    {
                        srcPath = candidate;
                        return true;
                    }
                }
            }
            catch { /* ignore */ }

            srcPath = null;
            return false;
        }

        private static bool TryResolveWithinPackage(UnityEditor.PackageManager.PackageInfo p, out string srcPath)
        {
            const string CurrentId = "com.coplaydev.unity-mcp";

            srcPath = null;
            if (p == null || p.name != CurrentId)
            {
                return false;
            }

            string packagePath = p.resolvedPath;

            // Preferred tilde folder (embedded but excluded from import)
            string embeddedTilde = Path.Combine(packagePath, "UnityMcpServer~", "src");
            if (Directory.Exists(embeddedTilde) && File.Exists(Path.Combine(embeddedTilde, "server.py")))
            {
                srcPath = embeddedTilde;
                return true;
            }

            // Legacy non-tilde folder
            string embedded = Path.Combine(packagePath, "UnityMcpServer", "src");
            if (Directory.Exists(embedded) && File.Exists(Path.Combine(embedded, "server.py")))
            {
                srcPath = embedded;
                return true;
            }

            // Dev-linked sibling of the package folder
            string sibling = Path.Combine(Path.GetDirectoryName(packagePath) ?? string.Empty, "UnityMcpServer", "src");
            if (Directory.Exists(sibling) && File.Exists(Path.Combine(sibling, "server.py")))
            {
                srcPath = sibling;
                return true;
            }

            return false;
        }
    }
}

```

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

```csharp
using System;
using System.IO;
using UnityEditor;
using UnityEngine;

namespace MCPForUnity.Editor.Helpers
{
    public static class ServerPathResolver
    {
        /// <summary>
        /// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package
        /// or common development locations. Returns true if found and sets srcPath to the folder
        /// containing server.py.
        /// </summary>
        public static bool TryFindEmbeddedServerSource(out string srcPath)
        {
            // 1) Repo development layouts commonly used alongside this package
            try
            {
                string projectRoot = Path.GetDirectoryName(Application.dataPath);
                string[] devCandidates =
                {
                    Path.Combine(projectRoot ?? string.Empty, "unity-mcp", "UnityMcpServer", "src"),
                    Path.Combine(projectRoot ?? string.Empty, "..", "unity-mcp", "UnityMcpServer", "src"),
                };
                foreach (string candidate in devCandidates)
                {
                    string full = Path.GetFullPath(candidate);
                    if (Directory.Exists(full) && File.Exists(Path.Combine(full, "server.py")))
                    {
                        srcPath = full;
                        return true;
                    }
                }
            }
            catch { /* ignore */ }

            // 2) Resolve via local package info (no network). Fall back to Client.List on older editors.
            try
            {
#if UNITY_2021_2_OR_NEWER
                // Primary: the package that owns this assembly
                var owner = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(ServerPathResolver).Assembly);
                if (owner != null)
                {
                    if (TryResolveWithinPackage(owner, out srcPath))
                    {
                        return true;
                    }
                }

                // Secondary: scan all registered packages locally
                foreach (var p in UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages())
                {
                    if (TryResolveWithinPackage(p, out srcPath))
                    {
                        return true;
                    }
                }
#else
                // Older Unity versions: use Package Manager Client.List as a fallback
                var list = UnityEditor.PackageManager.Client.List();
                while (!list.IsCompleted) { }
                if (list.Status == UnityEditor.PackageManager.StatusCode.Success)
                {
                    foreach (var pkg in list.Result)
                    {
                        if (TryResolveWithinPackage(pkg, out srcPath))
                        {
                            return true;
                        }
                    }
                }
#endif
            }
            catch { /* ignore */ }

            // 3) Fallback to previous common install locations
            try
            {
                string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
                string[] candidates =
                {
                    Path.Combine(home, "unity-mcp", "UnityMcpServer", "src"),
                    Path.Combine(home, "Applications", "UnityMCP", "UnityMcpServer", "src"),
                };
                foreach (string candidate in candidates)
                {
                    if (Directory.Exists(candidate) && File.Exists(Path.Combine(candidate, "server.py")))
                    {
                        srcPath = candidate;
                        return true;
                    }
                }
            }
            catch { /* ignore */ }

            srcPath = null;
            return false;
        }

        private static bool TryResolveWithinPackage(UnityEditor.PackageManager.PackageInfo p, out string srcPath)
        {
            const string CurrentId = "com.coplaydev.unity-mcp";

            srcPath = null;
            if (p == null || p.name != CurrentId)
            {
                return false;
            }

            string packagePath = p.resolvedPath;

            // Preferred tilde folder (embedded but excluded from import)
            string embeddedTilde = Path.Combine(packagePath, "UnityMcpServer~", "src");
            if (Directory.Exists(embeddedTilde) && File.Exists(Path.Combine(embeddedTilde, "server.py")))
            {
                srcPath = embeddedTilde;
                return true;
            }

            // Legacy non-tilde folder
            string embedded = Path.Combine(packagePath, "UnityMcpServer", "src");
            if (Directory.Exists(embedded) && File.Exists(Path.Combine(embedded, "server.py")))
            {
                srcPath = embedded;
                return true;
            }

            // Dev-linked sibling of the package folder
            string sibling = Path.Combine(Path.GetDirectoryName(packagePath) ?? string.Empty, "UnityMcpServer", "src");
            if (Directory.Exists(sibling) && File.Exists(Path.Combine(sibling, "server.py")))
            {
                srcPath = sibling;
                return true;
            }

            return false;
        }
    }
}

```

--------------------------------------------------------------------------------
/mcp_source.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
Generic helper to switch the MCP for Unity package source in a Unity project's
Packages/manifest.json.  This is useful for switching between upstream and local repos while working on the MCP.

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

Choices:
  1) Upstream main (CoplayDev/unity-mcp)
  2) Your remote current branch (derived from `origin` and current branch)
  3) Local repo workspace (file: URL to MCPForUnity in your checkout)
"""

from __future__ import annotations

import argparse
import json
import pathlib
import subprocess
import sys
from typing import Optional

PKG_NAME = "com.coplaydev.unity-mcp"
BRIDGE_SUBPATH = "MCPForUnity"


def run_git(repo: pathlib.Path, *args: str) -> str:
    result = subprocess.run([
        "git", "-C", str(repo), *args
    ], capture_output=True, text=True)
    if result.returncode != 0:
        raise RuntimeError(result.stderr.strip()
                           or f"git {' '.join(args)} failed")
    return result.stdout.strip()


def normalize_origin_to_https(url: str) -> str:
    """Map common SSH origin forms to https for Unity's git URL scheme."""
    if url.startswith("[email protected]:"):
        owner_repo = url.split(":", 1)[1]
        if owner_repo.endswith(".git"):
            owner_repo = owner_repo[:-4]
        return f"https://github.com/{owner_repo}.git"
    # already https or file: etc.
    return url


def detect_repo_root(explicit: Optional[str]) -> pathlib.Path:
    if explicit:
        return pathlib.Path(explicit).resolve()
    # Prefer the git toplevel from the script's directory
    here = pathlib.Path(__file__).resolve().parent
    try:
        top = run_git(here, "rev-parse", "--show-toplevel")
        return pathlib.Path(top)
    except Exception:
        return here


def detect_branch(repo: pathlib.Path) -> str:
    return run_git(repo, "rev-parse", "--abbrev-ref", "HEAD")


def detect_origin(repo: pathlib.Path) -> str:
    url = run_git(repo, "remote", "get-url", "origin")
    return normalize_origin_to_https(url)


def find_manifest(explicit: Optional[str]) -> pathlib.Path:
    if explicit:
        return pathlib.Path(explicit).resolve()
    # Walk up from CWD looking for Packages/manifest.json
    cur = pathlib.Path.cwd().resolve()
    for parent in [cur, *cur.parents]:
        candidate = parent / "Packages" / "manifest.json"
        if candidate.exists():
            return candidate
    raise FileNotFoundError(
        "Could not find Packages/manifest.json from current directory. Use --manifest to specify a path.")


def read_json(path: pathlib.Path) -> dict:
    with path.open("r", encoding="utf-8") as f:
        return json.load(f)


def write_json(path: pathlib.Path, data: dict) -> None:
    with path.open("w", encoding="utf-8") as f:
        json.dump(data, f, indent=2)
        f.write("\n")


def build_options(repo_root: pathlib.Path, branch: str, origin_https: str):
    upstream = "git+https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity"
    # Ensure origin is https
    origin = origin_https
    # If origin is a local file path or non-https, try to coerce to https github if possible
    if origin.startswith("file:"):
        # Not meaningful for remote option; keep upstream
        origin_remote = upstream
    else:
        origin_remote = origin
    return [
        ("[1] Upstream main", upstream),
        ("[2] Remote current branch",
         f"{origin_remote}?path=/{BRIDGE_SUBPATH}#{branch}"),
        ("[3] Local workspace",
         f"file:{(repo_root / BRIDGE_SUBPATH).as_posix()}"),
    ]


def parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(
        description="Switch MCP for Unity package source")
    p.add_argument("--manifest", help="Path to Packages/manifest.json")
    p.add_argument(
        "--repo", help="Path to unity-mcp repo root (for local file option)")
    p.add_argument(
        "--choice", choices=["1", "2", "3"], help="Pick option non-interactively")
    return p.parse_args()


def main() -> None:
    args = parse_args()
    try:
        repo_root = detect_repo_root(args.repo)
        branch = detect_branch(repo_root)
        origin = detect_origin(repo_root)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)

    options = build_options(repo_root, branch, origin)

    try:
        manifest_path = find_manifest(args.manifest)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)

    print("Select MCP package source by number:")
    for label, _ in options:
        print(label)

    if args.choice:
        choice = args.choice
    else:
        choice = input("Enter 1-3: ").strip()

    if choice not in {"1", "2", "3"}:
        print("Invalid selection.", file=sys.stderr)
        sys.exit(1)

    idx = int(choice) - 1
    _, chosen = options[idx]

    data = read_json(manifest_path)
    deps = data.get("dependencies", {})
    if PKG_NAME not in deps:
        print(
            f"Error: '{PKG_NAME}' not found in manifest dependencies.", file=sys.stderr)
        sys.exit(1)

    print(f"\nUpdating {PKG_NAME} → {chosen}")
    deps[PKG_NAME] = chosen
    data["dependencies"] = deps
    write_json(manifest_path, data)
    print(f"Done. Wrote to: {manifest_path}")
    print("Tip: In Unity, open Package Manager and Refresh to re-resolve packages.")


if __name__ == "__main__":
    main()

```

--------------------------------------------------------------------------------
/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Data/PythonToolsAssetTests.cs:
--------------------------------------------------------------------------------

```csharp
using System;
using System.Linq;
using NUnit.Framework;
using UnityEngine;
using MCPForUnity.Editor.Data;

namespace MCPForUnityTests.Editor.Data
{
    public class PythonToolsAssetTests
    {
        private PythonToolsAsset _asset;

        [SetUp]
        public void SetUp()
        {
            _asset = ScriptableObject.CreateInstance<PythonToolsAsset>();
        }

        [TearDown]
        public void TearDown()
        {
            if (_asset != null)
            {
                UnityEngine.Object.DestroyImmediate(_asset, true);
            }
        }

        [Test]
        public void GetValidFiles_ReturnsEmptyList_WhenNoFilesAdded()
        {
            var validFiles = _asset.GetValidFiles().ToList();

            Assert.IsEmpty(validFiles, "Should return empty list when no files added");
        }

        [Test]
        public void GetValidFiles_FiltersOutNullReferences()
        {
            _asset.pythonFiles.Add(null);
            _asset.pythonFiles.Add(new TextAsset("print('test')"));
            _asset.pythonFiles.Add(null);

            var validFiles = _asset.GetValidFiles().ToList();

            Assert.AreEqual(1, validFiles.Count, "Should filter out null references");
        }

        [Test]
        public void GetValidFiles_ReturnsAllNonNullFiles()
        {
            var file1 = new TextAsset("print('test1')");
            var file2 = new TextAsset("print('test2')");

            _asset.pythonFiles.Add(file1);
            _asset.pythonFiles.Add(file2);

            var validFiles = _asset.GetValidFiles().ToList();

            Assert.AreEqual(2, validFiles.Count, "Should return all non-null files");
            CollectionAssert.Contains(validFiles, file1);
            CollectionAssert.Contains(validFiles, file2);
        }

        [Test]
        public void NeedsSync_ReturnsTrue_WhenHashingDisabled()
        {
            _asset.useContentHashing = false;
            var textAsset = new TextAsset("print('test')");

            bool needsSync = _asset.NeedsSync(textAsset, "any_hash");

            Assert.IsTrue(needsSync, "Should always need sync when hashing disabled");
        }

        [Test]
        public void NeedsSync_ReturnsTrue_WhenFileNotInStates()
        {
            _asset.useContentHashing = true;
            var textAsset = new TextAsset("print('test')");

            bool needsSync = _asset.NeedsSync(textAsset, "new_hash");

            Assert.IsTrue(needsSync, "Should need sync for new file");
        }

        [Test]
        public void NeedsSync_ReturnsFalse_WhenHashMatches()
        {
            _asset.useContentHashing = true;
            var textAsset = new TextAsset("print('test')");
            string hash = "test_hash_123";

            // Record the file with a hash
            _asset.RecordSync(textAsset, hash);

            // Check if needs sync with same hash
            bool needsSync = _asset.NeedsSync(textAsset, hash);

            Assert.IsFalse(needsSync, "Should not need sync when hash matches");
        }

        [Test]
        public void NeedsSync_ReturnsTrue_WhenHashDiffers()
        {
            _asset.useContentHashing = true;
            var textAsset = new TextAsset("print('test')");

            // Record with one hash
            _asset.RecordSync(textAsset, "old_hash");

            // Check with different hash
            bool needsSync = _asset.NeedsSync(textAsset, "new_hash");

            Assert.IsTrue(needsSync, "Should need sync when hash differs");
        }

        [Test]
        public void RecordSync_AddsNewFileState()
        {
            var textAsset = new TextAsset("print('test')");
            string hash = "test_hash";

            _asset.RecordSync(textAsset, hash);

            Assert.AreEqual(1, _asset.fileStates.Count, "Should add one file state");
            Assert.AreEqual(hash, _asset.fileStates[0].contentHash, "Should store the hash");
            Assert.IsNotNull(_asset.fileStates[0].assetGuid, "Should store the GUID");
        }

        [Test]
        public void RecordSync_UpdatesExistingFileState()
        {
            var textAsset = new TextAsset("print('test')");

            // Record first time
            _asset.RecordSync(textAsset, "hash1");
            var firstTime = _asset.fileStates[0].lastSyncTime;

            // Wait a tiny bit to ensure time difference
            System.Threading.Thread.Sleep(10);

            // Record second time with different hash
            _asset.RecordSync(textAsset, "hash2");

            Assert.AreEqual(1, _asset.fileStates.Count, "Should still have only one state");
            Assert.AreEqual("hash2", _asset.fileStates[0].contentHash, "Should update the hash");
            Assert.Greater(_asset.fileStates[0].lastSyncTime, firstTime, "Should update sync time");
        }

        [Test]
        public void CleanupStaleStates_KeepsStatesForCurrentFiles()
        {
            var file1 = new TextAsset("print('test1')");

            _asset.pythonFiles.Add(file1);
            _asset.RecordSync(file1, "hash1");

            _asset.CleanupStaleStates();

            Assert.AreEqual(1, _asset.fileStates.Count, "Should keep state for current file");
        }

        [Test]
        public void CleanupStaleStates_HandlesEmptyFilesList()
        {
            // Add some states without corresponding files
            _asset.fileStates.Add(new PythonFileState
            {
                assetGuid = "fake_guid_1",
                contentHash = "hash1",
                fileName = "test1.py",
                lastSyncTime = DateTime.UtcNow
            });

            _asset.CleanupStaleStates();

            Assert.IsEmpty(_asset.fileStates, "Should remove all states when no files exist");
        }
    }
}

```

--------------------------------------------------------------------------------
/MCPForUnity/UnityMcpServer~/src/port_discovery.py:
--------------------------------------------------------------------------------

```python
"""
Port discovery utility for MCP for Unity Server.

What changed and why:
- Unity now writes a per-project port file named like
  `~/.unity-mcp/unity-mcp-port-<hash>.json` to avoid projects overwriting
  each other's saved port. The legacy file `unity-mcp-port.json` may still
  exist.
- This module now scans for both patterns, prefers the most recently
  modified file, and verifies that the port is actually a MCP for Unity listener
  (quick socket connect + ping) before choosing it.
"""

import glob
import json
import logging
from pathlib import Path
import socket
from typing import Optional, List

logger = logging.getLogger("mcp-for-unity-server")


class PortDiscovery:
    """Handles port discovery from Unity Bridge registry"""
    REGISTRY_FILE = "unity-mcp-port.json"  # legacy single-project file
    DEFAULT_PORT = 6400
    CONNECT_TIMEOUT = 0.3  # seconds, keep this snappy during discovery

    @staticmethod
    def get_registry_path() -> Path:
        """Get the path to the port registry file"""
        return Path.home() / ".unity-mcp" / PortDiscovery.REGISTRY_FILE

    @staticmethod
    def get_registry_dir() -> Path:
        return Path.home() / ".unity-mcp"

    @staticmethod
    def list_candidate_files() -> List[Path]:
        """Return candidate registry files, newest first.
        Includes hashed per-project files and the legacy file (if present).
        """
        base = PortDiscovery.get_registry_dir()
        hashed = sorted(
            (Path(p) for p in glob.glob(str(base / "unity-mcp-port-*.json"))),
            key=lambda p: p.stat().st_mtime,
            reverse=True,
        )
        legacy = PortDiscovery.get_registry_path()
        if legacy.exists():
            # Put legacy at the end so hashed, per-project files win
            hashed.append(legacy)
        return hashed

    @staticmethod
    def _try_probe_unity_mcp(port: int) -> bool:
        """Quickly check if a MCP for Unity listener is on this port.
        Tries a short TCP connect, sends 'ping', expects a JSON 'pong'.
        """
        try:
            with socket.create_connection(("127.0.0.1", port), PortDiscovery.CONNECT_TIMEOUT) as s:
                s.settimeout(PortDiscovery.CONNECT_TIMEOUT)
                try:
                    s.sendall(b"ping")
                    data = s.recv(512)
                    # Minimal validation: look for a success pong response
                    if data and b'"message":"pong"' in data:
                        return True
                except Exception:
                    return False
        except Exception:
            return False
        return False

    @staticmethod
    def _read_latest_status() -> Optional[dict]:
        try:
            base = PortDiscovery.get_registry_dir()
            status_files = sorted(
                (Path(p)
                 for p in glob.glob(str(base / "unity-mcp-status-*.json"))),
                key=lambda p: p.stat().st_mtime,
                reverse=True,
            )
            if not status_files:
                return None
            with status_files[0].open('r') as f:
                return json.load(f)
        except Exception:
            return None

    @staticmethod
    def discover_unity_port() -> int:
        """
        Discover Unity port by scanning per-project and legacy registry files.
        Prefer the newest file whose port responds; fall back to first parsed
        value; finally default to 6400.

        Returns:
            Port number to connect to
        """
        # Prefer the latest heartbeat status if it points to a responsive port
        status = PortDiscovery._read_latest_status()
        if status:
            port = status.get('unity_port')
            if isinstance(port, int) and PortDiscovery._try_probe_unity_mcp(port):
                logger.info(f"Using Unity port from status: {port}")
                return port

        candidates = PortDiscovery.list_candidate_files()

        first_seen_port: Optional[int] = None

        for path in candidates:
            try:
                with open(path, 'r') as f:
                    cfg = json.load(f)
                unity_port = cfg.get('unity_port')
                if isinstance(unity_port, int):
                    if first_seen_port is None:
                        first_seen_port = unity_port
                    if PortDiscovery._try_probe_unity_mcp(unity_port):
                        logger.info(
                            f"Using Unity port from {path.name}: {unity_port}")
                        return unity_port
            except Exception as e:
                logger.warning(f"Could not read port registry {path}: {e}")

        if first_seen_port is not None:
            logger.info(
                f"No responsive port found; using first seen value {first_seen_port}")
            return first_seen_port

        # Fallback to default port
        logger.info(
            f"No port registry found; using default port {PortDiscovery.DEFAULT_PORT}")
        return PortDiscovery.DEFAULT_PORT

    @staticmethod
    def get_port_config() -> Optional[dict]:
        """
        Get the most relevant port configuration from registry.
        Returns the most recent hashed file's config if present,
        otherwise the legacy file's config. Returns None if nothing exists.

        Returns:
            Port configuration dict or None if not found
        """
        candidates = PortDiscovery.list_candidate_files()
        if not candidates:
            return None
        for path in candidates:
            try:
                with open(path, 'r') as f:
                    return json.load(f)
            except Exception as e:
                logger.warning(
                    f"Could not read port configuration {path}: {e}")
        return None

```

--------------------------------------------------------------------------------
/UnityMcpBridge/UnityMcpServer~/src/port_discovery.py:
--------------------------------------------------------------------------------

```python
"""
Port discovery utility for MCP for Unity Server.

What changed and why:
- Unity now writes a per-project port file named like
  `~/.unity-mcp/unity-mcp-port-<hash>.json` to avoid projects overwriting
  each other's saved port. The legacy file `unity-mcp-port.json` may still
  exist.
- This module now scans for both patterns, prefers the most recently
  modified file, and verifies that the port is actually a MCP for Unity listener
  (quick socket connect + ping) before choosing it.
"""

import glob
import json
import logging
from pathlib import Path
import socket
from typing import Optional, List

logger = logging.getLogger("mcp-for-unity-server")


class PortDiscovery:
    """Handles port discovery from Unity Bridge registry"""
    REGISTRY_FILE = "unity-mcp-port.json"  # legacy single-project file
    DEFAULT_PORT = 6400
    CONNECT_TIMEOUT = 0.3  # seconds, keep this snappy during discovery

    @staticmethod
    def get_registry_path() -> Path:
        """Get the path to the port registry file"""
        return Path.home() / ".unity-mcp" / PortDiscovery.REGISTRY_FILE

    @staticmethod
    def get_registry_dir() -> Path:
        return Path.home() / ".unity-mcp"

    @staticmethod
    def list_candidate_files() -> List[Path]:
        """Return candidate registry files, newest first.
        Includes hashed per-project files and the legacy file (if present).
        """
        base = PortDiscovery.get_registry_dir()
        hashed = sorted(
            (Path(p) for p in glob.glob(str(base / "unity-mcp-port-*.json"))),
            key=lambda p: p.stat().st_mtime,
            reverse=True,
        )
        legacy = PortDiscovery.get_registry_path()
        if legacy.exists():
            # Put legacy at the end so hashed, per-project files win
            hashed.append(legacy)
        return hashed

    @staticmethod
    def _try_probe_unity_mcp(port: int) -> bool:
        """Quickly check if a MCP for Unity listener is on this port.
        Tries a short TCP connect, sends 'ping', expects a JSON 'pong'.
        """
        try:
            with socket.create_connection(("127.0.0.1", port), PortDiscovery.CONNECT_TIMEOUT) as s:
                s.settimeout(PortDiscovery.CONNECT_TIMEOUT)
                try:
                    s.sendall(b"ping")
                    data = s.recv(512)
                    # Minimal validation: look for a success pong response
                    if data and b'"message":"pong"' in data:
                        return True
                except Exception:
                    return False
        except Exception:
            return False
        return False

    @staticmethod
    def _read_latest_status() -> Optional[dict]:
        try:
            base = PortDiscovery.get_registry_dir()
            status_files = sorted(
                (Path(p)
                 for p in glob.glob(str(base / "unity-mcp-status-*.json"))),
                key=lambda p: p.stat().st_mtime,
                reverse=True,
            )
            if not status_files:
                return None
            with status_files[0].open('r') as f:
                return json.load(f)
        except Exception:
            return None

    @staticmethod
    def discover_unity_port() -> int:
        """
        Discover Unity port by scanning per-project and legacy registry files.
        Prefer the newest file whose port responds; fall back to first parsed
        value; finally default to 6400.

        Returns:
            Port number to connect to
        """
        # Prefer the latest heartbeat status if it points to a responsive port
        status = PortDiscovery._read_latest_status()
        if status:
            port = status.get('unity_port')
            if isinstance(port, int) and PortDiscovery._try_probe_unity_mcp(port):
                logger.info(f"Using Unity port from status: {port}")
                return port

        candidates = PortDiscovery.list_candidate_files()

        first_seen_port: Optional[int] = None

        for path in candidates:
            try:
                with open(path, 'r') as f:
                    cfg = json.load(f)
                unity_port = cfg.get('unity_port')
                if isinstance(unity_port, int):
                    if first_seen_port is None:
                        first_seen_port = unity_port
                    if PortDiscovery._try_probe_unity_mcp(unity_port):
                        logger.info(
                            f"Using Unity port from {path.name}: {unity_port}")
                        return unity_port
            except Exception as e:
                logger.warning(f"Could not read port registry {path}: {e}")

        if first_seen_port is not None:
            logger.info(
                f"No responsive port found; using first seen value {first_seen_port}")
            return first_seen_port

        # Fallback to default port
        logger.info(
            f"No port registry found; using default port {PortDiscovery.DEFAULT_PORT}")
        return PortDiscovery.DEFAULT_PORT

    @staticmethod
    def get_port_config() -> Optional[dict]:
        """
        Get the most relevant port configuration from registry.
        Returns the most recent hashed file's config if present,
        otherwise the legacy file's config. Returns None if nothing exists.

        Returns:
            Port configuration dict or None if not found
        """
        candidates = PortDiscovery.list_candidate_files()
        if not candidates:
            return None
        for path in candidates:
            try:
                with open(path, 'r') as f:
                    return json.load(f)
            except Exception as e:
                logger.warning(
                    f"Could not read port configuration {path}: {e}")
        return None

```
Page 2/13FirstPrevNextLast