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

# Directory Structure

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

# Files

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

```markdown
 1 | ### Cursor/VSCode/Windsurf: UV path issue on Windows (diagnosis and fix)
 2 | 
 3 | #### The issue
 4 | - 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.
 5 | 
 6 | #### Typical symptoms
 7 | - Cursor shows the MCP for Unity server but never connects or reports it “can’t start.”
 8 | - Your `%USERPROFILE%\\.cursor\\mcp.json` flips back to a different `command` path when Unity or the MCP for Unity window refreshes.
 9 | 
10 | #### Real-world example
11 | - Wrong/fragile path (auto-picked):
12 |   - `C:\Users\mrken.local\bin\uv.exe` (malformed, not standard)
13 |   - `C:\Users\mrken\AppData\Local\Microsoft\WinGet\Packages\astral-sh.uv_Microsoft.Winget.Source_8wekyb3d8bbwe\uv.exe`
14 | - Correct/stable path (works with Cursor):
15 |   - `C:\Users\mrken\AppData\Local\Microsoft\WinGet\Links\uv.exe`
16 | 
17 | #### Quick fix (recommended)
18 | 1) In MCP for Unity: `Window > MCP for Unity` → select your MCP client (Cursor or Windsurf)
19 | 2) If you see “uv Not Found,” click “Choose `uv` Install Location” and browse to:
20 |    - `C:\Users\<YOU>\AppData\Local\Microsoft\WinGet\Links\uv.exe`
21 | 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.
22 | 4) Click “Auto Configure” (or re-open the client) and restart Cursor.
23 | 
24 | 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.
25 | 
26 | #### Verify the fix
27 | - Confirm global Cursor config is at: `%USERPROFILE%\\.cursor\\mcp.json`
28 | - You should see something like:
29 | 
30 | ```json
31 | {
32 |   "mcpServers": {
33 |     "unityMCP": {
34 |       "command": "C:\\Users\\YOU\\AppData\\Local\\Microsoft\\WinGet\\Links\\uv.exe",
35 |       "args": [
36 |         "--directory",
37 |         "C:\\Users\\YOU\\AppData\\Local\\Programs\\UnityMCP\\UnityMcpServer\\src",
38 |         "run",
39 |         "server.py"
40 |       ]
41 |     }
42 |   }
43 | }
44 | ```
45 | 
46 | - Manually run the same command in PowerShell to confirm it launches:
47 | 
48 | ```powershell
49 | "C:\Users\YOU\AppData\Local\Microsoft\WinGet\Links\uv.exe" --directory "C:\Users\YOU\AppData\Local\Programs\UnityMCP\UnityMcpServer\src" run server.py
50 | ```
51 | 
52 | If that runs without error, restart Cursor and it should connect.
53 | 
54 | #### Why this happens
55 | - 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.
56 | - 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.
57 | 
58 | #### Extra notes
59 | - Restart Cursor after changing `mcp.json`; it doesn’t always hot-reload that file.
60 | - If you also have a project-scoped `.cursor\\mcp.json` in your Unity project folder, that file overrides the global one.
61 | 
62 | 
63 | ### Why pin the WinGet Links shim (and not the Packages path)
64 | 
65 | - 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"`.
66 | - WinGet publishes stable launch shims in these locations:
67 |   - User scope: `%LOCALAPPDATA%\Microsoft\WinGet\Links\uv.exe`
68 |   - Machine scope: `C:\Program Files\WinGet\Links\uv.exe`
69 |   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)
70 | - The `Packages` root is where payloads live and can change across updates, so avoid pointing your config at it.
71 | 
72 | Recommended practice
73 | 
74 | - Prefer the WinGet Links shim paths above. If present, select one via “Choose `uv` Install Location”.
75 | - 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.
76 | - 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.
77 | 
78 | References
79 | 
80 | - WinGet portable Links: [GitHub discussion](https://github.com/microsoft/winget-pkgs/discussions/184459)
81 | - WinGet install locations: [Super User](https://superuser.com/questions/1739292/how-to-know-where-winget-installed-a-program)
82 | - GUI client PATH caveats (Cursor): [Cursor community thread](https://forum.cursor.com/t/mcp-feature-client-closed-fix/54651?page=4)
83 | - uv tools install location (`~/.local/bin`): [Astral docs](https://docs.astral.sh/uv/concepts/tools/)
84 | 
85 | 
86 | 
```

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

```csharp
  1 | using System;
  2 | using MCPForUnity.Editor.Dependencies;
  3 | using MCPForUnity.Editor.Dependencies.Models;
  4 | using MCPForUnity.Editor.Helpers;
  5 | using MCPForUnity.Editor.Windows;
  6 | using UnityEditor;
  7 | using UnityEngine;
  8 | 
  9 | namespace MCPForUnity.Editor.Setup
 10 | {
 11 |     /// <summary>
 12 |     /// Handles automatic triggering of the setup wizard
 13 |     /// </summary>
 14 |     [InitializeOnLoad]
 15 |     public static class SetupWizard
 16 |     {
 17 |         private const string SETUP_COMPLETED_KEY = "MCPForUnity.SetupCompleted";
 18 |         private const string SETUP_DISMISSED_KEY = "MCPForUnity.SetupDismissed";
 19 |         private static bool _hasCheckedThisSession = false;
 20 | 
 21 |         static SetupWizard()
 22 |         {
 23 |             // Skip in batch mode
 24 |             if (Application.isBatchMode)
 25 |                 return;
 26 | 
 27 |             // Show setup wizard on package import
 28 |             EditorApplication.delayCall += CheckSetupNeeded;
 29 |         }
 30 | 
 31 |         /// <summary>
 32 |         /// Check if setup wizard should be shown
 33 |         /// </summary>
 34 |         private static void CheckSetupNeeded()
 35 |         {
 36 |             if (_hasCheckedThisSession)
 37 |                 return;
 38 | 
 39 |             _hasCheckedThisSession = true;
 40 | 
 41 |             try
 42 |             {
 43 |                 // Check if setup was already completed or dismissed in previous sessions
 44 |                 bool setupCompleted = EditorPrefs.GetBool(SETUP_COMPLETED_KEY, false);
 45 |                 bool setupDismissed = EditorPrefs.GetBool(SETUP_DISMISSED_KEY, false);
 46 | 
 47 |                 // Only show setup wizard if it hasn't been completed or dismissed before
 48 |                 if (!(setupCompleted || setupDismissed))
 49 |                 {
 50 |                     McpLog.Info("Package imported - showing setup wizard", always: false);
 51 | 
 52 |                     var dependencyResult = DependencyManager.CheckAllDependencies();
 53 |                     EditorApplication.delayCall += () => ShowSetupWizard(dependencyResult);
 54 |                 }
 55 |                 else
 56 |                 {
 57 |                     McpLog.Info("Setup wizard skipped - previously completed or dismissed", always: false);
 58 |                 }
 59 |             }
 60 |             catch (Exception ex)
 61 |             {
 62 |                 McpLog.Error($"Error checking setup status: {ex.Message}");
 63 |             }
 64 |         }
 65 | 
 66 |         /// <summary>
 67 |         /// Show the setup wizard window
 68 |         /// </summary>
 69 |         public static void ShowSetupWizard(DependencyCheckResult dependencyResult = null)
 70 |         {
 71 |             try
 72 |             {
 73 |                 dependencyResult ??= DependencyManager.CheckAllDependencies();
 74 |                 SetupWizardWindow.ShowWindow(dependencyResult);
 75 |             }
 76 |             catch (Exception ex)
 77 |             {
 78 |                 McpLog.Error($"Error showing setup wizard: {ex.Message}");
 79 |             }
 80 |         }
 81 | 
 82 |         /// <summary>
 83 |         /// Mark setup as completed
 84 |         /// </summary>
 85 |         public static void MarkSetupCompleted()
 86 |         {
 87 |             EditorPrefs.SetBool(SETUP_COMPLETED_KEY, true);
 88 |             McpLog.Info("Setup marked as completed");
 89 |         }
 90 | 
 91 |         /// <summary>
 92 |         /// Mark setup as dismissed
 93 |         /// </summary>
 94 |         public static void MarkSetupDismissed()
 95 |         {
 96 |             EditorPrefs.SetBool(SETUP_DISMISSED_KEY, true);
 97 |             McpLog.Info("Setup marked as dismissed");
 98 |         }
 99 | 
100 |         /// <summary>
101 |         /// Force show setup wizard (for manual invocation)
102 |         /// </summary>
103 |         [MenuItem("Window/MCP For Unity/Setup Wizard", priority = 1)]
104 |         public static void ShowSetupWizardManual()
105 |         {
106 |             ShowSetupWizard();
107 |         }
108 | 
109 |         /// <summary>
110 |         /// Check dependencies and show status
111 |         /// </summary>
112 |         [MenuItem("Window/MCP For Unity/Check Dependencies", priority = 3)]
113 |         public static void CheckDependencies()
114 |         {
115 |             var result = DependencyManager.CheckAllDependencies();
116 | 
117 |             if (!result.IsSystemReady)
118 |             {
119 |                 bool showWizard = EditorUtility.DisplayDialog(
120 |                     "MCP for Unity - Dependencies",
121 |                     $"System Status: {result.Summary}\n\nWould you like to open the Setup Wizard?",
122 |                     "Open Setup Wizard",
123 |                     "Close"
124 |                 );
125 | 
126 |                 if (showWizard)
127 |                 {
128 |                     ShowSetupWizard(result);
129 |                 }
130 |             }
131 |             else
132 |             {
133 |                 EditorUtility.DisplayDialog(
134 |                     "MCP for Unity - Dependencies",
135 |                     "✓ All dependencies are available and ready!\n\nMCP for Unity is ready to use.",
136 |                     "OK"
137 |                 );
138 |             }
139 |         }
140 | 
141 |         /// <summary>
142 |         /// Open MCP Client Configuration window
143 |         /// </summary>
144 |         [MenuItem("Window/MCP For Unity/Open MCP Window", priority = 4)]
145 |         public static void OpenClientConfiguration()
146 |         {
147 |             Windows.MCPForUnityEditorWindow.ShowWindow();
148 |         }
149 |     }
150 | }
151 | 
```

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

```python
  1 | """
  2 | Test the improved anchor matching logic.
  3 | """
  4 | 
  5 | import sys
  6 | import pathlib
  7 | import importlib.util
  8 | import types
  9 | 
 10 | # add server src to path and load modules
 11 | ROOT = pathlib.Path(__file__).resolve().parents[1]
 12 | SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src"
 13 | sys.path.insert(0, str(SRC))
 14 | 
 15 | # stub mcp.server.fastmcp
 16 | mcp_pkg = types.ModuleType("mcp")
 17 | server_pkg = types.ModuleType("mcp.server")
 18 | fastmcp_pkg = types.ModuleType("mcp.server.fastmcp")
 19 | 
 20 | 
 21 | class _Dummy:
 22 |     pass
 23 | 
 24 | 
 25 | fastmcp_pkg.FastMCP = _Dummy
 26 | fastmcp_pkg.Context = _Dummy
 27 | server_pkg.fastmcp = fastmcp_pkg
 28 | mcp_pkg.server = server_pkg
 29 | sys.modules.setdefault("mcp", mcp_pkg)
 30 | sys.modules.setdefault("mcp.server", server_pkg)
 31 | sys.modules.setdefault("mcp.server.fastmcp", fastmcp_pkg)
 32 | 
 33 | 
 34 | def load_module(path, name):
 35 |     spec = importlib.util.spec_from_file_location(name, path)
 36 |     module = importlib.util.module_from_spec(spec)
 37 |     spec.loader.exec_module(module)
 38 |     return module
 39 | 
 40 | 
 41 | manage_script_edits_module = load_module(
 42 |     SRC / "tools" / "manage_script_edits.py", "manage_script_edits_module")
 43 | 
 44 | 
 45 | def test_improved_anchor_matching():
 46 |     """Test that our improved anchor matching finds the right closing brace."""
 47 | 
 48 |     test_code = '''using UnityEngine;
 49 | 
 50 | public class TestClass : MonoBehaviour  
 51 | {
 52 |     void Start()
 53 |     {
 54 |         Debug.Log("test");
 55 |     }
 56 |     
 57 |     void Update()
 58 |     {
 59 |         // Update logic
 60 |     }
 61 | }'''
 62 | 
 63 |     import re
 64 | 
 65 |     # Test the problematic anchor pattern
 66 |     anchor_pattern = r"\s*}\s*$"
 67 |     flags = re.MULTILINE
 68 | 
 69 |     # Test our improved function
 70 |     best_match = manage_script_edits_module._find_best_anchor_match(
 71 |         anchor_pattern, test_code, flags, prefer_last=True
 72 |     )
 73 | 
 74 |     assert best_match is not None, "anchor pattern not found"
 75 |     match_pos = best_match.start()
 76 |     line_num = test_code[:match_pos].count('\n') + 1
 77 |     total_lines = test_code.count('\n') + 1
 78 |     assert line_num >= total_lines - \
 79 |         2, f"expected match near end (>= {total_lines-2}), got line {line_num}"
 80 | 
 81 | 
 82 | def test_old_vs_new_matching():
 83 |     """Compare old vs new matching behavior."""
 84 | 
 85 |     test_code = '''using UnityEngine;
 86 | 
 87 | public class TestClass : MonoBehaviour  
 88 | {
 89 |     void Start()
 90 |     {
 91 |         Debug.Log("test");
 92 |     }
 93 |     
 94 |     void Update()
 95 |     {
 96 |         if (condition)
 97 |         {
 98 |             DoSomething();
 99 |         }
100 |     }
101 |     
102 |     void LateUpdate()
103 |     {
104 |         // More logic
105 |     }
106 | }'''
107 | 
108 |     import re
109 | 
110 |     anchor_pattern = r"\s*}\s*$"
111 |     flags = re.MULTILINE
112 | 
113 |     # Old behavior (first match)
114 |     old_match = re.search(anchor_pattern, test_code, flags)
115 |     old_line = test_code[:old_match.start()].count(
116 |         '\n') + 1 if old_match else None
117 | 
118 |     # New behavior (improved matching)
119 |     new_match = manage_script_edits_module._find_best_anchor_match(
120 |         anchor_pattern, test_code, flags, prefer_last=True
121 |     )
122 |     new_line = test_code[:new_match.start()].count(
123 |         '\n') + 1 if new_match else None
124 | 
125 |     assert old_line is not None and new_line is not None, "failed to locate anchors"
126 |     assert new_line > old_line, f"improved matcher should choose a later line (old={old_line}, new={new_line})"
127 |     total_lines = test_code.count('\n') + 1
128 |     assert new_line >= total_lines - \
129 |         2, f"expected class-end match near end (>= {total_lines-2}), got {new_line}"
130 | 
131 | 
132 | def test_apply_edits_with_improved_matching():
133 |     """Test that _apply_edits_locally uses improved matching."""
134 | 
135 |     original_code = '''using UnityEngine;
136 | 
137 | public class TestClass : MonoBehaviour
138 | {
139 |     public string message = "Hello World";
140 |     
141 |     void Start()
142 |     {
143 |         Debug.Log(message);
144 |     }
145 | }'''
146 | 
147 |     # Test anchor_insert with the problematic pattern
148 |     edits = [{
149 |         "op": "anchor_insert",
150 |         "anchor": r"\s*}\s*$",  # This should now find the class end
151 |         "position": "before",
152 |         "text": "\n    public void NewMethod() { Debug.Log(\"Added at class end\"); }\n"
153 |     }]
154 | 
155 |     result = manage_script_edits_module._apply_edits_locally(
156 |         original_code, edits)
157 |     lines = result.split('\n')
158 |     try:
159 |         idx = next(i for i, line in enumerate(lines) if "NewMethod" in line)
160 |     except StopIteration:
161 |         assert False, "NewMethod not found in result"
162 |     total_lines = len(lines)
163 |     assert idx >= total_lines - \
164 |         5, f"method inserted too early (idx={idx}, total_lines={total_lines})"
165 | 
166 | 
167 | if __name__ == "__main__":
168 |     print("Testing improved anchor matching...")
169 |     print("="*60)
170 | 
171 |     success1 = test_improved_anchor_matching()
172 | 
173 |     print("\n" + "="*60)
174 |     print("Comparing old vs new behavior...")
175 |     success2 = test_old_vs_new_matching()
176 | 
177 |     print("\n" + "="*60)
178 |     print("Testing _apply_edits_locally with improved matching...")
179 |     success3 = test_apply_edits_with_improved_matching()
180 | 
181 |     print("\n" + "="*60)
182 |     if success1 and success2 and success3:
183 |         print("🎉 ALL TESTS PASSED! Improved anchor matching is working!")
184 |     else:
185 |         print("💥 Some tests failed. Need more work on anchor matching.")
186 | 
```

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

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.IO;
  4 | using System.Linq;
  5 | using MCPForUnity.Editor.Helpers;
  6 | using UnityEditor;
  7 | 
  8 | namespace MCPForUnity.Editor.Services
  9 | {
 10 |     public class ToolSyncService : IToolSyncService
 11 |     {
 12 |         private readonly IPythonToolRegistryService _registryService;
 13 | 
 14 |         public ToolSyncService(IPythonToolRegistryService registryService = null)
 15 |         {
 16 |             _registryService = registryService ?? MCPServiceLocator.PythonToolRegistry;
 17 |         }
 18 | 
 19 |         public ToolSyncResult SyncProjectTools(string destToolsDir)
 20 |         {
 21 |             var result = new ToolSyncResult();
 22 | 
 23 |             try
 24 |             {
 25 |                 Directory.CreateDirectory(destToolsDir);
 26 | 
 27 |                 // Get all PythonToolsAsset instances in the project
 28 |                 var registries = _registryService.GetAllRegistries().ToList();
 29 | 
 30 |                 if (!registries.Any())
 31 |                 {
 32 |                     McpLog.Info("No PythonToolsAsset found. Create one via Assets > Create > MCP For Unity > Python Tools");
 33 |                     return result;
 34 |                 }
 35 | 
 36 |                 var syncedFiles = new HashSet<string>();
 37 | 
 38 |                 // Batch all asset modifications together to minimize reimports
 39 |                 AssetDatabase.StartAssetEditing();
 40 |                 try
 41 |                 {
 42 |                     foreach (var registry in registries)
 43 |                     {
 44 |                         foreach (var file in registry.GetValidFiles())
 45 |                         {
 46 |                             try
 47 |                             {
 48 |                                 // Check if needs syncing (hash-based or always)
 49 |                                 if (_registryService.NeedsSync(registry, file))
 50 |                                 {
 51 |                                     string destPath = Path.Combine(destToolsDir, file.name + ".py");
 52 | 
 53 |                                     // Write the Python file content
 54 |                                     File.WriteAllText(destPath, file.text);
 55 | 
 56 |                                     // Record sync
 57 |                                     _registryService.RecordSync(registry, file);
 58 | 
 59 |                                     result.CopiedCount++;
 60 |                                     syncedFiles.Add(destPath);
 61 |                                     McpLog.Info($"Synced Python tool: {file.name}.py");
 62 |                                 }
 63 |                                 else
 64 |                                 {
 65 |                                     string destPath = Path.Combine(destToolsDir, file.name + ".py");
 66 |                                     syncedFiles.Add(destPath);
 67 |                                     result.SkippedCount++;
 68 |                                 }
 69 |                             }
 70 |                             catch (Exception ex)
 71 |                             {
 72 |                                 result.ErrorCount++;
 73 |                                 result.Messages.Add($"Failed to sync {file.name}: {ex.Message}");
 74 |                             }
 75 |                         }
 76 | 
 77 |                         // Cleanup stale states in registry
 78 |                         registry.CleanupStaleStates();
 79 |                         EditorUtility.SetDirty(registry);
 80 |                     }
 81 | 
 82 |                     // Cleanup stale Python files in destination
 83 |                     CleanupStaleFiles(destToolsDir, syncedFiles);
 84 |                 }
 85 |                 finally
 86 |                 {
 87 |                     // End batch editing - this triggers a single asset refresh
 88 |                     AssetDatabase.StopAssetEditing();
 89 |                 }
 90 | 
 91 |                 // Save all modified registries
 92 |                 AssetDatabase.SaveAssets();
 93 |             }
 94 |             catch (Exception ex)
 95 |             {
 96 |                 result.ErrorCount++;
 97 |                 result.Messages.Add($"Sync failed: {ex.Message}");
 98 |             }
 99 | 
100 |             return result;
101 |         }
102 | 
103 |         private void CleanupStaleFiles(string destToolsDir, HashSet<string> currentFiles)
104 |         {
105 |             try
106 |             {
107 |                 if (!Directory.Exists(destToolsDir)) return;
108 | 
109 |                 // Find all .py files in destination that aren't in our current set
110 |                 var existingFiles = Directory.GetFiles(destToolsDir, "*.py");
111 | 
112 |                 foreach (var file in existingFiles)
113 |                 {
114 |                     if (!currentFiles.Contains(file))
115 |                     {
116 |                         try
117 |                         {
118 |                             File.Delete(file);
119 |                             McpLog.Info($"Cleaned up stale tool: {Path.GetFileName(file)}");
120 |                         }
121 |                         catch (Exception ex)
122 |                         {
123 |                             McpLog.Warn($"Failed to cleanup {file}: {ex.Message}");
124 |                         }
125 |                     }
126 |                 }
127 |             }
128 |             catch (Exception ex)
129 |             {
130 |                 McpLog.Warn($"Failed to cleanup stale files: {ex.Message}");
131 |             }
132 |         }
133 |     }
134 | }
135 | 
```

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

```csharp
  1 | using Newtonsoft.Json;
  2 | using Newtonsoft.Json.Linq;
  3 | using MCPForUnity.Editor.Models;
  4 | 
  5 | namespace MCPForUnity.Editor.Helpers
  6 | {
  7 |     public static class ConfigJsonBuilder
  8 |     {
  9 |         public static string BuildManualConfigJson(string uvPath, string pythonDir, McpClient client)
 10 |         {
 11 |             var root = new JObject();
 12 |             bool isVSCode = client?.mcpType == McpTypes.VSCode;
 13 |             JObject container;
 14 |             if (isVSCode)
 15 |             {
 16 |                 container = EnsureObject(root, "servers");
 17 |             }
 18 |             else
 19 |             {
 20 |                 container = EnsureObject(root, "mcpServers");
 21 |             }
 22 | 
 23 |             var unity = new JObject();
 24 |             PopulateUnityNode(unity, uvPath, pythonDir, client, isVSCode);
 25 | 
 26 |             container["unityMCP"] = unity;
 27 | 
 28 |             return root.ToString(Formatting.Indented);
 29 |         }
 30 | 
 31 |         public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPath, string serverSrc, McpClient client)
 32 |         {
 33 |             if (root == null) root = new JObject();
 34 |             bool isVSCode = client?.mcpType == McpTypes.VSCode;
 35 |             JObject container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers");
 36 |             JObject unity = container["unityMCP"] as JObject ?? new JObject();
 37 |             PopulateUnityNode(unity, uvPath, serverSrc, client, isVSCode);
 38 | 
 39 |             container["unityMCP"] = unity;
 40 |             return root;
 41 |         }
 42 | 
 43 |         /// <summary>
 44 |         /// Centralized builder that applies all caveats consistently.
 45 |         /// - Sets command/args with provided directory
 46 |         /// - Ensures env exists
 47 |         /// - Adds type:"stdio" for VSCode
 48 |         /// - Adds disabled:false for Windsurf/Kiro only when missing
 49 |         /// </summary>
 50 |         private static void PopulateUnityNode(JObject unity, string uvPath, string directory, McpClient client, bool isVSCode)
 51 |         {
 52 |             unity["command"] = uvPath;
 53 | 
 54 |             // For Cursor (non-VSCode) on macOS, prefer a no-spaces symlink path to avoid arg parsing issues in some runners
 55 |             string effectiveDir = directory;
 56 | #if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
 57 |             bool isCursor = !isVSCode && (client == null || client.mcpType != McpTypes.VSCode);
 58 |             if (isCursor && !string.IsNullOrEmpty(directory))
 59 |             {
 60 |                 // Replace canonical path segment with the symlink path if present
 61 |                 const string canonical = "/Library/Application Support/";
 62 |                 const string symlinkSeg = "/Library/AppSupport/";
 63 |                 try
 64 |                 {
 65 |                     // Normalize to full path style
 66 |                     if (directory.Contains(canonical))
 67 |                     {
 68 |                         var candidate = directory.Replace(canonical, symlinkSeg).Replace('\\', '/');
 69 |                         if (System.IO.Directory.Exists(candidate))
 70 |                         {
 71 |                             effectiveDir = candidate;
 72 |                         }
 73 |                     }
 74 |                     else
 75 |                     {
 76 |                         // If installer returned XDG-style on macOS, map to canonical symlink
 77 |                         string norm = directory.Replace('\\', '/');
 78 |                         int idx = norm.IndexOf("/.local/share/UnityMCP/", System.StringComparison.Ordinal);
 79 |                         if (idx >= 0)
 80 |                         {
 81 |                             string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal) ?? string.Empty;
 82 |                             string suffix = norm.Substring(idx + "/.local/share/".Length); // UnityMCP/...
 83 |                             string candidate = System.IO.Path.Combine(home, "Library", "AppSupport", suffix).Replace('\\', '/');
 84 |                             if (System.IO.Directory.Exists(candidate))
 85 |                             {
 86 |                                 effectiveDir = candidate;
 87 |                             }
 88 |                         }
 89 |                     }
 90 |                 }
 91 |                 catch { /* fallback to original directory on any error */ }
 92 |             }
 93 | #endif
 94 | 
 95 |             unity["args"] = JArray.FromObject(new[] { "run", "--directory", effectiveDir, "server.py" });
 96 | 
 97 |             if (isVSCode)
 98 |             {
 99 |                 unity["type"] = "stdio";
100 |             }
101 |             else
102 |             {
103 |                 // Remove type if it somehow exists from previous clients
104 |                 if (unity["type"] != null) unity.Remove("type");
105 |             }
106 | 
107 |             if (client != null && (client.mcpType == McpTypes.Windsurf || client.mcpType == McpTypes.Kiro))
108 |             {
109 |                 if (unity["env"] == null)
110 |                 {
111 |                     unity["env"] = new JObject();
112 |                 }
113 | 
114 |                 if (unity["disabled"] == null)
115 |                 {
116 |                     unity["disabled"] = false;
117 |                 }
118 |             }
119 |         }
120 | 
121 |         private static JObject EnsureObject(JObject parent, string name)
122 |         {
123 |             if (parent[name] is JObject o) return o;
124 |             var created = new JObject();
125 |             parent[name] = created;
126 |             return created;
127 |         }
128 |     }
129 | }
130 | 
```

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

```csharp
  1 | using Newtonsoft.Json;
  2 | using Newtonsoft.Json.Linq;
  3 | using MCPForUnity.Editor.Models;
  4 | 
  5 | namespace MCPForUnity.Editor.Helpers
  6 | {
  7 |     public static class ConfigJsonBuilder
  8 |     {
  9 |         public static string BuildManualConfigJson(string uvPath, string pythonDir, McpClient client)
 10 |         {
 11 |             var root = new JObject();
 12 |             bool isVSCode = client?.mcpType == McpTypes.VSCode;
 13 |             JObject container;
 14 |             if (isVSCode)
 15 |             {
 16 |                 container = EnsureObject(root, "servers");
 17 |             }
 18 |             else
 19 |             {
 20 |                 container = EnsureObject(root, "mcpServers");
 21 |             }
 22 | 
 23 |             var unity = new JObject();
 24 |             PopulateUnityNode(unity, uvPath, pythonDir, client, isVSCode);
 25 | 
 26 |             container["unityMCP"] = unity;
 27 | 
 28 |             return root.ToString(Formatting.Indented);
 29 |         }
 30 | 
 31 |         public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPath, string serverSrc, McpClient client)
 32 |         {
 33 |             if (root == null) root = new JObject();
 34 |             bool isVSCode = client?.mcpType == McpTypes.VSCode;
 35 |             JObject container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers");
 36 |             JObject unity = container["unityMCP"] as JObject ?? new JObject();
 37 |             PopulateUnityNode(unity, uvPath, serverSrc, client, isVSCode);
 38 | 
 39 |             container["unityMCP"] = unity;
 40 |             return root;
 41 |         }
 42 | 
 43 |         /// <summary>
 44 |         /// Centralized builder that applies all caveats consistently.
 45 |         /// - Sets command/args with provided directory
 46 |         /// - Ensures env exists
 47 |         /// - Adds type:"stdio" for VSCode
 48 |         /// - Adds disabled:false for Windsurf/Kiro only when missing
 49 |         /// </summary>
 50 |         private static void PopulateUnityNode(JObject unity, string uvPath, string directory, McpClient client, bool isVSCode)
 51 |         {
 52 |             unity["command"] = uvPath;
 53 | 
 54 |             // For Cursor (non-VSCode) on macOS, prefer a no-spaces symlink path to avoid arg parsing issues in some runners
 55 |             string effectiveDir = directory;
 56 | #if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
 57 |             bool isCursor = !isVSCode && (client == null || client.mcpType != McpTypes.VSCode);
 58 |             if (isCursor && !string.IsNullOrEmpty(directory))
 59 |             {
 60 |                 // Replace canonical path segment with the symlink path if present
 61 |                 const string canonical = "/Library/Application Support/";
 62 |                 const string symlinkSeg = "/Library/AppSupport/";
 63 |                 try
 64 |                 {
 65 |                     // Normalize to full path style
 66 |                     if (directory.Contains(canonical))
 67 |                     {
 68 |                         var candidate = directory.Replace(canonical, symlinkSeg).Replace('\\', '/');
 69 |                         if (System.IO.Directory.Exists(candidate))
 70 |                         {
 71 |                             effectiveDir = candidate;
 72 |                         }
 73 |                     }
 74 |                     else
 75 |                     {
 76 |                         // If installer returned XDG-style on macOS, map to canonical symlink
 77 |                         string norm = directory.Replace('\\', '/');
 78 |                         int idx = norm.IndexOf("/.local/share/UnityMCP/", System.StringComparison.Ordinal);
 79 |                         if (idx >= 0)
 80 |                         {
 81 |                             string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal) ?? string.Empty;
 82 |                             string suffix = norm.Substring(idx + "/.local/share/".Length); // UnityMCP/...
 83 |                             string candidate = System.IO.Path.Combine(home, "Library", "AppSupport", suffix).Replace('\\', '/');
 84 |                             if (System.IO.Directory.Exists(candidate))
 85 |                             {
 86 |                                 effectiveDir = candidate;
 87 |                             }
 88 |                         }
 89 |                     }
 90 |                 }
 91 |                 catch { /* fallback to original directory on any error */ }
 92 |             }
 93 | #endif
 94 | 
 95 |             unity["args"] = JArray.FromObject(new[] { "run", "--directory", effectiveDir, "server.py" });
 96 | 
 97 |             if (isVSCode)
 98 |             {
 99 |                 unity["type"] = "stdio";
100 |             }
101 |             else
102 |             {
103 |                 // Remove type if it somehow exists from previous clients
104 |                 if (unity["type"] != null) unity.Remove("type");
105 |             }
106 | 
107 |             if (client != null && (client.mcpType == McpTypes.Windsurf || client.mcpType == McpTypes.Kiro))
108 |             {
109 |                 if (unity["env"] == null)
110 |                 {
111 |                     unity["env"] = new JObject();
112 |                 }
113 | 
114 |                 if (unity["disabled"] == null)
115 |                 {
116 |                     unity["disabled"] = false;
117 |                 }
118 |             }
119 |         }
120 | 
121 |         private static JObject EnsureObject(JObject parent, string name)
122 |         {
123 |             if (parent[name] is JObject o) return o;
124 |             var created = new JObject();
125 |             parent[name] = created;
126 |             return created;
127 |         }
128 |     }
129 | }
130 | 
```

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

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.Linq;
  4 | using System.Runtime.InteropServices;
  5 | using MCPForUnity.Editor.Dependencies.Models;
  6 | using MCPForUnity.Editor.Dependencies.PlatformDetectors;
  7 | using MCPForUnity.Editor.Helpers;
  8 | using UnityEditor;
  9 | using UnityEngine;
 10 | 
 11 | namespace MCPForUnity.Editor.Dependencies
 12 | {
 13 |     /// <summary>
 14 |     /// Main orchestrator for dependency validation and management
 15 |     /// </summary>
 16 |     public static class DependencyManager
 17 |     {
 18 |         private static readonly List<IPlatformDetector> _detectors = new List<IPlatformDetector>
 19 |         {
 20 |             new WindowsPlatformDetector(),
 21 |             new MacOSPlatformDetector(),
 22 |             new LinuxPlatformDetector()
 23 |         };
 24 | 
 25 |         private static IPlatformDetector _currentDetector;
 26 | 
 27 |         /// <summary>
 28 |         /// Get the platform detector for the current operating system
 29 |         /// </summary>
 30 |         public static IPlatformDetector GetCurrentPlatformDetector()
 31 |         {
 32 |             if (_currentDetector == null)
 33 |             {
 34 |                 _currentDetector = _detectors.FirstOrDefault(d => d.CanDetect);
 35 |                 if (_currentDetector == null)
 36 |                 {
 37 |                     throw new PlatformNotSupportedException($"No detector available for current platform: {RuntimeInformation.OSDescription}");
 38 |                 }
 39 |             }
 40 |             return _currentDetector;
 41 |         }
 42 | 
 43 |         /// <summary>
 44 |         /// Perform a comprehensive dependency check
 45 |         /// </summary>
 46 |         public static DependencyCheckResult CheckAllDependencies()
 47 |         {
 48 |             var result = new DependencyCheckResult();
 49 | 
 50 |             try
 51 |             {
 52 |                 var detector = GetCurrentPlatformDetector();
 53 |                 McpLog.Info($"Checking dependencies on {detector.PlatformName}...", always: false);
 54 | 
 55 |                 // Check Python
 56 |                 var pythonStatus = detector.DetectPython();
 57 |                 result.Dependencies.Add(pythonStatus);
 58 | 
 59 |                 // Check UV
 60 |                 var uvStatus = detector.DetectUV();
 61 |                 result.Dependencies.Add(uvStatus);
 62 | 
 63 |                 // Check MCP Server
 64 |                 var serverStatus = detector.DetectMCPServer();
 65 |                 result.Dependencies.Add(serverStatus);
 66 | 
 67 |                 // Generate summary and recommendations
 68 |                 result.GenerateSummary();
 69 |                 GenerateRecommendations(result, detector);
 70 | 
 71 |                 McpLog.Info($"Dependency check completed. System ready: {result.IsSystemReady}", always: false);
 72 |             }
 73 |             catch (Exception ex)
 74 |             {
 75 |                 McpLog.Error($"Error during dependency check: {ex.Message}");
 76 |                 result.Summary = $"Dependency check failed: {ex.Message}";
 77 |                 result.IsSystemReady = false;
 78 |             }
 79 | 
 80 |             return result;
 81 |         }
 82 | 
 83 |         /// <summary>
 84 |         /// Get installation recommendations for the current platform
 85 |         /// </summary>
 86 |         public static string GetInstallationRecommendations()
 87 |         {
 88 |             try
 89 |             {
 90 |                 var detector = GetCurrentPlatformDetector();
 91 |                 return detector.GetInstallationRecommendations();
 92 |             }
 93 |             catch (Exception ex)
 94 |             {
 95 |                 return $"Error getting installation recommendations: {ex.Message}";
 96 |             }
 97 |         }
 98 | 
 99 |         /// <summary>
100 |         /// Get platform-specific installation URLs
101 |         /// </summary>
102 |         public static (string pythonUrl, string uvUrl) GetInstallationUrls()
103 |         {
104 |             try
105 |             {
106 |                 var detector = GetCurrentPlatformDetector();
107 |                 return (detector.GetPythonInstallUrl(), detector.GetUVInstallUrl());
108 |             }
109 |             catch
110 |             {
111 |                 return ("https://python.org/downloads/", "https://docs.astral.sh/uv/getting-started/installation/");
112 |             }
113 |         }
114 | 
115 |         private static void GenerateRecommendations(DependencyCheckResult result, IPlatformDetector detector)
116 |         {
117 |             var missing = result.GetMissingDependencies();
118 | 
119 |             if (missing.Count == 0)
120 |             {
121 |                 result.RecommendedActions.Add("All dependencies are available. You can start using MCP for Unity.");
122 |                 return;
123 |             }
124 | 
125 |             foreach (var dep in missing)
126 |             {
127 |                 if (dep.Name == "Python")
128 |                 {
129 |                     result.RecommendedActions.Add($"Install Python 3.10+ from: {detector.GetPythonInstallUrl()}");
130 |                 }
131 |                 else if (dep.Name == "UV Package Manager")
132 |                 {
133 |                     result.RecommendedActions.Add($"Install UV package manager from: {detector.GetUVInstallUrl()}");
134 |                 }
135 |                 else if (dep.Name == "MCP Server")
136 |                 {
137 |                     result.RecommendedActions.Add("MCP Server will be installed automatically when needed.");
138 |                 }
139 |             }
140 | 
141 |             if (result.GetMissingRequired().Count > 0)
142 |             {
143 |                 result.RecommendedActions.Add("Use the Setup Wizard (Window > MCP for Unity > Setup Wizard) for guided installation.");
144 |             }
145 |         }
146 |     }
147 | }
148 | 
```

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

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.Linq;
  4 | using System.Runtime.InteropServices;
  5 | using MCPForUnity.Editor.Dependencies.Models;
  6 | using MCPForUnity.Editor.Dependencies.PlatformDetectors;
  7 | using MCPForUnity.Editor.Helpers;
  8 | using UnityEditor;
  9 | using UnityEngine;
 10 | 
 11 | namespace MCPForUnity.Editor.Dependencies
 12 | {
 13 |     /// <summary>
 14 |     /// Main orchestrator for dependency validation and management
 15 |     /// </summary>
 16 |     public static class DependencyManager
 17 |     {
 18 |         private static readonly List<IPlatformDetector> _detectors = new List<IPlatformDetector>
 19 |         {
 20 |             new WindowsPlatformDetector(),
 21 |             new MacOSPlatformDetector(),
 22 |             new LinuxPlatformDetector()
 23 |         };
 24 | 
 25 |         private static IPlatformDetector _currentDetector;
 26 | 
 27 |         /// <summary>
 28 |         /// Get the platform detector for the current operating system
 29 |         /// </summary>
 30 |         public static IPlatformDetector GetCurrentPlatformDetector()
 31 |         {
 32 |             if (_currentDetector == null)
 33 |             {
 34 |                 _currentDetector = _detectors.FirstOrDefault(d => d.CanDetect);
 35 |                 if (_currentDetector == null)
 36 |                 {
 37 |                     throw new PlatformNotSupportedException($"No detector available for current platform: {RuntimeInformation.OSDescription}");
 38 |                 }
 39 |             }
 40 |             return _currentDetector;
 41 |         }
 42 | 
 43 |         /// <summary>
 44 |         /// Perform a comprehensive dependency check
 45 |         /// </summary>
 46 |         public static DependencyCheckResult CheckAllDependencies()
 47 |         {
 48 |             var result = new DependencyCheckResult();
 49 | 
 50 |             try
 51 |             {
 52 |                 var detector = GetCurrentPlatformDetector();
 53 |                 McpLog.Info($"Checking dependencies on {detector.PlatformName}...", always: false);
 54 | 
 55 |                 // Check Python
 56 |                 var pythonStatus = detector.DetectPython();
 57 |                 result.Dependencies.Add(pythonStatus);
 58 | 
 59 |                 // Check UV
 60 |                 var uvStatus = detector.DetectUV();
 61 |                 result.Dependencies.Add(uvStatus);
 62 | 
 63 |                 // Check MCP Server
 64 |                 var serverStatus = detector.DetectMCPServer();
 65 |                 result.Dependencies.Add(serverStatus);
 66 | 
 67 |                 // Generate summary and recommendations
 68 |                 result.GenerateSummary();
 69 |                 GenerateRecommendations(result, detector);
 70 | 
 71 |                 McpLog.Info($"Dependency check completed. System ready: {result.IsSystemReady}", always: false);
 72 |             }
 73 |             catch (Exception ex)
 74 |             {
 75 |                 McpLog.Error($"Error during dependency check: {ex.Message}");
 76 |                 result.Summary = $"Dependency check failed: {ex.Message}";
 77 |                 result.IsSystemReady = false;
 78 |             }
 79 | 
 80 |             return result;
 81 |         }
 82 | 
 83 |         /// <summary>
 84 |         /// Get installation recommendations for the current platform
 85 |         /// </summary>
 86 |         public static string GetInstallationRecommendations()
 87 |         {
 88 |             try
 89 |             {
 90 |                 var detector = GetCurrentPlatformDetector();
 91 |                 return detector.GetInstallationRecommendations();
 92 |             }
 93 |             catch (Exception ex)
 94 |             {
 95 |                 return $"Error getting installation recommendations: {ex.Message}";
 96 |             }
 97 |         }
 98 | 
 99 |         /// <summary>
100 |         /// Get platform-specific installation URLs
101 |         /// </summary>
102 |         public static (string pythonUrl, string uvUrl) GetInstallationUrls()
103 |         {
104 |             try
105 |             {
106 |                 var detector = GetCurrentPlatformDetector();
107 |                 return (detector.GetPythonInstallUrl(), detector.GetUVInstallUrl());
108 |             }
109 |             catch
110 |             {
111 |                 return ("https://python.org/downloads/", "https://docs.astral.sh/uv/getting-started/installation/");
112 |             }
113 |         }
114 | 
115 |         private static void GenerateRecommendations(DependencyCheckResult result, IPlatformDetector detector)
116 |         {
117 |             var missing = result.GetMissingDependencies();
118 | 
119 |             if (missing.Count == 0)
120 |             {
121 |                 result.RecommendedActions.Add("All dependencies are available. You can start using MCP for Unity.");
122 |                 return;
123 |             }
124 | 
125 |             foreach (var dep in missing)
126 |             {
127 |                 if (dep.Name == "Python")
128 |                 {
129 |                     result.RecommendedActions.Add($"Install Python 3.10+ from: {detector.GetPythonInstallUrl()}");
130 |                 }
131 |                 else if (dep.Name == "UV Package Manager")
132 |                 {
133 |                     result.RecommendedActions.Add($"Install UV package manager from: {detector.GetUVInstallUrl()}");
134 |                 }
135 |                 else if (dep.Name == "MCP Server")
136 |                 {
137 |                     result.RecommendedActions.Add("MCP Server will be installed automatically when needed.");
138 |                 }
139 |             }
140 | 
141 |             if (result.GetMissingRequired().Count > 0)
142 |             {
143 |                 result.RecommendedActions.Add("Use the Setup Wizard (Window > MCP for Unity > Setup Wizard) for guided installation.");
144 |             }
145 |         }
146 |     }
147 | }
148 | 
```

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

```csharp
  1 | using System;
  2 | using MCPForUnity.Editor.Dependencies;
  3 | using MCPForUnity.Editor.Dependencies.Models;
  4 | using MCPForUnity.Editor.Helpers;
  5 | using MCPForUnity.Editor.Windows;
  6 | using UnityEditor;
  7 | using UnityEngine;
  8 | 
  9 | namespace MCPForUnity.Editor.Setup
 10 | {
 11 |     /// <summary>
 12 |     /// Handles automatic triggering of the setup wizard
 13 |     /// </summary>
 14 |     [InitializeOnLoad]
 15 |     public static class SetupWizard
 16 |     {
 17 |         private const string SETUP_COMPLETED_KEY = "MCPForUnity.SetupCompleted";
 18 |         private const string SETUP_DISMISSED_KEY = "MCPForUnity.SetupDismissed";
 19 |         private static bool _hasCheckedThisSession = false;
 20 | 
 21 |         static SetupWizard()
 22 |         {
 23 |             // Skip in batch mode
 24 |             if (Application.isBatchMode)
 25 |                 return;
 26 | 
 27 |             // Show setup wizard on package import
 28 |             EditorApplication.delayCall += CheckSetupNeeded;
 29 |         }
 30 | 
 31 |         /// <summary>
 32 |         /// Check if setup wizard should be shown
 33 |         /// </summary>
 34 |         private static void CheckSetupNeeded()
 35 |         {
 36 |             if (_hasCheckedThisSession)
 37 |                 return;
 38 | 
 39 |             _hasCheckedThisSession = true;
 40 | 
 41 |             try
 42 |             {
 43 |                 // Check if setup was already completed or dismissed in previous sessions
 44 |                 bool setupCompleted = EditorPrefs.GetBool(SETUP_COMPLETED_KEY, false);
 45 |                 bool setupDismissed = EditorPrefs.GetBool(SETUP_DISMISSED_KEY, false);
 46 | 
 47 |                 // Only show setup wizard if it hasn't been completed or dismissed before
 48 |                 if (!(setupCompleted || setupDismissed))
 49 |                 {
 50 |                     McpLog.Info("Package imported - showing setup wizard", always: false);
 51 | 
 52 |                     var dependencyResult = DependencyManager.CheckAllDependencies();
 53 |                     EditorApplication.delayCall += () => ShowSetupWizard(dependencyResult);
 54 |                 }
 55 |                 else
 56 |                 {
 57 |                     McpLog.Info("Setup wizard skipped - previously completed or dismissed", always: false);
 58 |                 }
 59 |             }
 60 |             catch (Exception ex)
 61 |             {
 62 |                 McpLog.Error($"Error checking setup status: {ex.Message}");
 63 |             }
 64 |         }
 65 | 
 66 |         /// <summary>
 67 |         /// Show the setup wizard window
 68 |         /// </summary>
 69 |         public static void ShowSetupWizard(DependencyCheckResult dependencyResult = null)
 70 |         {
 71 |             try
 72 |             {
 73 |                 dependencyResult ??= DependencyManager.CheckAllDependencies();
 74 |                 SetupWizardWindow.ShowWindow(dependencyResult);
 75 |             }
 76 |             catch (Exception ex)
 77 |             {
 78 |                 McpLog.Error($"Error showing setup wizard: {ex.Message}");
 79 |             }
 80 |         }
 81 | 
 82 |         /// <summary>
 83 |         /// Mark setup as completed
 84 |         /// </summary>
 85 |         public static void MarkSetupCompleted()
 86 |         {
 87 |             EditorPrefs.SetBool(SETUP_COMPLETED_KEY, true);
 88 |             McpLog.Info("Setup marked as completed");
 89 |         }
 90 | 
 91 |         /// <summary>
 92 |         /// Mark setup as dismissed
 93 |         /// </summary>
 94 |         public static void MarkSetupDismissed()
 95 |         {
 96 |             EditorPrefs.SetBool(SETUP_DISMISSED_KEY, true);
 97 |             McpLog.Info("Setup marked as dismissed");
 98 |         }
 99 | 
100 |         /// <summary>
101 |         /// Force show setup wizard (for manual invocation)
102 |         /// </summary>
103 |         [MenuItem("Window/MCP For Unity/Setup Wizard", priority = 1)]
104 |         public static void ShowSetupWizardManual()
105 |         {
106 |             ShowSetupWizard();
107 |         }
108 | 
109 |         /// <summary>
110 |         /// Check dependencies and show status
111 |         /// </summary>
112 |         [MenuItem("Window/MCP For Unity/Check Dependencies", priority = 3)]
113 |         public static void CheckDependencies()
114 |         {
115 |             var result = DependencyManager.CheckAllDependencies();
116 | 
117 |             if (!result.IsSystemReady)
118 |             {
119 |                 bool showWizard = EditorUtility.DisplayDialog(
120 |                     "MCP for Unity - Dependencies",
121 |                     $"System Status: {result.Summary}\n\nWould you like to open the Setup Wizard?",
122 |                     "Open Setup Wizard",
123 |                     "Close"
124 |                 );
125 | 
126 |                 if (showWizard)
127 |                 {
128 |                     ShowSetupWizard(result);
129 |                 }
130 |             }
131 |             else
132 |             {
133 |                 EditorUtility.DisplayDialog(
134 |                     "MCP for Unity - Dependencies",
135 |                     "✓ All dependencies are available and ready!\n\nMCP for Unity is ready to use.",
136 |                     "OK"
137 |                 );
138 |             }
139 |         }
140 | 
141 |         /// <summary>
142 |         /// Open MCP Client Configuration window
143 |         /// </summary>
144 |         [MenuItem("Window/MCP For Unity/Open MCP Window %#m", priority = 4)]
145 |         public static void OpenClientConfiguration()
146 |         {
147 |             Windows.MCPForUnityEditorWindowNew.ShowWindow();
148 |         }
149 | 
150 |         /// <summary>
151 |         /// Open legacy MCP Client Configuration window
152 |         /// </summary>
153 |         [MenuItem("Window/MCP For Unity/Open Legacy MCP Window", priority = 5)]
154 |         public static void OpenLegacyClientConfiguration()
155 |         {
156 |             Windows.MCPForUnityEditorWindow.ShowWindow();
157 |         }
158 |     }
159 | }
160 | 
```

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

```csharp
  1 | using System;
  2 | using System.IO;
  3 | using UnityEngine;
  4 | using UnityEditor;
  5 | using MCPForUnity.Editor.Helpers;
  6 | 
  7 | namespace MCPForUnity.Editor.Helpers
  8 | {
  9 |     /// <summary>
 10 |     /// Shared helper for resolving MCP server directory paths with support for
 11 |     /// development mode, embedded servers, and installed packages
 12 |     /// </summary>
 13 |     public static class McpPathResolver
 14 |     {
 15 |         private const string USE_EMBEDDED_SERVER_KEY = "MCPForUnity.UseEmbeddedServer";
 16 | 
 17 |         /// <summary>
 18 |         /// Resolves the MCP server directory path with comprehensive logic
 19 |         /// including development mode support and fallback mechanisms
 20 |         /// </summary>
 21 |         public static string FindPackagePythonDirectory(bool debugLogsEnabled = false)
 22 |         {
 23 |             string pythonDir = McpConfigFileHelper.ResolveServerSource();
 24 | 
 25 |             try
 26 |             {
 27 |                 // Only check dev paths if we're using a file-based package (development mode)
 28 |                 bool isDevelopmentMode = IsDevelopmentMode();
 29 |                 if (isDevelopmentMode)
 30 |                 {
 31 |                     string currentPackagePath = Path.GetDirectoryName(Application.dataPath);
 32 |                     string[] devPaths = {
 33 |                         Path.Combine(currentPackagePath, "unity-mcp", "UnityMcpServer", "src"),
 34 |                         Path.Combine(Path.GetDirectoryName(currentPackagePath), "unity-mcp", "UnityMcpServer", "src"),
 35 |                     };
 36 | 
 37 |                     foreach (string devPath in devPaths)
 38 |                     {
 39 |                         if (Directory.Exists(devPath) && File.Exists(Path.Combine(devPath, "server.py")))
 40 |                         {
 41 |                             if (debugLogsEnabled)
 42 |                             {
 43 |                                 Debug.Log($"Currently in development mode. Package: {devPath}");
 44 |                             }
 45 |                             return devPath;
 46 |                         }
 47 |                     }
 48 |                 }
 49 | 
 50 |                 // Resolve via shared helper (handles local registry and older fallback) only if dev override on
 51 |                 if (EditorPrefs.GetBool(USE_EMBEDDED_SERVER_KEY, false))
 52 |                 {
 53 |                     if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded))
 54 |                     {
 55 |                         return embedded;
 56 |                     }
 57 |                 }
 58 | 
 59 |                 // Log only if the resolved path does not actually contain server.py
 60 |                 if (debugLogsEnabled)
 61 |                 {
 62 |                     bool hasServer = false;
 63 |                     try { hasServer = File.Exists(Path.Combine(pythonDir, "server.py")); } catch { }
 64 |                     if (!hasServer)
 65 |                     {
 66 |                         Debug.LogWarning("Could not find Python directory with server.py; falling back to installed path");
 67 |                     }
 68 |                 }
 69 |             }
 70 |             catch (Exception e)
 71 |             {
 72 |                 Debug.LogError($"Error finding package path: {e.Message}");
 73 |             }
 74 | 
 75 |             return pythonDir;
 76 |         }
 77 | 
 78 |         /// <summary>
 79 |         /// Checks if the current Unity project is in development mode
 80 |         /// (i.e., the package is referenced as a local file path in manifest.json)
 81 |         /// </summary>
 82 |         private static bool IsDevelopmentMode()
 83 |         {
 84 |             try
 85 |             {
 86 |                 // Only treat as development if manifest explicitly references a local file path for the package
 87 |                 string manifestPath = Path.Combine(Application.dataPath, "..", "Packages", "manifest.json");
 88 |                 if (!File.Exists(manifestPath)) return false;
 89 | 
 90 |                 string manifestContent = File.ReadAllText(manifestPath);
 91 |                 // Look specifically for our package dependency set to a file: URL
 92 |                 // This avoids auto-enabling dev mode just because a repo exists elsewhere on disk
 93 |                 if (manifestContent.IndexOf("\"com.coplaydev.unity-mcp\"", StringComparison.OrdinalIgnoreCase) >= 0)
 94 |                 {
 95 |                     int idx = manifestContent.IndexOf("com.coplaydev.unity-mcp", StringComparison.OrdinalIgnoreCase);
 96 |                     // Crude but effective: check for "file:" in the same line/value
 97 |                     if (manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase) >= 0
 98 |                         && manifestContent.IndexOf("\n", idx, StringComparison.OrdinalIgnoreCase) > manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase))
 99 |                     {
100 |                         return true;
101 |                     }
102 |                 }
103 |                 return false;
104 |             }
105 |             catch
106 |             {
107 |                 return false;
108 |             }
109 |         }
110 | 
111 |         /// <summary>
112 |         /// Gets the appropriate PATH prepend for the current platform when running external processes
113 |         /// </summary>
114 |         public static string GetPathPrepend()
115 |         {
116 |             if (Application.platform == RuntimePlatform.OSXEditor)
117 |                 return "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
118 |             else if (Application.platform == RuntimePlatform.LinuxEditor)
119 |                 return "/usr/local/bin:/usr/bin:/bin";
120 |             return null;
121 |         }
122 |     }
123 | }
124 | 
```

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

```csharp
  1 | using System;
  2 | using System.IO;
  3 | using UnityEngine;
  4 | using UnityEditor;
  5 | using MCPForUnity.Editor.Helpers;
  6 | 
  7 | namespace MCPForUnity.Editor.Helpers
  8 | {
  9 |     /// <summary>
 10 |     /// Shared helper for resolving Python server directory paths with support for
 11 |     /// development mode, embedded servers, and installed packages
 12 |     /// </summary>
 13 |     public static class McpPathResolver
 14 |     {
 15 |         private const string USE_EMBEDDED_SERVER_KEY = "MCPForUnity.UseEmbeddedServer";
 16 | 
 17 |         /// <summary>
 18 |         /// Resolves the Python server directory path with comprehensive logic
 19 |         /// including development mode support and fallback mechanisms
 20 |         /// </summary>
 21 |         public static string FindPackagePythonDirectory(bool debugLogsEnabled = false)
 22 |         {
 23 |             string pythonDir = McpConfigFileHelper.ResolveServerSource();
 24 | 
 25 |             try
 26 |             {
 27 |                 // Only check dev paths if we're using a file-based package (development mode)
 28 |                 bool isDevelopmentMode = IsDevelopmentMode();
 29 |                 if (isDevelopmentMode)
 30 |                 {
 31 |                     string currentPackagePath = Path.GetDirectoryName(Application.dataPath);
 32 |                     string[] devPaths = {
 33 |                         Path.Combine(currentPackagePath, "unity-mcp", "UnityMcpServer", "src"),
 34 |                         Path.Combine(Path.GetDirectoryName(currentPackagePath), "unity-mcp", "UnityMcpServer", "src"),
 35 |                     };
 36 | 
 37 |                     foreach (string devPath in devPaths)
 38 |                     {
 39 |                         if (Directory.Exists(devPath) && File.Exists(Path.Combine(devPath, "server.py")))
 40 |                         {
 41 |                             if (debugLogsEnabled)
 42 |                             {
 43 |                                 Debug.Log($"Currently in development mode. Package: {devPath}");
 44 |                             }
 45 |                             return devPath;
 46 |                         }
 47 |                     }
 48 |                 }
 49 | 
 50 |                 // Resolve via shared helper (handles local registry and older fallback) only if dev override on
 51 |                 if (EditorPrefs.GetBool(USE_EMBEDDED_SERVER_KEY, false))
 52 |                 {
 53 |                     if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded))
 54 |                     {
 55 |                         return embedded;
 56 |                     }
 57 |                 }
 58 | 
 59 |                 // Log only if the resolved path does not actually contain server.py
 60 |                 if (debugLogsEnabled)
 61 |                 {
 62 |                     bool hasServer = false;
 63 |                     try { hasServer = File.Exists(Path.Combine(pythonDir, "server.py")); } catch { }
 64 |                     if (!hasServer)
 65 |                     {
 66 |                         Debug.LogWarning("Could not find Python directory with server.py; falling back to installed path");
 67 |                     }
 68 |                 }
 69 |             }
 70 |             catch (Exception e)
 71 |             {
 72 |                 Debug.LogError($"Error finding package path: {e.Message}");
 73 |             }
 74 | 
 75 |             return pythonDir;
 76 |         }
 77 | 
 78 |         /// <summary>
 79 |         /// Checks if the current Unity project is in development mode
 80 |         /// (i.e., the package is referenced as a local file path in manifest.json)
 81 |         /// </summary>
 82 |         private static bool IsDevelopmentMode()
 83 |         {
 84 |             try
 85 |             {
 86 |                 // Only treat as development if manifest explicitly references a local file path for the package
 87 |                 string manifestPath = Path.Combine(Application.dataPath, "..", "Packages", "manifest.json");
 88 |                 if (!File.Exists(manifestPath)) return false;
 89 | 
 90 |                 string manifestContent = File.ReadAllText(manifestPath);
 91 |                 // Look specifically for our package dependency set to a file: URL
 92 |                 // This avoids auto-enabling dev mode just because a repo exists elsewhere on disk
 93 |                 if (manifestContent.IndexOf("\"com.coplaydev.unity-mcp\"", StringComparison.OrdinalIgnoreCase) >= 0)
 94 |                 {
 95 |                     int idx = manifestContent.IndexOf("com.coplaydev.unity-mcp", StringComparison.OrdinalIgnoreCase);
 96 |                     // Crude but effective: check for "file:" in the same line/value
 97 |                     if (manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase) >= 0
 98 |                         && manifestContent.IndexOf("\n", idx, StringComparison.OrdinalIgnoreCase) > manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase))
 99 |                     {
100 |                         return true;
101 |                     }
102 |                 }
103 |                 return false;
104 |             }
105 |             catch
106 |             {
107 |                 return false;
108 |             }
109 |         }
110 | 
111 |         /// <summary>
112 |         /// Gets the appropriate PATH prepend for the current platform when running external processes
113 |         /// </summary>
114 |         public static string GetPathPrepend()
115 |         {
116 |             if (Application.platform == RuntimePlatform.OSXEditor)
117 |                 return "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
118 |             else if (Application.platform == RuntimePlatform.LinuxEditor)
119 |                 return "/usr/local/bin:/usr/bin:/bin";
120 |             return null;
121 |         }
122 |     }
123 | }
124 | 
```

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

```markdown
  1 | # MCP for Unity Telemetry
  2 | 
  3 | 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.
  4 | 
  5 | ## 🔒 Privacy First
  6 | 
  7 | - **Anonymous**: We use randomly generated UUIDs - no personal information
  8 | - **Non-blocking**: Telemetry never interferes with your Unity workflow  
  9 | - **Easy opt-out**: Simple environment variable or Unity Editor setting
 10 | - **Transparent**: All collected data types are documented here
 11 | 
 12 | ## 📊 What We Collect
 13 | 
 14 | ### Usage Analytics
 15 | - **Tool Usage**: Which MCP tools you use (manage_script, manage_scene, etc.)
 16 | - **Performance**: Execution times and success/failure rates
 17 | - **System Info**: Unity version, platform (Windows/Mac/Linux), MCP version
 18 | - **Milestones**: First-time usage events (first script creation, first tool use, etc.)
 19 | 
 20 | ### Technical Diagnostics  
 21 | - **Connection Events**: Bridge startup/connection success/failures
 22 | - **Error Reports**: Anonymized error messages (truncated to 200 chars)
 23 | - **Server Health**: Startup time, connection latency
 24 | 
 25 | ### What We **DON'T** Collect
 26 | - ❌ Your code or script contents
 27 | - ❌ Project names, file names, or paths
 28 | - ❌ Personal information or identifiers
 29 | - ❌ Sensitive project data
 30 | - ❌ IP addresses (beyond what's needed for HTTP requests)
 31 | 
 32 | ## 🚫 How to Opt Out
 33 | 
 34 | ### Method 1: Environment Variable (Recommended)
 35 | Set any of these environment variables to `true`:
 36 | 
 37 | ```bash
 38 | # Disable all telemetry
 39 | export DISABLE_TELEMETRY=true
 40 | 
 41 | # MCP for Unity specific
 42 | export UNITY_MCP_DISABLE_TELEMETRY=true
 43 | 
 44 | # MCP protocol wide  
 45 | export MCP_DISABLE_TELEMETRY=true
 46 | ```
 47 | 
 48 | ### Method 2: Unity Editor (Coming Soon)
 49 | In Unity Editor: `Window > MCP for Unity > Settings > Disable Telemetry`
 50 | 
 51 | ### Method 3: Manual Config
 52 | Add to your MCP client config:
 53 | ```json
 54 | {
 55 |   "env": {
 56 |     "DISABLE_TELEMETRY": "true"
 57 |   }
 58 | }
 59 | ```
 60 | 
 61 | ## 🔧 Technical Implementation
 62 | 
 63 | ### Architecture
 64 | - **Python Server**: Core telemetry collection and transmission
 65 | - **Unity Bridge**: Local event collection from Unity Editor
 66 | - **Anonymous UUIDs**: Generated per-installation for aggregate analytics
 67 | - **Thread-safe**: Non-blocking background transmission
 68 | - **Fail-safe**: Errors never interrupt your workflow
 69 | 
 70 | ### Data Storage
 71 | Telemetry data is stored locally in:
 72 | - **Windows**: `%APPDATA%\UnityMCP\`
 73 | - **macOS**: `~/Library/Application Support/UnityMCP/`  
 74 | - **Linux**: `~/.local/share/UnityMCP/`
 75 | 
 76 | Files created:
 77 | - `customer_uuid.txt`: Anonymous identifier
 78 | - `milestones.json`: One-time events tracker
 79 | 
 80 | ### Data Transmission
 81 | - **Endpoint**: `https://api-prod.coplay.dev/telemetry/events`
 82 | - **Method**: HTTPS POST with JSON payload
 83 | - **Retry**: Background thread with graceful failure
 84 | - **Timeout**: 10 second timeout, no retries on failure
 85 | 
 86 | ## 📈 How We Use This Data
 87 | 
 88 | ### Product Improvement
 89 | - **Feature Usage**: Understand which tools are most/least used
 90 | - **Performance**: Identify slow operations to optimize
 91 | - **Reliability**: Track error rates and connection issues
 92 | - **Compatibility**: Ensure Unity version compatibility
 93 | 
 94 | ### Development Priorities
 95 | - **Roadmap**: Focus development on most-used features
 96 | - **Bug Fixes**: Prioritize fixes based on error frequency
 97 | - **Platform Support**: Allocate resources based on platform usage
 98 | - **Documentation**: Improve docs for commonly problematic areas
 99 | 
100 | ### What We Don't Do
101 | - ❌ Sell data to third parties
102 | - ❌ Use data for advertising/marketing
103 | - ❌ Track individual developers
104 | - ❌ Store sensitive project information
105 | 
106 | ## 🛠️ For Developers
107 | 
108 | ### Testing Telemetry
109 | ```bash
110 | cd MCPForUnity/UnityMcpServer~/src
111 | python test_telemetry.py
112 | ```
113 | 
114 | ### Custom Telemetry Events
115 | ```python
116 | from telemetry import record_telemetry, RecordType
117 | 
118 | record_telemetry(RecordType.USAGE, {
119 |     "custom_event": "my_feature_used",
120 |     "metadata": "optional_data"
121 | })
122 | ```
123 | 
124 | ### Telemetry Status Check
125 | ```python  
126 | from telemetry import is_telemetry_enabled
127 | 
128 | if is_telemetry_enabled():
129 |     print("Telemetry is active")
130 | else:
131 |     print("Telemetry is disabled")
132 | ```
133 | 
134 | ## 📋 Data Retention Policy
135 | 
136 | - **Aggregated Data**: Retained indefinitely for product insights
137 | - **Raw Events**: Automatically purged after 90 days
138 | - **Personal Data**: None collected, so none to purge
139 | - **Opt-out**: Immediate - no data sent after opting out
140 | 
141 | ## 🤝 Contact & Transparency
142 | 
143 | - **Questions**: [Discord Community](https://discord.gg/y4p8KfzrN4)
144 | - **Issues**: [GitHub Issues](https://github.com/CoplayDev/unity-mcp/issues)
145 | - **Privacy Concerns**: Create a GitHub issue with "Privacy" label
146 | - **Source Code**: All telemetry code is open source in this repository
147 | 
148 | ## 📊 Example Telemetry Event
149 | 
150 | Here's what a typical telemetry event looks like:
151 | 
152 | ```json
153 | {
154 |   "record": "tool_execution",
155 |   "timestamp": 1704067200,
156 |   "customer_uuid": "550e8400-e29b-41d4-a716-446655440000", 
157 |   "session_id": "abc123-def456-ghi789",
158 |   "version": "3.0.2",
159 |   "platform": "posix",
160 |   "data": {
161 |     "tool_name": "manage_script",
162 |     "success": true,
163 |     "duration_ms": 42.5
164 |   }
165 | }
166 | ```
167 | 
168 | Notice:
169 | - ✅ Anonymous UUID (randomly generated)
170 | - ✅ Tool performance metrics  
171 | - ✅ Success/failure tracking
172 | - ❌ No code content
173 | - ❌ No project information
174 | - ❌ No personal data
175 | 
176 | ---
177 | 
178 | *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!*
179 | 
```

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

```csharp
  1 | using System;
  2 | using System.Diagnostics;
  3 | using System.IO;
  4 | using MCPForUnity.Editor.Dependencies.Models;
  5 | using MCPForUnity.Editor.Helpers;
  6 | 
  7 | namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
  8 | {
  9 |     /// <summary>
 10 |     /// Base class for platform-specific dependency detection
 11 |     /// </summary>
 12 |     public abstract class PlatformDetectorBase : IPlatformDetector
 13 |     {
 14 |         public abstract string PlatformName { get; }
 15 |         public abstract bool CanDetect { get; }
 16 | 
 17 |         public abstract DependencyStatus DetectPython();
 18 |         public abstract string GetPythonInstallUrl();
 19 |         public abstract string GetUVInstallUrl();
 20 |         public abstract string GetInstallationRecommendations();
 21 | 
 22 |         public virtual DependencyStatus DetectUV()
 23 |         {
 24 |             var status = new DependencyStatus("UV Package Manager", isRequired: true)
 25 |             {
 26 |                 InstallationHint = GetUVInstallUrl()
 27 |             };
 28 | 
 29 |             try
 30 |             {
 31 |                 // Use existing UV detection from ServerInstaller
 32 |                 string uvPath = ServerInstaller.FindUvPath();
 33 |                 if (!string.IsNullOrEmpty(uvPath))
 34 |                 {
 35 |                     if (TryValidateUV(uvPath, out string version))
 36 |                     {
 37 |                         status.IsAvailable = true;
 38 |                         status.Version = version;
 39 |                         status.Path = uvPath;
 40 |                         status.Details = $"Found UV {version} at {uvPath}";
 41 |                         return status;
 42 |                     }
 43 |                 }
 44 | 
 45 |                 status.ErrorMessage = "UV package manager not found. Please install UV.";
 46 |                 status.Details = "UV is required for managing Python dependencies.";
 47 |             }
 48 |             catch (Exception ex)
 49 |             {
 50 |                 status.ErrorMessage = $"Error detecting UV: {ex.Message}";
 51 |             }
 52 | 
 53 |             return status;
 54 |         }
 55 | 
 56 |         public virtual DependencyStatus DetectMCPServer()
 57 |         {
 58 |             var status = new DependencyStatus("MCP Server", isRequired: false);
 59 | 
 60 |             try
 61 |             {
 62 |                 // Check if server is installed
 63 |                 string serverPath = ServerInstaller.GetServerPath();
 64 |                 string serverPy = Path.Combine(serverPath, "server.py");
 65 | 
 66 |                 if (File.Exists(serverPy))
 67 |                 {
 68 |                     status.IsAvailable = true;
 69 |                     status.Path = serverPath;
 70 | 
 71 |                     // Try to get version
 72 |                     string versionFile = Path.Combine(serverPath, "server_version.txt");
 73 |                     if (File.Exists(versionFile))
 74 |                     {
 75 |                         status.Version = File.ReadAllText(versionFile).Trim();
 76 |                     }
 77 | 
 78 |                     status.Details = $"MCP Server found at {serverPath}";
 79 |                 }
 80 |                 else
 81 |                 {
 82 |                     // Check for embedded server
 83 |                     if (ServerPathResolver.TryFindEmbeddedServerSource(out string embeddedPath))
 84 |                     {
 85 |                         status.IsAvailable = true;
 86 |                         status.Path = embeddedPath;
 87 |                         status.Details = "MCP Server available (embedded in package)";
 88 |                     }
 89 |                     else
 90 |                     {
 91 |                         status.ErrorMessage = "MCP Server not found";
 92 |                         status.Details = "Server will be installed automatically when needed";
 93 |                     }
 94 |                 }
 95 |             }
 96 |             catch (Exception ex)
 97 |             {
 98 |                 status.ErrorMessage = $"Error detecting MCP Server: {ex.Message}";
 99 |             }
100 | 
101 |             return status;
102 |         }
103 | 
104 |         protected bool TryValidateUV(string uvPath, out string version)
105 |         {
106 |             version = null;
107 | 
108 |             try
109 |             {
110 |                 var psi = new ProcessStartInfo
111 |                 {
112 |                     FileName = uvPath,
113 |                     Arguments = "--version",
114 |                     UseShellExecute = false,
115 |                     RedirectStandardOutput = true,
116 |                     RedirectStandardError = true,
117 |                     CreateNoWindow = true
118 |                 };
119 | 
120 |                 using var process = Process.Start(psi);
121 |                 if (process == null) return false;
122 | 
123 |                 string output = process.StandardOutput.ReadToEnd().Trim();
124 |                 process.WaitForExit(5000);
125 | 
126 |                 if (process.ExitCode == 0 && output.StartsWith("uv "))
127 |                 {
128 |                     version = output.Substring(3); // Remove "uv " prefix
129 |                     return true;
130 |                 }
131 |             }
132 |             catch
133 |             {
134 |                 // Ignore validation errors
135 |             }
136 | 
137 |             return false;
138 |         }
139 | 
140 |         protected bool TryParseVersion(string version, out int major, out int minor)
141 |         {
142 |             major = 0;
143 |             minor = 0;
144 | 
145 |             try
146 |             {
147 |                 var parts = version.Split('.');
148 |                 if (parts.Length >= 2)
149 |                 {
150 |                     return int.TryParse(parts[0], out major) && int.TryParse(parts[1], out minor);
151 |                 }
152 |             }
153 |             catch
154 |             {
155 |                 // Ignore parsing errors
156 |             }
157 | 
158 |             return false;
159 |         }
160 |     }
161 | }
162 | 
```

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

```csharp
  1 | using System;
  2 | using System.Diagnostics;
  3 | using System.IO;
  4 | using MCPForUnity.Editor.Dependencies.Models;
  5 | using MCPForUnity.Editor.Helpers;
  6 | 
  7 | namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
  8 | {
  9 |     /// <summary>
 10 |     /// Base class for platform-specific dependency detection
 11 |     /// </summary>
 12 |     public abstract class PlatformDetectorBase : IPlatformDetector
 13 |     {
 14 |         public abstract string PlatformName { get; }
 15 |         public abstract bool CanDetect { get; }
 16 | 
 17 |         public abstract DependencyStatus DetectPython();
 18 |         public abstract string GetPythonInstallUrl();
 19 |         public abstract string GetUVInstallUrl();
 20 |         public abstract string GetInstallationRecommendations();
 21 | 
 22 |         public virtual DependencyStatus DetectUV()
 23 |         {
 24 |             var status = new DependencyStatus("UV Package Manager", isRequired: true)
 25 |             {
 26 |                 InstallationHint = GetUVInstallUrl()
 27 |             };
 28 | 
 29 |             try
 30 |             {
 31 |                 // Use existing UV detection from ServerInstaller
 32 |                 string uvPath = ServerInstaller.FindUvPath();
 33 |                 if (!string.IsNullOrEmpty(uvPath))
 34 |                 {
 35 |                     if (TryValidateUV(uvPath, out string version))
 36 |                     {
 37 |                         status.IsAvailable = true;
 38 |                         status.Version = version;
 39 |                         status.Path = uvPath;
 40 |                         status.Details = $"Found UV {version} at {uvPath}";
 41 |                         return status;
 42 |                     }
 43 |                 }
 44 | 
 45 |                 status.ErrorMessage = "UV package manager not found. Please install UV.";
 46 |                 status.Details = "UV is required for managing Python dependencies.";
 47 |             }
 48 |             catch (Exception ex)
 49 |             {
 50 |                 status.ErrorMessage = $"Error detecting UV: {ex.Message}";
 51 |             }
 52 | 
 53 |             return status;
 54 |         }
 55 | 
 56 |         public virtual DependencyStatus DetectMCPServer()
 57 |         {
 58 |             var status = new DependencyStatus("MCP Server", isRequired: false);
 59 | 
 60 |             try
 61 |             {
 62 |                 // Check if server is installed
 63 |                 string serverPath = ServerInstaller.GetServerPath();
 64 |                 string serverPy = Path.Combine(serverPath, "server.py");
 65 | 
 66 |                 if (File.Exists(serverPy))
 67 |                 {
 68 |                     status.IsAvailable = true;
 69 |                     status.Path = serverPath;
 70 | 
 71 |                     // Try to get version
 72 |                     string versionFile = Path.Combine(serverPath, "server_version.txt");
 73 |                     if (File.Exists(versionFile))
 74 |                     {
 75 |                         status.Version = File.ReadAllText(versionFile).Trim();
 76 |                     }
 77 | 
 78 |                     status.Details = $"MCP Server found at {serverPath}";
 79 |                 }
 80 |                 else
 81 |                 {
 82 |                     // Check for embedded server
 83 |                     if (ServerPathResolver.TryFindEmbeddedServerSource(out string embeddedPath))
 84 |                     {
 85 |                         status.IsAvailable = true;
 86 |                         status.Path = embeddedPath;
 87 |                         status.Details = "MCP Server available (embedded in package)";
 88 |                     }
 89 |                     else
 90 |                     {
 91 |                         status.ErrorMessage = "MCP Server not found";
 92 |                         status.Details = "Server will be installed automatically when needed";
 93 |                     }
 94 |                 }
 95 |             }
 96 |             catch (Exception ex)
 97 |             {
 98 |                 status.ErrorMessage = $"Error detecting MCP Server: {ex.Message}";
 99 |             }
100 | 
101 |             return status;
102 |         }
103 | 
104 |         protected bool TryValidateUV(string uvPath, out string version)
105 |         {
106 |             version = null;
107 | 
108 |             try
109 |             {
110 |                 var psi = new ProcessStartInfo
111 |                 {
112 |                     FileName = uvPath,
113 |                     Arguments = "--version",
114 |                     UseShellExecute = false,
115 |                     RedirectStandardOutput = true,
116 |                     RedirectStandardError = true,
117 |                     CreateNoWindow = true
118 |                 };
119 | 
120 |                 using var process = Process.Start(psi);
121 |                 if (process == null) return false;
122 | 
123 |                 string output = process.StandardOutput.ReadToEnd().Trim();
124 |                 process.WaitForExit(5000);
125 | 
126 |                 if (process.ExitCode == 0 && output.StartsWith("uv "))
127 |                 {
128 |                     version = output.Substring(3); // Remove "uv " prefix
129 |                     return true;
130 |                 }
131 |             }
132 |             catch
133 |             {
134 |                 // Ignore validation errors
135 |             }
136 | 
137 |             return false;
138 |         }
139 | 
140 |         protected bool TryParseVersion(string version, out int major, out int minor)
141 |         {
142 |             major = 0;
143 |             minor = 0;
144 | 
145 |             try
146 |             {
147 |                 var parts = version.Split('.');
148 |                 if (parts.Length >= 2)
149 |                 {
150 |                     return int.TryParse(parts[0], out major) && int.TryParse(parts[1], out minor);
151 |                 }
152 |             }
153 |             catch
154 |             {
155 |                 // Ignore parsing errors
156 |             }
157 | 
158 |             return false;
159 |         }
160 |     }
161 | }
162 | 
```

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

```csharp
  1 | using System;
  2 | using System.IO;
  3 | using UnityEditor;
  4 | using UnityEngine;
  5 | 
  6 | namespace MCPForUnity.Editor.Helpers
  7 | {
  8 |     public static class ServerPathResolver
  9 |     {
 10 |         /// <summary>
 11 |         /// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package
 12 |         /// or common development locations. Returns true if found and sets srcPath to the folder
 13 |         /// containing server.py.
 14 |         /// </summary>
 15 |         public static bool TryFindEmbeddedServerSource(out string srcPath)
 16 |         {
 17 |             // 1) Repo development layouts commonly used alongside this package
 18 |             try
 19 |             {
 20 |                 string projectRoot = Path.GetDirectoryName(Application.dataPath);
 21 |                 string[] devCandidates =
 22 |                 {
 23 |                     Path.Combine(projectRoot ?? string.Empty, "unity-mcp", "UnityMcpServer", "src"),
 24 |                     Path.Combine(projectRoot ?? string.Empty, "..", "unity-mcp", "UnityMcpServer", "src"),
 25 |                 };
 26 |                 foreach (string candidate in devCandidates)
 27 |                 {
 28 |                     string full = Path.GetFullPath(candidate);
 29 |                     if (Directory.Exists(full) && File.Exists(Path.Combine(full, "server.py")))
 30 |                     {
 31 |                         srcPath = full;
 32 |                         return true;
 33 |                     }
 34 |                 }
 35 |             }
 36 |             catch { /* ignore */ }
 37 | 
 38 |             // 2) Resolve via local package info (no network). Fall back to Client.List on older editors.
 39 |             try
 40 |             {
 41 | #if UNITY_2021_2_OR_NEWER
 42 |                 // Primary: the package that owns this assembly
 43 |                 var owner = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(ServerPathResolver).Assembly);
 44 |                 if (owner != null)
 45 |                 {
 46 |                     if (TryResolveWithinPackage(owner, out srcPath))
 47 |                     {
 48 |                         return true;
 49 |                     }
 50 |                 }
 51 | 
 52 |                 // Secondary: scan all registered packages locally
 53 |                 foreach (var p in UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages())
 54 |                 {
 55 |                     if (TryResolveWithinPackage(p, out srcPath))
 56 |                     {
 57 |                         return true;
 58 |                     }
 59 |                 }
 60 | #else
 61 |                 // Older Unity versions: use Package Manager Client.List as a fallback
 62 |                 var list = UnityEditor.PackageManager.Client.List();
 63 |                 while (!list.IsCompleted) { }
 64 |                 if (list.Status == UnityEditor.PackageManager.StatusCode.Success)
 65 |                 {
 66 |                     foreach (var pkg in list.Result)
 67 |                     {
 68 |                         if (TryResolveWithinPackage(pkg, out srcPath))
 69 |                         {
 70 |                             return true;
 71 |                         }
 72 |                     }
 73 |                 }
 74 | #endif
 75 |             }
 76 |             catch { /* ignore */ }
 77 | 
 78 |             // 3) Fallback to previous common install locations
 79 |             try
 80 |             {
 81 |                 string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
 82 |                 string[] candidates =
 83 |                 {
 84 |                     Path.Combine(home, "unity-mcp", "UnityMcpServer", "src"),
 85 |                     Path.Combine(home, "Applications", "UnityMCP", "UnityMcpServer", "src"),
 86 |                 };
 87 |                 foreach (string candidate in candidates)
 88 |                 {
 89 |                     if (Directory.Exists(candidate) && File.Exists(Path.Combine(candidate, "server.py")))
 90 |                     {
 91 |                         srcPath = candidate;
 92 |                         return true;
 93 |                     }
 94 |                 }
 95 |             }
 96 |             catch { /* ignore */ }
 97 | 
 98 |             srcPath = null;
 99 |             return false;
100 |         }
101 | 
102 |         private static bool TryResolveWithinPackage(UnityEditor.PackageManager.PackageInfo p, out string srcPath)
103 |         {
104 |             const string CurrentId = "com.coplaydev.unity-mcp";
105 | 
106 |             srcPath = null;
107 |             if (p == null || p.name != CurrentId)
108 |             {
109 |                 return false;
110 |             }
111 | 
112 |             string packagePath = p.resolvedPath;
113 | 
114 |             // Preferred tilde folder (embedded but excluded from import)
115 |             string embeddedTilde = Path.Combine(packagePath, "UnityMcpServer~", "src");
116 |             if (Directory.Exists(embeddedTilde) && File.Exists(Path.Combine(embeddedTilde, "server.py")))
117 |             {
118 |                 srcPath = embeddedTilde;
119 |                 return true;
120 |             }
121 | 
122 |             // Legacy non-tilde folder
123 |             string embedded = Path.Combine(packagePath, "UnityMcpServer", "src");
124 |             if (Directory.Exists(embedded) && File.Exists(Path.Combine(embedded, "server.py")))
125 |             {
126 |                 srcPath = embedded;
127 |                 return true;
128 |             }
129 | 
130 |             // Dev-linked sibling of the package folder
131 |             string sibling = Path.Combine(Path.GetDirectoryName(packagePath) ?? string.Empty, "UnityMcpServer", "src");
132 |             if (Directory.Exists(sibling) && File.Exists(Path.Combine(sibling, "server.py")))
133 |             {
134 |                 srcPath = sibling;
135 |                 return true;
136 |             }
137 | 
138 |             return false;
139 |         }
140 |     }
141 | }
142 | 
```

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

```csharp
  1 | using System;
  2 | using System.IO;
  3 | using UnityEditor;
  4 | using UnityEngine;
  5 | 
  6 | namespace MCPForUnity.Editor.Helpers
  7 | {
  8 |     public static class ServerPathResolver
  9 |     {
 10 |         /// <summary>
 11 |         /// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package
 12 |         /// or common development locations. Returns true if found and sets srcPath to the folder
 13 |         /// containing server.py.
 14 |         /// </summary>
 15 |         public static bool TryFindEmbeddedServerSource(out string srcPath)
 16 |         {
 17 |             // 1) Repo development layouts commonly used alongside this package
 18 |             try
 19 |             {
 20 |                 string projectRoot = Path.GetDirectoryName(Application.dataPath);
 21 |                 string[] devCandidates =
 22 |                 {
 23 |                     Path.Combine(projectRoot ?? string.Empty, "unity-mcp", "UnityMcpServer", "src"),
 24 |                     Path.Combine(projectRoot ?? string.Empty, "..", "unity-mcp", "UnityMcpServer", "src"),
 25 |                 };
 26 |                 foreach (string candidate in devCandidates)
 27 |                 {
 28 |                     string full = Path.GetFullPath(candidate);
 29 |                     if (Directory.Exists(full) && File.Exists(Path.Combine(full, "server.py")))
 30 |                     {
 31 |                         srcPath = full;
 32 |                         return true;
 33 |                     }
 34 |                 }
 35 |             }
 36 |             catch { /* ignore */ }
 37 | 
 38 |             // 2) Resolve via local package info (no network). Fall back to Client.List on older editors.
 39 |             try
 40 |             {
 41 | #if UNITY_2021_2_OR_NEWER
 42 |                 // Primary: the package that owns this assembly
 43 |                 var owner = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(ServerPathResolver).Assembly);
 44 |                 if (owner != null)
 45 |                 {
 46 |                     if (TryResolveWithinPackage(owner, out srcPath))
 47 |                     {
 48 |                         return true;
 49 |                     }
 50 |                 }
 51 | 
 52 |                 // Secondary: scan all registered packages locally
 53 |                 foreach (var p in UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages())
 54 |                 {
 55 |                     if (TryResolveWithinPackage(p, out srcPath))
 56 |                     {
 57 |                         return true;
 58 |                     }
 59 |                 }
 60 | #else
 61 |                 // Older Unity versions: use Package Manager Client.List as a fallback
 62 |                 var list = UnityEditor.PackageManager.Client.List();
 63 |                 while (!list.IsCompleted) { }
 64 |                 if (list.Status == UnityEditor.PackageManager.StatusCode.Success)
 65 |                 {
 66 |                     foreach (var pkg in list.Result)
 67 |                     {
 68 |                         if (TryResolveWithinPackage(pkg, out srcPath))
 69 |                         {
 70 |                             return true;
 71 |                         }
 72 |                     }
 73 |                 }
 74 | #endif
 75 |             }
 76 |             catch { /* ignore */ }
 77 | 
 78 |             // 3) Fallback to previous common install locations
 79 |             try
 80 |             {
 81 |                 string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
 82 |                 string[] candidates =
 83 |                 {
 84 |                     Path.Combine(home, "unity-mcp", "UnityMcpServer", "src"),
 85 |                     Path.Combine(home, "Applications", "UnityMCP", "UnityMcpServer", "src"),
 86 |                 };
 87 |                 foreach (string candidate in candidates)
 88 |                 {
 89 |                     if (Directory.Exists(candidate) && File.Exists(Path.Combine(candidate, "server.py")))
 90 |                     {
 91 |                         srcPath = candidate;
 92 |                         return true;
 93 |                     }
 94 |                 }
 95 |             }
 96 |             catch { /* ignore */ }
 97 | 
 98 |             srcPath = null;
 99 |             return false;
100 |         }
101 | 
102 |         private static bool TryResolveWithinPackage(UnityEditor.PackageManager.PackageInfo p, out string srcPath)
103 |         {
104 |             const string CurrentId = "com.coplaydev.unity-mcp";
105 | 
106 |             srcPath = null;
107 |             if (p == null || p.name != CurrentId)
108 |             {
109 |                 return false;
110 |             }
111 | 
112 |             string packagePath = p.resolvedPath;
113 | 
114 |             // Preferred tilde folder (embedded but excluded from import)
115 |             string embeddedTilde = Path.Combine(packagePath, "UnityMcpServer~", "src");
116 |             if (Directory.Exists(embeddedTilde) && File.Exists(Path.Combine(embeddedTilde, "server.py")))
117 |             {
118 |                 srcPath = embeddedTilde;
119 |                 return true;
120 |             }
121 | 
122 |             // Legacy non-tilde folder
123 |             string embedded = Path.Combine(packagePath, "UnityMcpServer", "src");
124 |             if (Directory.Exists(embedded) && File.Exists(Path.Combine(embedded, "server.py")))
125 |             {
126 |                 srcPath = embedded;
127 |                 return true;
128 |             }
129 | 
130 |             // Dev-linked sibling of the package folder
131 |             string sibling = Path.Combine(Path.GetDirectoryName(packagePath) ?? string.Empty, "UnityMcpServer", "src");
132 |             if (Directory.Exists(sibling) && File.Exists(Path.Combine(sibling, "server.py")))
133 |             {
134 |                 srcPath = sibling;
135 |                 return true;
136 |             }
137 | 
138 |             return false;
139 |         }
140 |     }
141 | }
142 | 
```

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

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | Generic helper to switch the MCP for Unity package source in a Unity project's
  4 | Packages/manifest.json.  This is useful for switching between upstream and local repos while working on the MCP.
  5 | 
  6 | Usage:
  7 |   python mcp_source.py [--manifest /abs/path/to/manifest.json] [--repo /abs/path/to/unity-mcp] [--choice 1|2|3]
  8 | 
  9 | Choices:
 10 |   1) Upstream main (CoplayDev/unity-mcp)
 11 |   2) Your remote current branch (derived from `origin` and current branch)
 12 |   3) Local repo workspace (file: URL to MCPForUnity in your checkout)
 13 | """
 14 | 
 15 | from __future__ import annotations
 16 | 
 17 | import argparse
 18 | import json
 19 | import pathlib
 20 | import subprocess
 21 | import sys
 22 | from typing import Optional
 23 | 
 24 | PKG_NAME = "com.coplaydev.unity-mcp"
 25 | BRIDGE_SUBPATH = "MCPForUnity"
 26 | 
 27 | 
 28 | def run_git(repo: pathlib.Path, *args: str) -> str:
 29 |     result = subprocess.run([
 30 |         "git", "-C", str(repo), *args
 31 |     ], capture_output=True, text=True)
 32 |     if result.returncode != 0:
 33 |         raise RuntimeError(result.stderr.strip()
 34 |                            or f"git {' '.join(args)} failed")
 35 |     return result.stdout.strip()
 36 | 
 37 | 
 38 | def normalize_origin_to_https(url: str) -> str:
 39 |     """Map common SSH origin forms to https for Unity's git URL scheme."""
 40 |     if url.startswith("[email protected]:"):
 41 |         owner_repo = url.split(":", 1)[1]
 42 |         if owner_repo.endswith(".git"):
 43 |             owner_repo = owner_repo[:-4]
 44 |         return f"https://github.com/{owner_repo}.git"
 45 |     # already https or file: etc.
 46 |     return url
 47 | 
 48 | 
 49 | def detect_repo_root(explicit: Optional[str]) -> pathlib.Path:
 50 |     if explicit:
 51 |         return pathlib.Path(explicit).resolve()
 52 |     # Prefer the git toplevel from the script's directory
 53 |     here = pathlib.Path(__file__).resolve().parent
 54 |     try:
 55 |         top = run_git(here, "rev-parse", "--show-toplevel")
 56 |         return pathlib.Path(top)
 57 |     except Exception:
 58 |         return here
 59 | 
 60 | 
 61 | def detect_branch(repo: pathlib.Path) -> str:
 62 |     return run_git(repo, "rev-parse", "--abbrev-ref", "HEAD")
 63 | 
 64 | 
 65 | def detect_origin(repo: pathlib.Path) -> str:
 66 |     url = run_git(repo, "remote", "get-url", "origin")
 67 |     return normalize_origin_to_https(url)
 68 | 
 69 | 
 70 | def find_manifest(explicit: Optional[str]) -> pathlib.Path:
 71 |     if explicit:
 72 |         return pathlib.Path(explicit).resolve()
 73 |     # Walk up from CWD looking for Packages/manifest.json
 74 |     cur = pathlib.Path.cwd().resolve()
 75 |     for parent in [cur, *cur.parents]:
 76 |         candidate = parent / "Packages" / "manifest.json"
 77 |         if candidate.exists():
 78 |             return candidate
 79 |     raise FileNotFoundError(
 80 |         "Could not find Packages/manifest.json from current directory. Use --manifest to specify a path.")
 81 | 
 82 | 
 83 | def read_json(path: pathlib.Path) -> dict:
 84 |     with path.open("r", encoding="utf-8") as f:
 85 |         return json.load(f)
 86 | 
 87 | 
 88 | def write_json(path: pathlib.Path, data: dict) -> None:
 89 |     with path.open("w", encoding="utf-8") as f:
 90 |         json.dump(data, f, indent=2)
 91 |         f.write("\n")
 92 | 
 93 | 
 94 | def build_options(repo_root: pathlib.Path, branch: str, origin_https: str):
 95 |     upstream = "git+https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity"
 96 |     # Ensure origin is https
 97 |     origin = origin_https
 98 |     # If origin is a local file path or non-https, try to coerce to https github if possible
 99 |     if origin.startswith("file:"):
100 |         # Not meaningful for remote option; keep upstream
101 |         origin_remote = upstream
102 |     else:
103 |         origin_remote = origin
104 |     return [
105 |         ("[1] Upstream main", upstream),
106 |         ("[2] Remote current branch",
107 |          f"{origin_remote}?path=/{BRIDGE_SUBPATH}#{branch}"),
108 |         ("[3] Local workspace",
109 |          f"file:{(repo_root / BRIDGE_SUBPATH).as_posix()}"),
110 |     ]
111 | 
112 | 
113 | def parse_args() -> argparse.Namespace:
114 |     p = argparse.ArgumentParser(
115 |         description="Switch MCP for Unity package source")
116 |     p.add_argument("--manifest", help="Path to Packages/manifest.json")
117 |     p.add_argument(
118 |         "--repo", help="Path to unity-mcp repo root (for local file option)")
119 |     p.add_argument(
120 |         "--choice", choices=["1", "2", "3"], help="Pick option non-interactively")
121 |     return p.parse_args()
122 | 
123 | 
124 | def main() -> None:
125 |     args = parse_args()
126 |     try:
127 |         repo_root = detect_repo_root(args.repo)
128 |         branch = detect_branch(repo_root)
129 |         origin = detect_origin(repo_root)
130 |     except Exception as e:
131 |         print(f"Error: {e}", file=sys.stderr)
132 |         sys.exit(1)
133 | 
134 |     options = build_options(repo_root, branch, origin)
135 | 
136 |     try:
137 |         manifest_path = find_manifest(args.manifest)
138 |     except Exception as e:
139 |         print(f"Error: {e}", file=sys.stderr)
140 |         sys.exit(1)
141 | 
142 |     print("Select MCP package source by number:")
143 |     for label, _ in options:
144 |         print(label)
145 | 
146 |     if args.choice:
147 |         choice = args.choice
148 |     else:
149 |         choice = input("Enter 1-3: ").strip()
150 | 
151 |     if choice not in {"1", "2", "3"}:
152 |         print("Invalid selection.", file=sys.stderr)
153 |         sys.exit(1)
154 | 
155 |     idx = int(choice) - 1
156 |     _, chosen = options[idx]
157 | 
158 |     data = read_json(manifest_path)
159 |     deps = data.get("dependencies", {})
160 |     if PKG_NAME not in deps:
161 |         print(
162 |             f"Error: '{PKG_NAME}' not found in manifest dependencies.", file=sys.stderr)
163 |         sys.exit(1)
164 | 
165 |     print(f"\nUpdating {PKG_NAME} → {chosen}")
166 |     deps[PKG_NAME] = chosen
167 |     data["dependencies"] = deps
168 |     write_json(manifest_path, data)
169 |     print(f"Done. Wrote to: {manifest_path}")
170 |     print("Tip: In Unity, open Package Manager and Refresh to re-resolve packages.")
171 | 
172 | 
173 | if __name__ == "__main__":
174 |     main()
175 | 
```

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

```csharp
  1 | using System;
  2 | using System.Linq;
  3 | using NUnit.Framework;
  4 | using UnityEngine;
  5 | using MCPForUnity.Editor.Data;
  6 | 
  7 | namespace MCPForUnityTests.Editor.Data
  8 | {
  9 |     public class PythonToolsAssetTests
 10 |     {
 11 |         private PythonToolsAsset _asset;
 12 | 
 13 |         [SetUp]
 14 |         public void SetUp()
 15 |         {
 16 |             _asset = ScriptableObject.CreateInstance<PythonToolsAsset>();
 17 |         }
 18 | 
 19 |         [TearDown]
 20 |         public void TearDown()
 21 |         {
 22 |             if (_asset != null)
 23 |             {
 24 |                 UnityEngine.Object.DestroyImmediate(_asset, true);
 25 |             }
 26 |         }
 27 | 
 28 |         [Test]
 29 |         public void GetValidFiles_ReturnsEmptyList_WhenNoFilesAdded()
 30 |         {
 31 |             var validFiles = _asset.GetValidFiles().ToList();
 32 | 
 33 |             Assert.IsEmpty(validFiles, "Should return empty list when no files added");
 34 |         }
 35 | 
 36 |         [Test]
 37 |         public void GetValidFiles_FiltersOutNullReferences()
 38 |         {
 39 |             _asset.pythonFiles.Add(null);
 40 |             _asset.pythonFiles.Add(new TextAsset("print('test')"));
 41 |             _asset.pythonFiles.Add(null);
 42 | 
 43 |             var validFiles = _asset.GetValidFiles().ToList();
 44 | 
 45 |             Assert.AreEqual(1, validFiles.Count, "Should filter out null references");
 46 |         }
 47 | 
 48 |         [Test]
 49 |         public void GetValidFiles_ReturnsAllNonNullFiles()
 50 |         {
 51 |             var file1 = new TextAsset("print('test1')");
 52 |             var file2 = new TextAsset("print('test2')");
 53 | 
 54 |             _asset.pythonFiles.Add(file1);
 55 |             _asset.pythonFiles.Add(file2);
 56 | 
 57 |             var validFiles = _asset.GetValidFiles().ToList();
 58 | 
 59 |             Assert.AreEqual(2, validFiles.Count, "Should return all non-null files");
 60 |             CollectionAssert.Contains(validFiles, file1);
 61 |             CollectionAssert.Contains(validFiles, file2);
 62 |         }
 63 | 
 64 |         [Test]
 65 |         public void NeedsSync_ReturnsTrue_WhenHashingDisabled()
 66 |         {
 67 |             _asset.useContentHashing = false;
 68 |             var textAsset = new TextAsset("print('test')");
 69 | 
 70 |             bool needsSync = _asset.NeedsSync(textAsset, "any_hash");
 71 | 
 72 |             Assert.IsTrue(needsSync, "Should always need sync when hashing disabled");
 73 |         }
 74 | 
 75 |         [Test]
 76 |         public void NeedsSync_ReturnsTrue_WhenFileNotInStates()
 77 |         {
 78 |             _asset.useContentHashing = true;
 79 |             var textAsset = new TextAsset("print('test')");
 80 | 
 81 |             bool needsSync = _asset.NeedsSync(textAsset, "new_hash");
 82 | 
 83 |             Assert.IsTrue(needsSync, "Should need sync for new file");
 84 |         }
 85 | 
 86 |         [Test]
 87 |         public void NeedsSync_ReturnsFalse_WhenHashMatches()
 88 |         {
 89 |             _asset.useContentHashing = true;
 90 |             var textAsset = new TextAsset("print('test')");
 91 |             string hash = "test_hash_123";
 92 | 
 93 |             // Record the file with a hash
 94 |             _asset.RecordSync(textAsset, hash);
 95 | 
 96 |             // Check if needs sync with same hash
 97 |             bool needsSync = _asset.NeedsSync(textAsset, hash);
 98 | 
 99 |             Assert.IsFalse(needsSync, "Should not need sync when hash matches");
100 |         }
101 | 
102 |         [Test]
103 |         public void NeedsSync_ReturnsTrue_WhenHashDiffers()
104 |         {
105 |             _asset.useContentHashing = true;
106 |             var textAsset = new TextAsset("print('test')");
107 | 
108 |             // Record with one hash
109 |             _asset.RecordSync(textAsset, "old_hash");
110 | 
111 |             // Check with different hash
112 |             bool needsSync = _asset.NeedsSync(textAsset, "new_hash");
113 | 
114 |             Assert.IsTrue(needsSync, "Should need sync when hash differs");
115 |         }
116 | 
117 |         [Test]
118 |         public void RecordSync_AddsNewFileState()
119 |         {
120 |             var textAsset = new TextAsset("print('test')");
121 |             string hash = "test_hash";
122 | 
123 |             _asset.RecordSync(textAsset, hash);
124 | 
125 |             Assert.AreEqual(1, _asset.fileStates.Count, "Should add one file state");
126 |             Assert.AreEqual(hash, _asset.fileStates[0].contentHash, "Should store the hash");
127 |             Assert.IsNotNull(_asset.fileStates[0].assetGuid, "Should store the GUID");
128 |         }
129 | 
130 |         [Test]
131 |         public void RecordSync_UpdatesExistingFileState()
132 |         {
133 |             var textAsset = new TextAsset("print('test')");
134 | 
135 |             // Record first time
136 |             _asset.RecordSync(textAsset, "hash1");
137 |             var firstTime = _asset.fileStates[0].lastSyncTime;
138 | 
139 |             // Wait a tiny bit to ensure time difference
140 |             System.Threading.Thread.Sleep(10);
141 | 
142 |             // Record second time with different hash
143 |             _asset.RecordSync(textAsset, "hash2");
144 | 
145 |             Assert.AreEqual(1, _asset.fileStates.Count, "Should still have only one state");
146 |             Assert.AreEqual("hash2", _asset.fileStates[0].contentHash, "Should update the hash");
147 |             Assert.Greater(_asset.fileStates[0].lastSyncTime, firstTime, "Should update sync time");
148 |         }
149 | 
150 |         [Test]
151 |         public void CleanupStaleStates_KeepsStatesForCurrentFiles()
152 |         {
153 |             var file1 = new TextAsset("print('test1')");
154 | 
155 |             _asset.pythonFiles.Add(file1);
156 |             _asset.RecordSync(file1, "hash1");
157 | 
158 |             _asset.CleanupStaleStates();
159 | 
160 |             Assert.AreEqual(1, _asset.fileStates.Count, "Should keep state for current file");
161 |         }
162 | 
163 |         [Test]
164 |         public void CleanupStaleStates_HandlesEmptyFilesList()
165 |         {
166 |             // Add some states without corresponding files
167 |             _asset.fileStates.Add(new PythonFileState
168 |             {
169 |                 assetGuid = "fake_guid_1",
170 |                 contentHash = "hash1",
171 |                 fileName = "test1.py",
172 |                 lastSyncTime = DateTime.UtcNow
173 |             });
174 | 
175 |             _asset.CleanupStaleStates();
176 | 
177 |             Assert.IsEmpty(_asset.fileStates, "Should remove all states when no files exist");
178 |         }
179 |     }
180 | }
181 | 
```

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

```python
  1 | """
  2 | Port discovery utility for MCP for Unity Server.
  3 | 
  4 | What changed and why:
  5 | - Unity now writes a per-project port file named like
  6 |   `~/.unity-mcp/unity-mcp-port-<hash>.json` to avoid projects overwriting
  7 |   each other's saved port. The legacy file `unity-mcp-port.json` may still
  8 |   exist.
  9 | - This module now scans for both patterns, prefers the most recently
 10 |   modified file, and verifies that the port is actually a MCP for Unity listener
 11 |   (quick socket connect + ping) before choosing it.
 12 | """
 13 | 
 14 | import glob
 15 | import json
 16 | import logging
 17 | from pathlib import Path
 18 | import socket
 19 | from typing import Optional, List
 20 | 
 21 | logger = logging.getLogger("mcp-for-unity-server")
 22 | 
 23 | 
 24 | class PortDiscovery:
 25 |     """Handles port discovery from Unity Bridge registry"""
 26 |     REGISTRY_FILE = "unity-mcp-port.json"  # legacy single-project file
 27 |     DEFAULT_PORT = 6400
 28 |     CONNECT_TIMEOUT = 0.3  # seconds, keep this snappy during discovery
 29 | 
 30 |     @staticmethod
 31 |     def get_registry_path() -> Path:
 32 |         """Get the path to the port registry file"""
 33 |         return Path.home() / ".unity-mcp" / PortDiscovery.REGISTRY_FILE
 34 | 
 35 |     @staticmethod
 36 |     def get_registry_dir() -> Path:
 37 |         return Path.home() / ".unity-mcp"
 38 | 
 39 |     @staticmethod
 40 |     def list_candidate_files() -> List[Path]:
 41 |         """Return candidate registry files, newest first.
 42 |         Includes hashed per-project files and the legacy file (if present).
 43 |         """
 44 |         base = PortDiscovery.get_registry_dir()
 45 |         hashed = sorted(
 46 |             (Path(p) for p in glob.glob(str(base / "unity-mcp-port-*.json"))),
 47 |             key=lambda p: p.stat().st_mtime,
 48 |             reverse=True,
 49 |         )
 50 |         legacy = PortDiscovery.get_registry_path()
 51 |         if legacy.exists():
 52 |             # Put legacy at the end so hashed, per-project files win
 53 |             hashed.append(legacy)
 54 |         return hashed
 55 | 
 56 |     @staticmethod
 57 |     def _try_probe_unity_mcp(port: int) -> bool:
 58 |         """Quickly check if a MCP for Unity listener is on this port.
 59 |         Tries a short TCP connect, sends 'ping', expects a JSON 'pong'.
 60 |         """
 61 |         try:
 62 |             with socket.create_connection(("127.0.0.1", port), PortDiscovery.CONNECT_TIMEOUT) as s:
 63 |                 s.settimeout(PortDiscovery.CONNECT_TIMEOUT)
 64 |                 try:
 65 |                     s.sendall(b"ping")
 66 |                     data = s.recv(512)
 67 |                     # Minimal validation: look for a success pong response
 68 |                     if data and b'"message":"pong"' in data:
 69 |                         return True
 70 |                 except Exception:
 71 |                     return False
 72 |         except Exception:
 73 |             return False
 74 |         return False
 75 | 
 76 |     @staticmethod
 77 |     def _read_latest_status() -> Optional[dict]:
 78 |         try:
 79 |             base = PortDiscovery.get_registry_dir()
 80 |             status_files = sorted(
 81 |                 (Path(p)
 82 |                  for p in glob.glob(str(base / "unity-mcp-status-*.json"))),
 83 |                 key=lambda p: p.stat().st_mtime,
 84 |                 reverse=True,
 85 |             )
 86 |             if not status_files:
 87 |                 return None
 88 |             with status_files[0].open('r') as f:
 89 |                 return json.load(f)
 90 |         except Exception:
 91 |             return None
 92 | 
 93 |     @staticmethod
 94 |     def discover_unity_port() -> int:
 95 |         """
 96 |         Discover Unity port by scanning per-project and legacy registry files.
 97 |         Prefer the newest file whose port responds; fall back to first parsed
 98 |         value; finally default to 6400.
 99 | 
100 |         Returns:
101 |             Port number to connect to
102 |         """
103 |         # Prefer the latest heartbeat status if it points to a responsive port
104 |         status = PortDiscovery._read_latest_status()
105 |         if status:
106 |             port = status.get('unity_port')
107 |             if isinstance(port, int) and PortDiscovery._try_probe_unity_mcp(port):
108 |                 logger.info(f"Using Unity port from status: {port}")
109 |                 return port
110 | 
111 |         candidates = PortDiscovery.list_candidate_files()
112 | 
113 |         first_seen_port: Optional[int] = None
114 | 
115 |         for path in candidates:
116 |             try:
117 |                 with open(path, 'r') as f:
118 |                     cfg = json.load(f)
119 |                 unity_port = cfg.get('unity_port')
120 |                 if isinstance(unity_port, int):
121 |                     if first_seen_port is None:
122 |                         first_seen_port = unity_port
123 |                     if PortDiscovery._try_probe_unity_mcp(unity_port):
124 |                         logger.info(
125 |                             f"Using Unity port from {path.name}: {unity_port}")
126 |                         return unity_port
127 |             except Exception as e:
128 |                 logger.warning(f"Could not read port registry {path}: {e}")
129 | 
130 |         if first_seen_port is not None:
131 |             logger.info(
132 |                 f"No responsive port found; using first seen value {first_seen_port}")
133 |             return first_seen_port
134 | 
135 |         # Fallback to default port
136 |         logger.info(
137 |             f"No port registry found; using default port {PortDiscovery.DEFAULT_PORT}")
138 |         return PortDiscovery.DEFAULT_PORT
139 | 
140 |     @staticmethod
141 |     def get_port_config() -> Optional[dict]:
142 |         """
143 |         Get the most relevant port configuration from registry.
144 |         Returns the most recent hashed file's config if present,
145 |         otherwise the legacy file's config. Returns None if nothing exists.
146 | 
147 |         Returns:
148 |             Port configuration dict or None if not found
149 |         """
150 |         candidates = PortDiscovery.list_candidate_files()
151 |         if not candidates:
152 |             return None
153 |         for path in candidates:
154 |             try:
155 |                 with open(path, 'r') as f:
156 |                     return json.load(f)
157 |             except Exception as e:
158 |                 logger.warning(
159 |                     f"Could not read port configuration {path}: {e}")
160 |         return None
161 | 
```

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

```python
  1 | """
  2 | Port discovery utility for MCP for Unity Server.
  3 | 
  4 | What changed and why:
  5 | - Unity now writes a per-project port file named like
  6 |   `~/.unity-mcp/unity-mcp-port-<hash>.json` to avoid projects overwriting
  7 |   each other's saved port. The legacy file `unity-mcp-port.json` may still
  8 |   exist.
  9 | - This module now scans for both patterns, prefers the most recently
 10 |   modified file, and verifies that the port is actually a MCP for Unity listener
 11 |   (quick socket connect + ping) before choosing it.
 12 | """
 13 | 
 14 | import glob
 15 | import json
 16 | import logging
 17 | from pathlib import Path
 18 | import socket
 19 | from typing import Optional, List
 20 | 
 21 | logger = logging.getLogger("mcp-for-unity-server")
 22 | 
 23 | 
 24 | class PortDiscovery:
 25 |     """Handles port discovery from Unity Bridge registry"""
 26 |     REGISTRY_FILE = "unity-mcp-port.json"  # legacy single-project file
 27 |     DEFAULT_PORT = 6400
 28 |     CONNECT_TIMEOUT = 0.3  # seconds, keep this snappy during discovery
 29 | 
 30 |     @staticmethod
 31 |     def get_registry_path() -> Path:
 32 |         """Get the path to the port registry file"""
 33 |         return Path.home() / ".unity-mcp" / PortDiscovery.REGISTRY_FILE
 34 | 
 35 |     @staticmethod
 36 |     def get_registry_dir() -> Path:
 37 |         return Path.home() / ".unity-mcp"
 38 | 
 39 |     @staticmethod
 40 |     def list_candidate_files() -> List[Path]:
 41 |         """Return candidate registry files, newest first.
 42 |         Includes hashed per-project files and the legacy file (if present).
 43 |         """
 44 |         base = PortDiscovery.get_registry_dir()
 45 |         hashed = sorted(
 46 |             (Path(p) for p in glob.glob(str(base / "unity-mcp-port-*.json"))),
 47 |             key=lambda p: p.stat().st_mtime,
 48 |             reverse=True,
 49 |         )
 50 |         legacy = PortDiscovery.get_registry_path()
 51 |         if legacy.exists():
 52 |             # Put legacy at the end so hashed, per-project files win
 53 |             hashed.append(legacy)
 54 |         return hashed
 55 | 
 56 |     @staticmethod
 57 |     def _try_probe_unity_mcp(port: int) -> bool:
 58 |         """Quickly check if a MCP for Unity listener is on this port.
 59 |         Tries a short TCP connect, sends 'ping', expects a JSON 'pong'.
 60 |         """
 61 |         try:
 62 |             with socket.create_connection(("127.0.0.1", port), PortDiscovery.CONNECT_TIMEOUT) as s:
 63 |                 s.settimeout(PortDiscovery.CONNECT_TIMEOUT)
 64 |                 try:
 65 |                     s.sendall(b"ping")
 66 |                     data = s.recv(512)
 67 |                     # Minimal validation: look for a success pong response
 68 |                     if data and b'"message":"pong"' in data:
 69 |                         return True
 70 |                 except Exception:
 71 |                     return False
 72 |         except Exception:
 73 |             return False
 74 |         return False
 75 | 
 76 |     @staticmethod
 77 |     def _read_latest_status() -> Optional[dict]:
 78 |         try:
 79 |             base = PortDiscovery.get_registry_dir()
 80 |             status_files = sorted(
 81 |                 (Path(p)
 82 |                  for p in glob.glob(str(base / "unity-mcp-status-*.json"))),
 83 |                 key=lambda p: p.stat().st_mtime,
 84 |                 reverse=True,
 85 |             )
 86 |             if not status_files:
 87 |                 return None
 88 |             with status_files[0].open('r') as f:
 89 |                 return json.load(f)
 90 |         except Exception:
 91 |             return None
 92 | 
 93 |     @staticmethod
 94 |     def discover_unity_port() -> int:
 95 |         """
 96 |         Discover Unity port by scanning per-project and legacy registry files.
 97 |         Prefer the newest file whose port responds; fall back to first parsed
 98 |         value; finally default to 6400.
 99 | 
100 |         Returns:
101 |             Port number to connect to
102 |         """
103 |         # Prefer the latest heartbeat status if it points to a responsive port
104 |         status = PortDiscovery._read_latest_status()
105 |         if status:
106 |             port = status.get('unity_port')
107 |             if isinstance(port, int) and PortDiscovery._try_probe_unity_mcp(port):
108 |                 logger.info(f"Using Unity port from status: {port}")
109 |                 return port
110 | 
111 |         candidates = PortDiscovery.list_candidate_files()
112 | 
113 |         first_seen_port: Optional[int] = None
114 | 
115 |         for path in candidates:
116 |             try:
117 |                 with open(path, 'r') as f:
118 |                     cfg = json.load(f)
119 |                 unity_port = cfg.get('unity_port')
120 |                 if isinstance(unity_port, int):
121 |                     if first_seen_port is None:
122 |                         first_seen_port = unity_port
123 |                     if PortDiscovery._try_probe_unity_mcp(unity_port):
124 |                         logger.info(
125 |                             f"Using Unity port from {path.name}: {unity_port}")
126 |                         return unity_port
127 |             except Exception as e:
128 |                 logger.warning(f"Could not read port registry {path}: {e}")
129 | 
130 |         if first_seen_port is not None:
131 |             logger.info(
132 |                 f"No responsive port found; using first seen value {first_seen_port}")
133 |             return first_seen_port
134 | 
135 |         # Fallback to default port
136 |         logger.info(
137 |             f"No port registry found; using default port {PortDiscovery.DEFAULT_PORT}")
138 |         return PortDiscovery.DEFAULT_PORT
139 | 
140 |     @staticmethod
141 |     def get_port_config() -> Optional[dict]:
142 |         """
143 |         Get the most relevant port configuration from registry.
144 |         Returns the most recent hashed file's config if present,
145 |         otherwise the legacy file's config. Returns None if nothing exists.
146 | 
147 |         Returns:
148 |             Port configuration dict or None if not found
149 |         """
150 |         candidates = PortDiscovery.list_candidate_files()
151 |         if not candidates:
152 |             return None
153 |         for path in candidates:
154 |             try:
155 |                 with open(path, 'r') as f:
156 |                     return json.load(f)
157 |             except Exception as e:
158 |                 logger.warning(
159 |                     f"Could not read port configuration {path}: {e}")
160 |         return None
161 | 
```

--------------------------------------------------------------------------------
/tests/test_script_tools.py:
--------------------------------------------------------------------------------

```python
  1 | import sys
  2 | import pathlib
  3 | import importlib.util
  4 | import types
  5 | import pytest
  6 | import asyncio
  7 | 
  8 | # add server src to path and load modules without triggering package imports
  9 | ROOT = pathlib.Path(__file__).resolve().parents[1]
 10 | SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src"
 11 | sys.path.insert(0, str(SRC))
 12 | 
 13 | # stub mcp.server.fastmcp to satisfy imports without full dependency
 14 | mcp_pkg = types.ModuleType("mcp")
 15 | server_pkg = types.ModuleType("mcp.server")
 16 | fastmcp_pkg = types.ModuleType("mcp.server.fastmcp")
 17 | 
 18 | 
 19 | class _Dummy:
 20 |     pass
 21 | 
 22 | 
 23 | fastmcp_pkg.FastMCP = _Dummy
 24 | fastmcp_pkg.Context = _Dummy
 25 | server_pkg.fastmcp = fastmcp_pkg
 26 | mcp_pkg.server = server_pkg
 27 | sys.modules.setdefault("mcp", mcp_pkg)
 28 | sys.modules.setdefault("mcp.server", server_pkg)
 29 | sys.modules.setdefault("mcp.server.fastmcp", fastmcp_pkg)
 30 | 
 31 | 
 32 | def load_module(path, name):
 33 |     spec = importlib.util.spec_from_file_location(name, path)
 34 |     module = importlib.util.module_from_spec(spec)
 35 |     spec.loader.exec_module(module)
 36 |     return module
 37 | 
 38 | 
 39 | manage_script_module = load_module(
 40 |     SRC / "tools" / "manage_script.py", "manage_script_module")
 41 | manage_asset_module = load_module(
 42 |     SRC / "tools" / "manage_asset.py", "manage_asset_module")
 43 | 
 44 | 
 45 | class DummyMCP:
 46 |     def __init__(self):
 47 |         self.tools = {}
 48 | 
 49 |     def tool(self, *args, **kwargs):  # accept decorator kwargs like description
 50 |         def decorator(func):
 51 |             self.tools[func.__name__] = func
 52 |             return func
 53 |         return decorator
 54 | 
 55 | 
 56 | def setup_manage_script():
 57 |     mcp = DummyMCP()
 58 |     manage_script_module.register_manage_script_tools(mcp)
 59 |     return mcp.tools
 60 | 
 61 | 
 62 | def setup_manage_asset():
 63 |     mcp = DummyMCP()
 64 |     manage_asset_module.register_manage_asset_tools(mcp)
 65 |     return mcp.tools
 66 | 
 67 | 
 68 | def test_apply_text_edits_long_file(monkeypatch):
 69 |     tools = setup_manage_script()
 70 |     apply_edits = tools["apply_text_edits"]
 71 |     captured = {}
 72 | 
 73 |     def fake_send(cmd, params):
 74 |         captured["cmd"] = cmd
 75 |         captured["params"] = params
 76 |         return {"success": True}
 77 | 
 78 |     monkeypatch.setattr(manage_script_module,
 79 |                         "send_command_with_retry", fake_send)
 80 | 
 81 |     edit = {"startLine": 1005, "startCol": 0,
 82 |             "endLine": 1005, "endCol": 5, "newText": "Hello"}
 83 |     resp = apply_edits(None, "unity://path/Assets/Scripts/LongFile.cs", [edit])
 84 |     assert captured["cmd"] == "manage_script"
 85 |     assert captured["params"]["action"] == "apply_text_edits"
 86 |     assert captured["params"]["edits"][0]["startLine"] == 1005
 87 |     assert resp["success"] is True
 88 | 
 89 | 
 90 | def test_sequential_edits_use_precondition(monkeypatch):
 91 |     tools = setup_manage_script()
 92 |     apply_edits = tools["apply_text_edits"]
 93 |     calls = []
 94 | 
 95 |     def fake_send(cmd, params):
 96 |         calls.append(params)
 97 |         return {"success": True, "sha256": f"hash{len(calls)}"}
 98 | 
 99 |     monkeypatch.setattr(manage_script_module,
100 |                         "send_command_with_retry", fake_send)
101 | 
102 |     edit1 = {"startLine": 1, "startCol": 0, "endLine": 1,
103 |              "endCol": 0, "newText": "//header\n"}
104 |     resp1 = apply_edits(None, "unity://path/Assets/Scripts/File.cs", [edit1])
105 |     edit2 = {"startLine": 2, "startCol": 0, "endLine": 2,
106 |              "endCol": 0, "newText": "//second\n"}
107 |     resp2 = apply_edits(None, "unity://path/Assets/Scripts/File.cs",
108 |                         [edit2], precondition_sha256=resp1["sha256"])
109 | 
110 |     assert calls[1]["precondition_sha256"] == resp1["sha256"]
111 |     assert resp2["sha256"] == "hash2"
112 | 
113 | 
114 | def test_apply_text_edits_forwards_options(monkeypatch):
115 |     tools = setup_manage_script()
116 |     apply_edits = tools["apply_text_edits"]
117 |     captured = {}
118 | 
119 |     def fake_send(cmd, params):
120 |         captured["params"] = params
121 |         return {"success": True}
122 | 
123 |     monkeypatch.setattr(manage_script_module,
124 |                         "send_command_with_retry", fake_send)
125 | 
126 |     opts = {"validate": "relaxed", "applyMode": "atomic", "refresh": "immediate"}
127 |     apply_edits(None, "unity://path/Assets/Scripts/File.cs",
128 |                 [{"startLine": 1, "startCol": 1, "endLine": 1, "endCol": 1, "newText": "x"}], options=opts)
129 |     assert captured["params"].get("options") == opts
130 | 
131 | 
132 | def test_apply_text_edits_defaults_atomic_for_multi_span(monkeypatch):
133 |     tools = setup_manage_script()
134 |     apply_edits = tools["apply_text_edits"]
135 |     captured = {}
136 | 
137 |     def fake_send(cmd, params):
138 |         captured["params"] = params
139 |         return {"success": True}
140 | 
141 |     monkeypatch.setattr(manage_script_module,
142 |                         "send_command_with_retry", fake_send)
143 | 
144 |     edits = [
145 |         {"startLine": 2, "startCol": 2, "endLine": 2, "endCol": 3, "newText": "A"},
146 |         {"startLine": 3, "startCol": 2, "endLine": 3,
147 |             "endCol": 2, "newText": "// tail\n"},
148 |     ]
149 |     apply_edits(None, "unity://path/Assets/Scripts/File.cs",
150 |                 edits, precondition_sha256="x")
151 |     opts = captured["params"].get("options", {})
152 |     assert opts.get("applyMode") == "atomic"
153 | 
154 | 
155 | def test_manage_asset_prefab_modify_request(monkeypatch):
156 |     tools = setup_manage_asset()
157 |     manage_asset = tools["manage_asset"]
158 |     captured = {}
159 | 
160 |     async def fake_async(cmd, params, loop=None):
161 |         captured["cmd"] = cmd
162 |         captured["params"] = params
163 |         return {"success": True}
164 | 
165 |     monkeypatch.setattr(manage_asset_module,
166 |                         "async_send_command_with_retry", fake_async)
167 |     monkeypatch.setattr(manage_asset_module,
168 |                         "get_unity_connection", lambda: object())
169 | 
170 |     async def run():
171 |         resp = await manage_asset(
172 |             None,
173 |             action="modify",
174 |             path="Assets/Prefabs/Player.prefab",
175 |             properties={"hp": 100},
176 |         )
177 |         assert captured["cmd"] == "manage_asset"
178 |         assert captured["params"]["action"] == "modify"
179 |         assert captured["params"]["path"] == "Assets/Prefabs/Player.prefab"
180 |         assert captured["params"]["properties"] == {"hp": 100}
181 |         assert resp["success"] is True
182 | 
183 |     asyncio.run(run())
184 | 
```

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

```csharp
  1 | using System;
  2 | using System.Net;
  3 | using MCPForUnity.Editor.Helpers;
  4 | using Newtonsoft.Json.Linq;
  5 | using UnityEditor;
  6 | 
  7 | namespace MCPForUnity.Editor.Services
  8 | {
  9 |     /// <summary>
 10 |     /// Service for checking package updates from GitHub
 11 |     /// </summary>
 12 |     public class PackageUpdateService : IPackageUpdateService
 13 |     {
 14 |         private const string LastCheckDateKey = "MCPForUnity.LastUpdateCheck";
 15 |         private const string CachedVersionKey = "MCPForUnity.LatestKnownVersion";
 16 |         private const string PackageJsonUrl = "https://raw.githubusercontent.com/CoplayDev/unity-mcp/main/MCPForUnity/package.json";
 17 | 
 18 |         /// <inheritdoc/>
 19 |         public UpdateCheckResult CheckForUpdate(string currentVersion)
 20 |         {
 21 |             // Check cache first - only check once per day
 22 |             string lastCheckDate = EditorPrefs.GetString(LastCheckDateKey, "");
 23 |             string cachedLatestVersion = EditorPrefs.GetString(CachedVersionKey, "");
 24 | 
 25 |             if (lastCheckDate == DateTime.Now.ToString("yyyy-MM-dd") && !string.IsNullOrEmpty(cachedLatestVersion))
 26 |             {
 27 |                 return new UpdateCheckResult
 28 |                 {
 29 |                     CheckSucceeded = true,
 30 |                     LatestVersion = cachedLatestVersion,
 31 |                     UpdateAvailable = IsNewerVersion(cachedLatestVersion, currentVersion),
 32 |                     Message = "Using cached version check"
 33 |                 };
 34 |             }
 35 | 
 36 |             // Don't check for Asset Store installations
 37 |             if (!IsGitInstallation())
 38 |             {
 39 |                 return new UpdateCheckResult
 40 |                 {
 41 |                     CheckSucceeded = false,
 42 |                     UpdateAvailable = false,
 43 |                     Message = "Asset Store installations are updated via Unity Asset Store"
 44 |                 };
 45 |             }
 46 | 
 47 |             // Fetch latest version from GitHub
 48 |             string latestVersion = FetchLatestVersionFromGitHub();
 49 | 
 50 |             if (!string.IsNullOrEmpty(latestVersion))
 51 |             {
 52 |                 // Cache the result
 53 |                 EditorPrefs.SetString(LastCheckDateKey, DateTime.Now.ToString("yyyy-MM-dd"));
 54 |                 EditorPrefs.SetString(CachedVersionKey, latestVersion);
 55 | 
 56 |                 return new UpdateCheckResult
 57 |                 {
 58 |                     CheckSucceeded = true,
 59 |                     LatestVersion = latestVersion,
 60 |                     UpdateAvailable = IsNewerVersion(latestVersion, currentVersion),
 61 |                     Message = "Successfully checked for updates"
 62 |                 };
 63 |             }
 64 | 
 65 |             return new UpdateCheckResult
 66 |             {
 67 |                 CheckSucceeded = false,
 68 |                 UpdateAvailable = false,
 69 |                 Message = "Failed to check for updates (network issue or offline)"
 70 |             };
 71 |         }
 72 | 
 73 |         /// <inheritdoc/>
 74 |         public bool IsNewerVersion(string version1, string version2)
 75 |         {
 76 |             try
 77 |             {
 78 |                 // Remove any "v" prefix
 79 |                 version1 = version1.TrimStart('v', 'V');
 80 |                 version2 = version2.TrimStart('v', 'V');
 81 | 
 82 |                 var version1Parts = version1.Split('.');
 83 |                 var version2Parts = version2.Split('.');
 84 | 
 85 |                 for (int i = 0; i < Math.Min(version1Parts.Length, version2Parts.Length); i++)
 86 |                 {
 87 |                     if (int.TryParse(version1Parts[i], out int v1Num) &&
 88 |                         int.TryParse(version2Parts[i], out int v2Num))
 89 |                     {
 90 |                         if (v1Num > v2Num) return true;
 91 |                         if (v1Num < v2Num) return false;
 92 |                     }
 93 |                 }
 94 |                 return false;
 95 |             }
 96 |             catch
 97 |             {
 98 |                 return false;
 99 |             }
100 |         }
101 | 
102 |         /// <inheritdoc/>
103 |         public bool IsGitInstallation()
104 |         {
105 |             // Git packages are installed via Package Manager and have a package.json in Packages/
106 |             // Asset Store packages are in Assets/
107 |             string packageRoot = AssetPathUtility.GetMcpPackageRootPath();
108 | 
109 |             if (string.IsNullOrEmpty(packageRoot))
110 |             {
111 |                 return false;
112 |             }
113 | 
114 |             // If the package is in Packages/ it's a PM install (likely Git)
115 |             // If it's in Assets/ it's an Asset Store install
116 |             return packageRoot.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase);
117 |         }
118 | 
119 |         /// <inheritdoc/>
120 |         public void ClearCache()
121 |         {
122 |             EditorPrefs.DeleteKey(LastCheckDateKey);
123 |             EditorPrefs.DeleteKey(CachedVersionKey);
124 |         }
125 | 
126 |         /// <summary>
127 |         /// Fetches the latest version from GitHub's main branch package.json
128 |         /// </summary>
129 |         private string FetchLatestVersionFromGitHub()
130 |         {
131 |             try
132 |             {
133 |                 // GitHub API endpoint (Option 1 - has rate limits):
134 |                 // https://api.github.com/repos/CoplayDev/unity-mcp/releases/latest
135 |                 //
136 |                 // We use Option 2 (package.json directly) because:
137 |                 // - No API rate limits (GitHub serves raw files freely)
138 |                 // - Simpler - just parse JSON for version field
139 |                 // - More reliable - doesn't require releases to be published
140 |                 // - Direct source of truth from the main branch
141 | 
142 |                 using (var client = new WebClient())
143 |                 {
144 |                     client.Headers.Add("User-Agent", "Unity-MCPForUnity-UpdateChecker");
145 |                     string jsonContent = client.DownloadString(PackageJsonUrl);
146 | 
147 |                     var packageJson = JObject.Parse(jsonContent);
148 |                     string version = packageJson["version"]?.ToString();
149 | 
150 |                     return string.IsNullOrEmpty(version) ? null : version;
151 |                 }
152 |             }
153 |             catch (Exception ex)
154 |             {
155 |                 // Silent fail - don't interrupt the user if network is unavailable
156 |                 McpLog.Info($"Update check failed (this is normal if offline): {ex.Message}");
157 |                 return null;
158 |             }
159 |         }
160 |     }
161 | }
162 | 
```

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

```csharp
  1 | using System;
  2 | using System.IO;
  3 | using Newtonsoft.Json.Linq;
  4 | using UnityEditor;
  5 | using UnityEngine;
  6 | using PackageInfo = UnityEditor.PackageManager.PackageInfo;
  7 | 
  8 | namespace MCPForUnity.Editor.Helpers
  9 | {
 10 |     /// <summary>
 11 |     /// Provides common utility methods for working with Unity asset paths.
 12 |     /// </summary>
 13 |     public static class AssetPathUtility
 14 |     {
 15 |         /// <summary>
 16 |         /// Normalizes a Unity asset path by ensuring forward slashes are used and that it is rooted under "Assets/".
 17 |         /// </summary>
 18 |         public static string SanitizeAssetPath(string path)
 19 |         {
 20 |             if (string.IsNullOrEmpty(path))
 21 |             {
 22 |                 return path;
 23 |             }
 24 | 
 25 |             path = path.Replace('\\', '/');
 26 |             if (!path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
 27 |             {
 28 |                 return "Assets/" + path.TrimStart('/');
 29 |             }
 30 | 
 31 |             return path;
 32 |         }
 33 | 
 34 |         /// <summary>
 35 |         /// Gets the MCP for Unity package root path.
 36 |         /// Works for registry Package Manager, local Package Manager, and Asset Store installations.
 37 |         /// </summary>
 38 |         /// <returns>The package root path (virtual for PM, absolute for Asset Store), or null if not found</returns>
 39 |         public static string GetMcpPackageRootPath()
 40 |         {
 41 |             try
 42 |             {
 43 |                 // Try Package Manager first (registry and local installs)
 44 |                 var packageInfo = PackageInfo.FindForAssembly(typeof(AssetPathUtility).Assembly);
 45 |                 if (packageInfo != null && !string.IsNullOrEmpty(packageInfo.assetPath))
 46 |                 {
 47 |                     return packageInfo.assetPath;
 48 |                 }
 49 | 
 50 |                 // Fallback to AssetDatabase for Asset Store installs (Assets/MCPForUnity)
 51 |                 string[] guids = AssetDatabase.FindAssets($"t:Script {nameof(AssetPathUtility)}");
 52 |                 
 53 |                 if (guids.Length == 0)
 54 |                 {
 55 |                     McpLog.Warn("Could not find AssetPathUtility script in AssetDatabase");
 56 |                     return null;
 57 |                 }
 58 | 
 59 |                 string scriptPath = AssetDatabase.GUIDToAssetPath(guids[0]);
 60 |                 
 61 |                 // Script is at: {packageRoot}/Editor/Helpers/AssetPathUtility.cs
 62 |                 // Extract {packageRoot}
 63 |                 int editorIndex = scriptPath.IndexOf("/Editor/", StringComparison.Ordinal);
 64 |                 
 65 |                 if (editorIndex >= 0)
 66 |                 {
 67 |                     return scriptPath.Substring(0, editorIndex);
 68 |                 }
 69 | 
 70 |                 McpLog.Warn($"Could not determine package root from script path: {scriptPath}");
 71 |                 return null;
 72 |             }
 73 |             catch (Exception ex)
 74 |             {
 75 |                 McpLog.Error($"Failed to get package root path: {ex.Message}");
 76 |                 return null;
 77 |             }
 78 |         }
 79 | 
 80 |         /// <summary>
 81 |         /// Reads and parses the package.json file for MCP for Unity.
 82 |         /// Handles both Package Manager (registry/local) and Asset Store installations.
 83 |         /// </summary>
 84 |         /// <returns>JObject containing package.json data, or null if not found or parse failed</returns>
 85 |         public static JObject GetPackageJson()
 86 |         {
 87 |             try
 88 |             {
 89 |                 string packageRoot = GetMcpPackageRootPath();
 90 |                 if (string.IsNullOrEmpty(packageRoot))
 91 |                 {
 92 |                     return null;
 93 |                 }
 94 | 
 95 |                 string packageJsonPath = Path.Combine(packageRoot, "package.json");
 96 | 
 97 |                 // Convert virtual asset path to file system path
 98 |                 if (packageRoot.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase))
 99 |                 {
100 |                     // Package Manager install - must use PackageInfo.resolvedPath
101 |                     // Virtual paths like "Packages/..." don't work with File.Exists()
102 |                     // Registry packages live in Library/PackageCache/package@version/
103 |                     var packageInfo = PackageInfo.FindForAssembly(typeof(AssetPathUtility).Assembly);
104 |                     if (packageInfo != null && !string.IsNullOrEmpty(packageInfo.resolvedPath))
105 |                     {
106 |                         packageJsonPath = Path.Combine(packageInfo.resolvedPath, "package.json");
107 |                     }
108 |                     else
109 |                     {
110 |                         McpLog.Warn("Could not resolve Package Manager path for package.json");
111 |                         return null;
112 |                     }
113 |                 }
114 |                 else if (packageRoot.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
115 |                 {
116 |                     // Asset Store install - convert to absolute file system path
117 |                     // Application.dataPath is the absolute path to the Assets folder
118 |                     string relativePath = packageRoot.Substring("Assets/".Length);
119 |                     packageJsonPath = Path.Combine(Application.dataPath, relativePath, "package.json");
120 |                 }
121 | 
122 |                 if (!File.Exists(packageJsonPath))
123 |                 {
124 |                     McpLog.Warn($"package.json not found at: {packageJsonPath}");
125 |                     return null;
126 |                 }
127 | 
128 |                 string json = File.ReadAllText(packageJsonPath);
129 |                 return JObject.Parse(json);
130 |             }
131 |             catch (Exception ex)
132 |             {
133 |                 McpLog.Warn($"Failed to read or parse package.json: {ex.Message}");
134 |                 return null;
135 |             }
136 |         }
137 | 
138 |         /// <summary>
139 |         /// Gets the version string from the package.json file.
140 |         /// </summary>
141 |         /// <returns>Version string, or "unknown" if not found</returns>
142 |         public static string GetPackageVersion()
143 |         {
144 |             try
145 |             {
146 |                 var packageJson = GetPackageJson();
147 |                 if (packageJson == null)
148 |                 {
149 |                     return "unknown";
150 |                 }
151 | 
152 |                 string version = packageJson["version"]?.ToString();
153 |                 return string.IsNullOrEmpty(version) ? "unknown" : version;
154 |             }
155 |             catch (Exception ex)
156 |             {
157 |                 McpLog.Warn($"Failed to get package version: {ex.Message}");
158 |                 return "unknown";
159 |             }
160 |         }
161 |     }
162 | }
163 | 
```

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

```csharp
  1 | using System.IO;
  2 | using System.Linq;
  3 | using MCPForUnity.Editor.Data;
  4 | using MCPForUnity.Editor.Services;
  5 | using UnityEditor;
  6 | using UnityEngine;
  7 | 
  8 | namespace MCPForUnity.Editor.Helpers
  9 | {
 10 |     /// <summary>
 11 |     /// Automatically syncs Python tools to the MCP server when:
 12 |     /// - PythonToolsAsset is modified
 13 |     /// - Python files are imported/reimported
 14 |     /// - Unity starts up
 15 |     /// </summary>
 16 |     [InitializeOnLoad]
 17 |     public class PythonToolSyncProcessor : AssetPostprocessor
 18 |     {
 19 |         private const string SyncEnabledKey = "MCPForUnity.AutoSyncEnabled";
 20 |         private static bool _isSyncing = false;
 21 | 
 22 |         static PythonToolSyncProcessor()
 23 |         {
 24 |             // Sync on Unity startup
 25 |             EditorApplication.delayCall += () =>
 26 |             {
 27 |                 if (IsAutoSyncEnabled())
 28 |                 {
 29 |                     SyncAllTools();
 30 |                 }
 31 |             };
 32 |         }
 33 | 
 34 |         /// <summary>
 35 |         /// Called after any assets are imported, deleted, or moved
 36 |         /// </summary>
 37 |         private static void OnPostprocessAllAssets(
 38 |             string[] importedAssets,
 39 |             string[] deletedAssets,
 40 |             string[] movedAssets,
 41 |             string[] movedFromAssetPaths)
 42 |         {
 43 |             // Prevent infinite loop - don't process if we're currently syncing
 44 |             if (_isSyncing || !IsAutoSyncEnabled())
 45 |                 return;
 46 | 
 47 |             bool needsSync = false;
 48 | 
 49 |             // Only check for .py file changes, not PythonToolsAsset changes
 50 |             // (PythonToolsAsset changes are internal state updates from syncing)
 51 |             foreach (string path in importedAssets.Concat(movedAssets))
 52 |             {
 53 |                 // Check if any .py files were modified
 54 |                 if (path.EndsWith(".py"))
 55 |                 {
 56 |                     needsSync = true;
 57 |                     break;
 58 |                 }
 59 |             }
 60 | 
 61 |             // Check if any .py files were deleted
 62 |             if (!needsSync && deletedAssets.Any(path => path.EndsWith(".py")))
 63 |             {
 64 |                 needsSync = true;
 65 |             }
 66 | 
 67 |             if (needsSync)
 68 |             {
 69 |                 SyncAllTools();
 70 |             }
 71 |         }
 72 | 
 73 |         /// <summary>
 74 |         /// Syncs all Python tools from all PythonToolsAsset instances to the MCP server
 75 |         /// </summary>
 76 |         public static void SyncAllTools()
 77 |         {
 78 |             // Prevent re-entrant calls
 79 |             if (_isSyncing)
 80 |             {
 81 |                 McpLog.Warn("Sync already in progress, skipping...");
 82 |                 return;
 83 |             }
 84 | 
 85 |             _isSyncing = true;
 86 |             try
 87 |             {
 88 |                 if (!ServerPathResolver.TryFindEmbeddedServerSource(out string srcPath))
 89 |                 {
 90 |                     McpLog.Warn("Cannot sync Python tools: MCP server source not found");
 91 |                     return;
 92 |                 }
 93 | 
 94 |                 string toolsDir = Path.Combine(srcPath, "tools", "custom");
 95 | 
 96 |                 var result = MCPServiceLocator.ToolSync.SyncProjectTools(toolsDir);
 97 | 
 98 |                 if (result.Success)
 99 |                 {
100 |                     if (result.CopiedCount > 0 || result.SkippedCount > 0)
101 |                     {
102 |                         McpLog.Info($"Python tools synced: {result.CopiedCount} copied, {result.SkippedCount} skipped");
103 |                     }
104 |                 }
105 |                 else
106 |                 {
107 |                     McpLog.Error($"Python tool sync failed with {result.ErrorCount} errors");
108 |                     foreach (var msg in result.Messages)
109 |                     {
110 |                         McpLog.Error($"  - {msg}");
111 |                     }
112 |                 }
113 |             }
114 |             catch (System.Exception ex)
115 |             {
116 |                 McpLog.Error($"Python tool sync exception: {ex.Message}");
117 |             }
118 |             finally
119 |             {
120 |                 _isSyncing = false;
121 |             }
122 |         }
123 | 
124 |         /// <summary>
125 |         /// Checks if auto-sync is enabled (default: true)
126 |         /// </summary>
127 |         public static bool IsAutoSyncEnabled()
128 |         {
129 |             return EditorPrefs.GetBool(SyncEnabledKey, true);
130 |         }
131 | 
132 |         /// <summary>
133 |         /// Enables or disables auto-sync
134 |         /// </summary>
135 |         public static void SetAutoSyncEnabled(bool enabled)
136 |         {
137 |             EditorPrefs.SetBool(SyncEnabledKey, enabled);
138 |             McpLog.Info($"Python tool auto-sync {(enabled ? "enabled" : "disabled")}");
139 |         }
140 | 
141 |         /// <summary>
142 |         /// Menu item to reimport all Python files in the project
143 |         /// </summary>
144 |         [MenuItem("Window/MCP For Unity/Tool Sync/Reimport Python Files", priority = 99)]
145 |         public static void ReimportPythonFiles()
146 |         {
147 |             // Find all Python files (imported as TextAssets by PythonFileImporter)
148 |             var pythonGuids = AssetDatabase.FindAssets("t:TextAsset", new[] { "Assets" })
149 |                 .Select(AssetDatabase.GUIDToAssetPath)
150 |                 .Where(path => path.EndsWith(".py", System.StringComparison.OrdinalIgnoreCase))
151 |                 .ToArray();
152 | 
153 |             foreach (string path in pythonGuids)
154 |             {
155 |                 AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
156 |             }
157 | 
158 |             int count = pythonGuids.Length;
159 |             McpLog.Info($"Reimported {count} Python files");
160 |             AssetDatabase.Refresh();
161 |         }
162 | 
163 |         /// <summary>
164 |         /// Menu item to manually trigger sync
165 |         /// </summary>
166 |         [MenuItem("Window/MCP For Unity/Tool Sync/Sync Python Tools", priority = 100)]
167 |         public static void ManualSync()
168 |         {
169 |             McpLog.Info("Manually syncing Python tools...");
170 |             SyncAllTools();
171 |         }
172 | 
173 |         /// <summary>
174 |         /// Menu item to toggle auto-sync
175 |         /// </summary>
176 |         [MenuItem("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", priority = 101)]
177 |         public static void ToggleAutoSync()
178 |         {
179 |             SetAutoSyncEnabled(!IsAutoSyncEnabled());
180 |         }
181 | 
182 |         /// <summary>
183 |         /// Validate menu item (shows checkmark when enabled)
184 |         /// </summary>
185 |         [MenuItem("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", true, priority = 101)]
186 |         public static bool ToggleAutoSyncValidate()
187 |         {
188 |             Menu.SetChecked("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", IsAutoSyncEnabled());
189 |             return true;
190 |         }
191 |     }
192 | }
193 | 
```

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

```csharp
  1 | using System;
  2 | using System.IO;
  3 | using System.Runtime.InteropServices;
  4 | using System.Text;
  5 | using UnityEditor;
  6 | 
  7 | namespace MCPForUnity.Editor.Helpers
  8 | {
  9 |     /// <summary>
 10 |     /// Shared helpers for reading and writing MCP client configuration files.
 11 |     /// Consolidates file atomics and server directory resolution so the editor
 12 |     /// window can focus on UI concerns only.
 13 |     /// </summary>
 14 |     public static class McpConfigFileHelper
 15 |     {
 16 |         public static string ExtractDirectoryArg(string[] args)
 17 |         {
 18 |             if (args == null) return null;
 19 |             for (int i = 0; i < args.Length - 1; i++)
 20 |             {
 21 |                 if (string.Equals(args[i], "--directory", StringComparison.OrdinalIgnoreCase))
 22 |                 {
 23 |                     return args[i + 1];
 24 |                 }
 25 |             }
 26 |             return null;
 27 |         }
 28 | 
 29 |         public static bool PathsEqual(string a, string b)
 30 |         {
 31 |             if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false;
 32 |             try
 33 |             {
 34 |                 string na = Path.GetFullPath(a.Trim());
 35 |                 string nb = Path.GetFullPath(b.Trim());
 36 |                 if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
 37 |                 {
 38 |                     return string.Equals(na, nb, StringComparison.OrdinalIgnoreCase);
 39 |                 }
 40 |                 return string.Equals(na, nb, StringComparison.Ordinal);
 41 |             }
 42 |             catch
 43 |             {
 44 |                 return false;
 45 |             }
 46 |         }
 47 | 
 48 |         /// <summary>
 49 |         /// Resolves the server directory to use for MCP tools, preferring
 50 |         /// existing config values and falling back to installed/embedded copies.
 51 |         /// </summary>
 52 |         public static string ResolveServerDirectory(string pythonDir, string[] existingArgs)
 53 |         {
 54 |             string serverSrc = ExtractDirectoryArg(existingArgs);
 55 |             bool serverValid = !string.IsNullOrEmpty(serverSrc)
 56 |                 && File.Exists(Path.Combine(serverSrc, "server.py"));
 57 |             if (!serverValid)
 58 |             {
 59 |                 if (!string.IsNullOrEmpty(pythonDir)
 60 |                     && File.Exists(Path.Combine(pythonDir, "server.py")))
 61 |                 {
 62 |                     serverSrc = pythonDir;
 63 |                 }
 64 |                 else
 65 |                 {
 66 |                     serverSrc = ResolveServerSource();
 67 |                 }
 68 |             }
 69 | 
 70 |             try
 71 |             {
 72 |                 if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && !string.IsNullOrEmpty(serverSrc))
 73 |                 {
 74 |                     string norm = serverSrc.Replace('\\', '/');
 75 |                     int idx = norm.IndexOf("/.local/share/UnityMCP/", StringComparison.Ordinal);
 76 |                     if (idx >= 0)
 77 |                     {
 78 |                         string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
 79 |                         string suffix = norm.Substring(idx + "/.local/share/".Length);
 80 |                         serverSrc = Path.Combine(home, "Library", "Application Support", suffix);
 81 |                     }
 82 |                 }
 83 |             }
 84 |             catch
 85 |             {
 86 |                 // Ignore failures and fall back to the original path.
 87 |             }
 88 | 
 89 |             if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
 90 |                 && !string.IsNullOrEmpty(serverSrc)
 91 |                 && serverSrc.IndexOf(@"\Library\PackageCache\", StringComparison.OrdinalIgnoreCase) >= 0
 92 |                 && !EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false))
 93 |             {
 94 |                 serverSrc = ServerInstaller.GetServerPath();
 95 |             }
 96 | 
 97 |             return serverSrc;
 98 |         }
 99 | 
100 |         public static void WriteAtomicFile(string path, string contents)
101 |         {
102 |             string tmp = path + ".tmp";
103 |             string backup = path + ".backup";
104 |             bool writeDone = false;
105 |             try
106 |             {
107 |                 File.WriteAllText(tmp, contents, new UTF8Encoding(false));
108 |                 try
109 |                 {
110 |                     File.Replace(tmp, path, backup);
111 |                     writeDone = true;
112 |                 }
113 |                 catch (FileNotFoundException)
114 |                 {
115 |                     File.Move(tmp, path);
116 |                     writeDone = true;
117 |                 }
118 |                 catch (PlatformNotSupportedException)
119 |                 {
120 |                     if (File.Exists(path))
121 |                     {
122 |                         try
123 |                         {
124 |                             if (File.Exists(backup)) File.Delete(backup);
125 |                         }
126 |                         catch { }
127 |                         File.Move(path, backup);
128 |                     }
129 |                     File.Move(tmp, path);
130 |                     writeDone = true;
131 |                 }
132 |             }
133 |             catch (Exception ex)
134 |             {
135 |                 try
136 |                 {
137 |                     if (!writeDone && File.Exists(backup))
138 |                     {
139 |                         try { File.Copy(backup, path, true); } catch { }
140 |                     }
141 |                 }
142 |                 catch { }
143 |                 throw new Exception($"Failed to write config file '{path}': {ex.Message}", ex);
144 |             }
145 |             finally
146 |             {
147 |                 try { if (File.Exists(tmp)) File.Delete(tmp); } catch { }
148 |                 try { if (writeDone && File.Exists(backup)) File.Delete(backup); } catch { }
149 |             }
150 |         }
151 | 
152 |         public static string ResolveServerSource()
153 |         {
154 |             try
155 |             {
156 |                 string remembered = EditorPrefs.GetString("MCPForUnity.ServerSrc", string.Empty);
157 |                 if (!string.IsNullOrEmpty(remembered)
158 |                     && File.Exists(Path.Combine(remembered, "server.py")))
159 |                 {
160 |                     return remembered;
161 |                 }
162 | 
163 |                 ServerInstaller.EnsureServerInstalled();
164 |                 string installed = ServerInstaller.GetServerPath();
165 |                 if (File.Exists(Path.Combine(installed, "server.py")))
166 |                 {
167 |                     return installed;
168 |                 }
169 | 
170 |                 bool useEmbedded = EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false);
171 |                 if (useEmbedded
172 |                     && ServerPathResolver.TryFindEmbeddedServerSource(out string embedded)
173 |                     && File.Exists(Path.Combine(embedded, "server.py")))
174 |                 {
175 |                     return embedded;
176 |                 }
177 | 
178 |                 return installed;
179 |             }
180 |             catch
181 |             {
182 |                 return ServerInstaller.GetServerPath();
183 |             }
184 |         }
185 |     }
186 | }
187 | 
```

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

```csharp
  1 | using System;
  2 | using System.IO;
  3 | using System.Runtime.InteropServices;
  4 | using System.Text;
  5 | using UnityEditor;
  6 | 
  7 | namespace MCPForUnity.Editor.Helpers
  8 | {
  9 |     /// <summary>
 10 |     /// Shared helpers for reading and writing MCP client configuration files.
 11 |     /// Consolidates file atomics and server directory resolution so the editor
 12 |     /// window can focus on UI concerns only.
 13 |     /// </summary>
 14 |     public static class McpConfigFileHelper
 15 |     {
 16 |         public static string ExtractDirectoryArg(string[] args)
 17 |         {
 18 |             if (args == null) return null;
 19 |             for (int i = 0; i < args.Length - 1; i++)
 20 |             {
 21 |                 if (string.Equals(args[i], "--directory", StringComparison.OrdinalIgnoreCase))
 22 |                 {
 23 |                     return args[i + 1];
 24 |                 }
 25 |             }
 26 |             return null;
 27 |         }
 28 | 
 29 |         public static bool PathsEqual(string a, string b)
 30 |         {
 31 |             if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false;
 32 |             try
 33 |             {
 34 |                 string na = Path.GetFullPath(a.Trim());
 35 |                 string nb = Path.GetFullPath(b.Trim());
 36 |                 if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
 37 |                 {
 38 |                     return string.Equals(na, nb, StringComparison.OrdinalIgnoreCase);
 39 |                 }
 40 |                 return string.Equals(na, nb, StringComparison.Ordinal);
 41 |             }
 42 |             catch
 43 |             {
 44 |                 return false;
 45 |             }
 46 |         }
 47 | 
 48 |         /// <summary>
 49 |         /// Resolves the server directory to use for MCP tools, preferring
 50 |         /// existing config values and falling back to installed/embedded copies.
 51 |         /// </summary>
 52 |         public static string ResolveServerDirectory(string pythonDir, string[] existingArgs)
 53 |         {
 54 |             string serverSrc = ExtractDirectoryArg(existingArgs);
 55 |             bool serverValid = !string.IsNullOrEmpty(serverSrc)
 56 |                 && File.Exists(Path.Combine(serverSrc, "server.py"));
 57 |             if (!serverValid)
 58 |             {
 59 |                 if (!string.IsNullOrEmpty(pythonDir)
 60 |                     && File.Exists(Path.Combine(pythonDir, "server.py")))
 61 |                 {
 62 |                     serverSrc = pythonDir;
 63 |                 }
 64 |                 else
 65 |                 {
 66 |                     serverSrc = ResolveServerSource();
 67 |                 }
 68 |             }
 69 | 
 70 |             try
 71 |             {
 72 |                 if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && !string.IsNullOrEmpty(serverSrc))
 73 |                 {
 74 |                     string norm = serverSrc.Replace('\\', '/');
 75 |                     int idx = norm.IndexOf("/.local/share/UnityMCP/", StringComparison.Ordinal);
 76 |                     if (idx >= 0)
 77 |                     {
 78 |                         string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
 79 |                         string suffix = norm.Substring(idx + "/.local/share/".Length);
 80 |                         serverSrc = Path.Combine(home, "Library", "Application Support", suffix);
 81 |                     }
 82 |                 }
 83 |             }
 84 |             catch
 85 |             {
 86 |                 // Ignore failures and fall back to the original path.
 87 |             }
 88 | 
 89 |             if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
 90 |                 && !string.IsNullOrEmpty(serverSrc)
 91 |                 && serverSrc.IndexOf(@"\Library\PackageCache\", StringComparison.OrdinalIgnoreCase) >= 0
 92 |                 && !EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false))
 93 |             {
 94 |                 serverSrc = ServerInstaller.GetServerPath();
 95 |             }
 96 | 
 97 |             return serverSrc;
 98 |         }
 99 | 
100 |         public static void WriteAtomicFile(string path, string contents)
101 |         {
102 |             string tmp = path + ".tmp";
103 |             string backup = path + ".backup";
104 |             bool writeDone = false;
105 |             try
106 |             {
107 |                 File.WriteAllText(tmp, contents, new UTF8Encoding(false));
108 |                 try
109 |                 {
110 |                     File.Replace(tmp, path, backup);
111 |                     writeDone = true;
112 |                 }
113 |                 catch (FileNotFoundException)
114 |                 {
115 |                     File.Move(tmp, path);
116 |                     writeDone = true;
117 |                 }
118 |                 catch (PlatformNotSupportedException)
119 |                 {
120 |                     if (File.Exists(path))
121 |                     {
122 |                         try
123 |                         {
124 |                             if (File.Exists(backup)) File.Delete(backup);
125 |                         }
126 |                         catch { }
127 |                         File.Move(path, backup);
128 |                     }
129 |                     File.Move(tmp, path);
130 |                     writeDone = true;
131 |                 }
132 |             }
133 |             catch (Exception ex)
134 |             {
135 |                 try
136 |                 {
137 |                     if (!writeDone && File.Exists(backup))
138 |                     {
139 |                         try { File.Copy(backup, path, true); } catch { }
140 |                     }
141 |                 }
142 |                 catch { }
143 |                 throw new Exception($"Failed to write config file '{path}': {ex.Message}", ex);
144 |             }
145 |             finally
146 |             {
147 |                 try { if (File.Exists(tmp)) File.Delete(tmp); } catch { }
148 |                 try { if (writeDone && File.Exists(backup)) File.Delete(backup); } catch { }
149 |             }
150 |         }
151 | 
152 |         public static string ResolveServerSource()
153 |         {
154 |             try
155 |             {
156 |                 string remembered = EditorPrefs.GetString("MCPForUnity.ServerSrc", string.Empty);
157 |                 if (!string.IsNullOrEmpty(remembered)
158 |                     && File.Exists(Path.Combine(remembered, "server.py")))
159 |                 {
160 |                     return remembered;
161 |                 }
162 | 
163 |                 ServerInstaller.EnsureServerInstalled();
164 |                 string installed = ServerInstaller.GetServerPath();
165 |                 if (File.Exists(Path.Combine(installed, "server.py")))
166 |                 {
167 |                     return installed;
168 |                 }
169 | 
170 |                 bool useEmbedded = EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false);
171 |                 if (useEmbedded
172 |                     && ServerPathResolver.TryFindEmbeddedServerSource(out string embedded)
173 |                     && File.Exists(Path.Combine(embedded, "server.py")))
174 |                 {
175 |                     return embedded;
176 |                 }
177 | 
178 |                 return installed;
179 |             }
180 |             catch
181 |             {
182 |                 return ServerInstaller.GetServerPath();
183 |             }
184 |         }
185 |     }
186 | }
187 | 
```

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

```csharp
  1 | using System;
  2 | using System.IO;
  3 | using System.Net;
  4 | using System.Net.Sockets;
  5 | using System.Text;
  6 | 
  7 | namespace MCPForUnity.Editor.Services
  8 | {
  9 |     /// <summary>
 10 |     /// Implementation of bridge control service
 11 |     /// </summary>
 12 |     public class BridgeControlService : IBridgeControlService
 13 |     {
 14 |         public bool IsRunning => MCPForUnityBridge.IsRunning;
 15 |         public int CurrentPort => MCPForUnityBridge.GetCurrentPort();
 16 |         public bool IsAutoConnectMode => MCPForUnityBridge.IsAutoConnectMode();
 17 | 
 18 |         public void Start()
 19 |         {
 20 |             // If server is installed, use auto-connect mode
 21 |             // Otherwise use standard mode
 22 |             string serverPath = MCPServiceLocator.Paths.GetMcpServerPath();
 23 |             if (!string.IsNullOrEmpty(serverPath) && File.Exists(Path.Combine(serverPath, "server.py")))
 24 |             {
 25 |                 MCPForUnityBridge.StartAutoConnect();
 26 |             }
 27 |             else
 28 |             {
 29 |                 MCPForUnityBridge.Start();
 30 |             }
 31 |         }
 32 | 
 33 |         public void Stop()
 34 |         {
 35 |             MCPForUnityBridge.Stop();
 36 |         }
 37 | 
 38 |         public BridgeVerificationResult Verify(int port)
 39 |         {
 40 |             var result = new BridgeVerificationResult
 41 |             {
 42 |                 Success = false,
 43 |                 HandshakeValid = false,
 44 |                 PingSucceeded = false,
 45 |                 Message = "Verification not started"
 46 |             };
 47 | 
 48 |             const int ConnectTimeoutMs = 1000;
 49 |             const int FrameTimeoutMs = 30000; // Match bridge frame I/O timeout
 50 | 
 51 |             try
 52 |             {
 53 |                 using (var client = new TcpClient())
 54 |                 {
 55 |                     // Attempt connection
 56 |                     var connectTask = client.ConnectAsync(IPAddress.Loopback, port);
 57 |                     if (!connectTask.Wait(ConnectTimeoutMs))
 58 |                     {
 59 |                         result.Message = "Connection timeout";
 60 |                         return result;
 61 |                     }
 62 | 
 63 |                     using (var stream = client.GetStream())
 64 |                     {
 65 |                         try { client.NoDelay = true; } catch { }
 66 | 
 67 |                         // 1) Read handshake line (ASCII, newline-terminated)
 68 |                         string handshake = ReadLineAscii(stream, 2000);
 69 |                         if (string.IsNullOrEmpty(handshake) || handshake.IndexOf("FRAMING=1", StringComparison.OrdinalIgnoreCase) < 0)
 70 |                         {
 71 |                             result.Message = "Bridge handshake missing FRAMING=1";
 72 |                             return result;
 73 |                         }
 74 | 
 75 |                         result.HandshakeValid = true;
 76 | 
 77 |                         // 2) Send framed "ping"
 78 |                         byte[] payload = Encoding.UTF8.GetBytes("ping");
 79 |                         WriteFrame(stream, payload, FrameTimeoutMs);
 80 | 
 81 |                         // 3) Read framed response and check for pong
 82 |                         string response = ReadFrameUtf8(stream, FrameTimeoutMs);
 83 |                         if (!string.IsNullOrEmpty(response) && response.IndexOf("pong", StringComparison.OrdinalIgnoreCase) >= 0)
 84 |                         {
 85 |                             result.PingSucceeded = true;
 86 |                             result.Success = true;
 87 |                             result.Message = "Bridge verified successfully";
 88 |                         }
 89 |                         else
 90 |                         {
 91 |                             result.Message = $"Ping failed; response='{response}'";
 92 |                         }
 93 |                     }
 94 |                 }
 95 |             }
 96 |             catch (Exception ex)
 97 |             {
 98 |                 result.Message = $"Verification error: {ex.Message}";
 99 |             }
100 | 
101 |             return result;
102 |         }
103 | 
104 |         // Minimal framing helpers (8-byte big-endian length prefix), blocking with timeouts
105 |         private static void WriteFrame(NetworkStream stream, byte[] payload, int timeoutMs)
106 |         {
107 |             if (payload == null) throw new ArgumentNullException(nameof(payload));
108 |             if (payload.LongLength < 1) throw new IOException("Zero-length frames are not allowed");
109 |             
110 |             byte[] header = new byte[8];
111 |             ulong len = (ulong)payload.LongLength;
112 |             header[0] = (byte)(len >> 56);
113 |             header[1] = (byte)(len >> 48);
114 |             header[2] = (byte)(len >> 40);
115 |             header[3] = (byte)(len >> 32);
116 |             header[4] = (byte)(len >> 24);
117 |             header[5] = (byte)(len >> 16);
118 |             header[6] = (byte)(len >> 8);
119 |             header[7] = (byte)(len);
120 | 
121 |             stream.WriteTimeout = timeoutMs;
122 |             stream.Write(header, 0, header.Length);
123 |             stream.Write(payload, 0, payload.Length);
124 |         }
125 | 
126 |         private static string ReadFrameUtf8(NetworkStream stream, int timeoutMs)
127 |         {
128 |             byte[] header = ReadExact(stream, 8, timeoutMs);
129 |             ulong len = ((ulong)header[0] << 56)
130 |                       | ((ulong)header[1] << 48)
131 |                       | ((ulong)header[2] << 40)
132 |                       | ((ulong)header[3] << 32)
133 |                       | ((ulong)header[4] << 24)
134 |                       | ((ulong)header[5] << 16)
135 |                       | ((ulong)header[6] << 8)
136 |                       | header[7];
137 |             if (len == 0UL) throw new IOException("Zero-length frames are not allowed");
138 |             if (len > int.MaxValue) throw new IOException("Frame too large");
139 |             byte[] payload = ReadExact(stream, (int)len, timeoutMs);
140 |             return Encoding.UTF8.GetString(payload);
141 |         }
142 | 
143 |         private static byte[] ReadExact(NetworkStream stream, int count, int timeoutMs)
144 |         {
145 |             byte[] buffer = new byte[count];
146 |             int offset = 0;
147 |             stream.ReadTimeout = timeoutMs;
148 |             while (offset < count)
149 |             {
150 |                 int read = stream.Read(buffer, offset, count - offset);
151 |                 if (read <= 0) throw new IOException("Connection closed before reading expected bytes");
152 |                 offset += read;
153 |             }
154 |             return buffer;
155 |         }
156 | 
157 |         private static string ReadLineAscii(NetworkStream stream, int timeoutMs, int maxLen = 512)
158 |         {
159 |             stream.ReadTimeout = timeoutMs;
160 |             using (var ms = new MemoryStream())
161 |             {
162 |                 byte[] one = new byte[1];
163 |                 while (ms.Length < maxLen)
164 |                 {
165 |                     int n = stream.Read(one, 0, 1);
166 |                     if (n <= 0) break;
167 |                     if (one[0] == (byte)'\n') break;
168 |                     ms.WriteByte(one[0]);
169 |                 }
170 |                 return Encoding.ASCII.GetString(ms.ToArray());
171 |             }
172 |         }
173 |     }
174 | }
175 | 
```

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

```python
  1 | """
  2 | Telemetry decorator for MCP for Unity tools
  3 | """
  4 | 
  5 | import functools
  6 | import inspect
  7 | import logging
  8 | import time
  9 | from typing import Callable, Any
 10 | 
 11 | from telemetry import record_resource_usage, record_tool_usage, record_milestone, MilestoneType
 12 | 
 13 | _log = logging.getLogger("unity-mcp-telemetry")
 14 | _decorator_log_count = 0
 15 | 
 16 | 
 17 | def telemetry_tool(tool_name: str):
 18 |     """Decorator to add telemetry tracking to MCP tools"""
 19 |     def decorator(func: Callable) -> Callable:
 20 |         @functools.wraps(func)
 21 |         def _sync_wrapper(*args, **kwargs) -> Any:
 22 |             start_time = time.time()
 23 |             success = False
 24 |             error = None
 25 |             # Extract sub-action (e.g., 'get_hierarchy') from bound args when available
 26 |             sub_action = None
 27 |             try:
 28 |                 sig = inspect.signature(func)
 29 |                 bound = sig.bind_partial(*args, **kwargs)
 30 |                 bound.apply_defaults()
 31 |                 sub_action = bound.arguments.get("action")
 32 |             except Exception:
 33 |                 sub_action = None
 34 |             try:
 35 |                 global _decorator_log_count
 36 |                 if _decorator_log_count < 10:
 37 |                     _log.info(f"telemetry_decorator sync: tool={tool_name}")
 38 |                     _decorator_log_count += 1
 39 |                 result = func(*args, **kwargs)
 40 |                 success = True
 41 |                 action_val = sub_action or kwargs.get("action")
 42 |                 try:
 43 |                     if tool_name == "manage_script" and action_val == "create":
 44 |                         record_milestone(MilestoneType.FIRST_SCRIPT_CREATION)
 45 |                     elif tool_name.startswith("manage_scene"):
 46 |                         record_milestone(
 47 |                             MilestoneType.FIRST_SCENE_MODIFICATION)
 48 |                     record_milestone(MilestoneType.FIRST_TOOL_USAGE)
 49 |                 except Exception:
 50 |                     _log.debug("milestone emit failed", exc_info=True)
 51 |                 return result
 52 |             except Exception as e:
 53 |                 error = str(e)
 54 |                 raise
 55 |             finally:
 56 |                 duration_ms = (time.time() - start_time) * 1000
 57 |                 try:
 58 |                     record_tool_usage(tool_name, success,
 59 |                                       duration_ms, error, sub_action=sub_action)
 60 |                 except Exception:
 61 |                     _log.debug("record_tool_usage failed", exc_info=True)
 62 | 
 63 |         @functools.wraps(func)
 64 |         async def _async_wrapper(*args, **kwargs) -> Any:
 65 |             start_time = time.time()
 66 |             success = False
 67 |             error = None
 68 |             # Extract sub-action (e.g., 'get_hierarchy') from bound args when available
 69 |             sub_action = None
 70 |             try:
 71 |                 sig = inspect.signature(func)
 72 |                 bound = sig.bind_partial(*args, **kwargs)
 73 |                 bound.apply_defaults()
 74 |                 sub_action = bound.arguments.get("action")
 75 |             except Exception:
 76 |                 sub_action = None
 77 |             try:
 78 |                 global _decorator_log_count
 79 |                 if _decorator_log_count < 10:
 80 |                     _log.info(f"telemetry_decorator async: tool={tool_name}")
 81 |                     _decorator_log_count += 1
 82 |                 result = await func(*args, **kwargs)
 83 |                 success = True
 84 |                 action_val = sub_action or kwargs.get("action")
 85 |                 try:
 86 |                     if tool_name == "manage_script" and action_val == "create":
 87 |                         record_milestone(MilestoneType.FIRST_SCRIPT_CREATION)
 88 |                     elif tool_name.startswith("manage_scene"):
 89 |                         record_milestone(
 90 |                             MilestoneType.FIRST_SCENE_MODIFICATION)
 91 |                     record_milestone(MilestoneType.FIRST_TOOL_USAGE)
 92 |                 except Exception:
 93 |                     _log.debug("milestone emit failed", exc_info=True)
 94 |                 return result
 95 |             except Exception as e:
 96 |                 error = str(e)
 97 |                 raise
 98 |             finally:
 99 |                 duration_ms = (time.time() - start_time) * 1000
100 |                 try:
101 |                     record_tool_usage(tool_name, success,
102 |                                       duration_ms, error, sub_action=sub_action)
103 |                 except Exception:
104 |                     _log.debug("record_tool_usage failed", exc_info=True)
105 | 
106 |         return _async_wrapper if inspect.iscoroutinefunction(func) else _sync_wrapper
107 |     return decorator
108 | 
109 | 
110 | def telemetry_resource(resource_name: str):
111 |     """Decorator to add telemetry tracking to MCP resources"""
112 |     def decorator(func: Callable) -> Callable:
113 |         @functools.wraps(func)
114 |         def _sync_wrapper(*args, **kwargs) -> Any:
115 |             start_time = time.time()
116 |             success = False
117 |             error = None
118 |             try:
119 |                 global _decorator_log_count
120 |                 if _decorator_log_count < 10:
121 |                     _log.info(
122 |                         f"telemetry_decorator sync: resource={resource_name}")
123 |                     _decorator_log_count += 1
124 |                 result = func(*args, **kwargs)
125 |                 success = True
126 |                 return result
127 |             except Exception as e:
128 |                 error = str(e)
129 |                 raise
130 |             finally:
131 |                 duration_ms = (time.time() - start_time) * 1000
132 |                 try:
133 |                     record_resource_usage(resource_name, success,
134 |                                           duration_ms, error)
135 |                 except Exception:
136 |                     _log.debug("record_resource_usage failed", exc_info=True)
137 | 
138 |         @functools.wraps(func)
139 |         async def _async_wrapper(*args, **kwargs) -> Any:
140 |             start_time = time.time()
141 |             success = False
142 |             error = None
143 |             try:
144 |                 global _decorator_log_count
145 |                 if _decorator_log_count < 10:
146 |                     _log.info(
147 |                         f"telemetry_decorator async: resource={resource_name}")
148 |                     _decorator_log_count += 1
149 |                 result = await func(*args, **kwargs)
150 |                 success = True
151 |                 return result
152 |             except Exception as e:
153 |                 error = str(e)
154 |                 raise
155 |             finally:
156 |                 duration_ms = (time.time() - start_time) * 1000
157 |                 try:
158 |                     record_resource_usage(resource_name, success,
159 |                                           duration_ms, error)
160 |                 except Exception:
161 |                     _log.debug("record_resource_usage failed", exc_info=True)
162 | 
163 |         return _async_wrapper if inspect.iscoroutinefunction(func) else _sync_wrapper
164 |     return decorator
165 | 
```
Page 3/18FirstPrevNextLast