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

# Directory Structure

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

# Files

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

```python
 1 | from typing import Annotated, Any, Literal
 2 | 
 3 | from mcp.server.fastmcp import Context
 4 | from registry import mcp_for_unity_tool
 5 | from unity_connection import send_command_with_retry
 6 | 
 7 | 
 8 | @mcp_for_unity_tool(
 9 |     description="Bridge for prefab management commands (stage control and creation)."
10 | )
11 | def manage_prefabs(
12 |     ctx: Context,
13 |     action: Annotated[Literal[
14 |         "open_stage",
15 |         "close_stage",
16 |         "save_open_stage",
17 |         "create_from_gameobject",
18 |     ], "Manage prefabs (stage control and creation)."],
19 |     prefab_path: Annotated[str,
20 |                            "Prefab asset path relative to Assets e.g. Assets/Prefabs/favorite.prefab"] | None = None,
21 |     mode: Annotated[str,
22 |                     "Optional prefab stage mode (only 'InIsolation' is currently supported)"] | None = None,
23 |     save_before_close: Annotated[bool,
24 |                                  "When true, `close_stage` will save the prefab before exiting the stage."] | None = None,
25 |     target: Annotated[str,
26 |                       "Scene GameObject name required for create_from_gameobject"] | None = None,
27 |     allow_overwrite: Annotated[bool,
28 |                                "Allow replacing an existing prefab at the same path"] | None = None,
29 |     search_inactive: Annotated[bool,
30 |                                "Include inactive objects when resolving the target name"] | None = None,
31 | ) -> dict[str, Any]:
32 |     ctx.info(f"Processing manage_prefabs: {action}")
33 |     try:
34 |         params: dict[str, Any] = {"action": action}
35 | 
36 |         if prefab_path:
37 |             params["prefabPath"] = prefab_path
38 |         if mode:
39 |             params["mode"] = mode
40 |         if save_before_close is not None:
41 |             params["saveBeforeClose"] = bool(save_before_close)
42 |         if target:
43 |             params["target"] = target
44 |         if allow_overwrite is not None:
45 |             params["allowOverwrite"] = bool(allow_overwrite)
46 |         if search_inactive is not None:
47 |             params["searchInactive"] = bool(search_inactive)
48 |         response = send_command_with_retry("manage_prefabs", params)
49 | 
50 |         if isinstance(response, dict) and response.get("success"):
51 |             return {
52 |                 "success": True,
53 |                 "message": response.get("message", "Prefab operation successful."),
54 |                 "data": response.get("data"),
55 |             }
56 |         return response if isinstance(response, dict) else {"success": False, "message": str(response)}
57 |     except Exception as exc:
58 |         return {"success": False, "message": f"Python error managing prefabs: {exc}"}
59 | 
```

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

```python
 1 | from typing import Annotated, Any, Literal
 2 | 
 3 | from mcp.server.fastmcp import Context
 4 | from registry import mcp_for_unity_tool
 5 | from unity_connection import send_command_with_retry
 6 | 
 7 | 
 8 | @mcp_for_unity_tool(
 9 |     description="Bridge for prefab management commands (stage control and creation)."
10 | )
11 | def manage_prefabs(
12 |     ctx: Context,
13 |     action: Annotated[Literal[
14 |         "open_stage",
15 |         "close_stage",
16 |         "save_open_stage",
17 |         "create_from_gameobject",
18 |     ], "Manage prefabs (stage control and creation)."],
19 |     prefab_path: Annotated[str,
20 |                            "Prefab asset path relative to Assets e.g. Assets/Prefabs/favorite.prefab"] | None = None,
21 |     mode: Annotated[str,
22 |                     "Optional prefab stage mode (only 'InIsolation' is currently supported)"] | None = None,
23 |     save_before_close: Annotated[bool,
24 |                                  "When true, `close_stage` will save the prefab before exiting the stage."] | None = None,
25 |     target: Annotated[str,
26 |                       "Scene GameObject name required for create_from_gameobject"] | None = None,
27 |     allow_overwrite: Annotated[bool,
28 |                                "Allow replacing an existing prefab at the same path"] | None = None,
29 |     search_inactive: Annotated[bool,
30 |                                "Include inactive objects when resolving the target name"] | None = None,
31 | ) -> dict[str, Any]:
32 |     ctx.info(f"Processing manage_prefabs: {action}")
33 |     try:
34 |         params: dict[str, Any] = {"action": action}
35 | 
36 |         if prefab_path:
37 |             params["prefabPath"] = prefab_path
38 |         if mode:
39 |             params["mode"] = mode
40 |         if save_before_close is not None:
41 |             params["saveBeforeClose"] = bool(save_before_close)
42 |         if target:
43 |             params["target"] = target
44 |         if allow_overwrite is not None:
45 |             params["allowOverwrite"] = bool(allow_overwrite)
46 |         if search_inactive is not None:
47 |             params["searchInactive"] = bool(search_inactive)
48 |         response = send_command_with_retry("manage_prefabs", params)
49 | 
50 |         if isinstance(response, dict) and response.get("success"):
51 |             return {
52 |                 "success": True,
53 |                 "message": response.get("message", "Prefab operation successful."),
54 |                 "data": response.get("data"),
55 |             }
56 |         return response if isinstance(response, dict) else {"success": False, "message": str(response)}
57 |     except Exception as exc:
58 |         return {"success": False, "message": f"Python error managing prefabs: {exc}"}
59 | 
```

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

```python
 1 | import base64
 2 | from typing import Annotated, Any, Literal
 3 | 
 4 | from mcp.server.fastmcp import Context
 5 | from registry import mcp_for_unity_tool
 6 | from unity_connection import send_command_with_retry
 7 | 
 8 | 
 9 | @mcp_for_unity_tool(
10 |     description="Manages shader scripts in Unity (create, read, update, delete)."
11 | )
12 | def manage_shader(
13 |     ctx: Context,
14 |     action: Annotated[Literal['create', 'read', 'update', 'delete'], "Perform CRUD operations on shader scripts."],
15 |     name: Annotated[str, "Shader name (no .cs extension)"],
16 |     path: Annotated[str, "Asset path (default: \"Assets/\")"],
17 |     contents: Annotated[str,
18 |                         "Shader code for 'create'/'update'"] | None = None,
19 | ) -> dict[str, Any]:
20 |     ctx.info(f"Processing manage_shader: {action}")
21 |     try:
22 |         # Prepare parameters for Unity
23 |         params = {
24 |             "action": action,
25 |             "name": name,
26 |             "path": path,
27 |         }
28 | 
29 |         # Base64 encode the contents if they exist to avoid JSON escaping issues
30 |         if contents is not None:
31 |             if action in ['create', 'update']:
32 |                 # Encode content for safer transmission
33 |                 params["encodedContents"] = base64.b64encode(
34 |                     contents.encode('utf-8')).decode('utf-8')
35 |                 params["contentsEncoded"] = True
36 |             else:
37 |                 params["contents"] = contents
38 | 
39 |         # Remove None values so they don't get sent as null
40 |         params = {k: v for k, v in params.items() if v is not None}
41 | 
42 |         # Send command via centralized retry helper
43 |         response = send_command_with_retry("manage_shader", params)
44 | 
45 |         # Process response from Unity
46 |         if isinstance(response, dict) and response.get("success"):
47 |             # If the response contains base64 encoded content, decode it
48 |             if response.get("data", {}).get("contentsEncoded"):
49 |                 decoded_contents = base64.b64decode(
50 |                     response["data"]["encodedContents"]).decode('utf-8')
51 |                 response["data"]["contents"] = decoded_contents
52 |                 del response["data"]["encodedContents"]
53 |                 del response["data"]["contentsEncoded"]
54 | 
55 |             return {"success": True, "message": response.get("message", "Operation successful."), "data": response.get("data")}
56 |         return response if isinstance(response, dict) else {"success": False, "message": str(response)}
57 | 
58 |     except Exception as e:
59 |         # Handle Python-side errors (e.g., connection issues)
60 |         return {"success": False, "message": f"Python error managing shader: {str(e)}"}
61 | 
```

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

```python
 1 | import base64
 2 | from typing import Annotated, Any, Literal
 3 | 
 4 | from mcp.server.fastmcp import Context
 5 | from registry import mcp_for_unity_tool
 6 | from unity_connection import send_command_with_retry
 7 | 
 8 | 
 9 | @mcp_for_unity_tool(
10 |     description="Manages shader scripts in Unity (create, read, update, delete)."
11 | )
12 | def manage_shader(
13 |     ctx: Context,
14 |     action: Annotated[Literal['create', 'read', 'update', 'delete'], "Perform CRUD operations on shader scripts."],
15 |     name: Annotated[str, "Shader name (no .cs extension)"],
16 |     path: Annotated[str, "Asset path (default: \"Assets/\")"],
17 |     contents: Annotated[str,
18 |                         "Shader code for 'create'/'update'"] | None = None,
19 | ) -> dict[str, Any]:
20 |     ctx.info(f"Processing manage_shader: {action}")
21 |     try:
22 |         # Prepare parameters for Unity
23 |         params = {
24 |             "action": action,
25 |             "name": name,
26 |             "path": path,
27 |         }
28 | 
29 |         # Base64 encode the contents if they exist to avoid JSON escaping issues
30 |         if contents is not None:
31 |             if action in ['create', 'update']:
32 |                 # Encode content for safer transmission
33 |                 params["encodedContents"] = base64.b64encode(
34 |                     contents.encode('utf-8')).decode('utf-8')
35 |                 params["contentsEncoded"] = True
36 |             else:
37 |                 params["contents"] = contents
38 | 
39 |         # Remove None values so they don't get sent as null
40 |         params = {k: v for k, v in params.items() if v is not None}
41 | 
42 |         # Send command via centralized retry helper
43 |         response = send_command_with_retry("manage_shader", params)
44 | 
45 |         # Process response from Unity
46 |         if isinstance(response, dict) and response.get("success"):
47 |             # If the response contains base64 encoded content, decode it
48 |             if response.get("data", {}).get("contentsEncoded"):
49 |                 decoded_contents = base64.b64decode(
50 |                     response["data"]["encodedContents"]).decode('utf-8')
51 |                 response["data"]["contents"] = decoded_contents
52 |                 del response["data"]["encodedContents"]
53 |                 del response["data"]["contentsEncoded"]
54 | 
55 |             return {"success": True, "message": response.get("message", "Operation successful."), "data": response.get("data")}
56 |         return response if isinstance(response, dict) else {"success": False, "message": str(response)}
57 | 
58 |     except Exception as e:
59 |         # Handle Python-side errors (e.g., connection issues)
60 |         return {"success": False, "message": f"Python error managing shader: {str(e)}"}
61 | 
```

--------------------------------------------------------------------------------
/tests/test_logging_stdout.py:
--------------------------------------------------------------------------------

```python
 1 | import ast
 2 | from pathlib import Path
 3 | 
 4 | import pytest
 5 | 
 6 | 
 7 | # locate server src dynamically to avoid hardcoded layout assumptions
 8 | ROOT = Path(__file__).resolve().parents[1]
 9 | candidates = [
10 |     ROOT / "MCPForUnity" / "UnityMcpServer~" / "src",
11 |     ROOT / "UnityMcpServer~" / "src",
12 | ]
13 | SRC = next((p for p in candidates if p.exists()), None)
14 | if SRC is None:
15 |     searched = "\n".join(str(p) for p in candidates)
16 |     pytest.skip(
17 |         "MCP for Unity server source not found. Tried:\n" + searched,
18 |         allow_module_level=True,
19 |     )
20 | 
21 | 
22 | @pytest.mark.skip(reason="TODO: ensure server logs only to stderr and rotating file")
23 | def test_no_stdout_output_from_tools():
24 |     pass
25 | 
26 | 
27 | def test_no_print_statements_in_codebase():
28 |     """Ensure no stray print/sys.stdout writes remain in server source."""
29 |     offenders = []
30 |     syntax_errors = []
31 |     for py_file in SRC.rglob("*.py"):
32 |         # Skip virtual envs and third-party packages if they exist under SRC
33 |         parts = set(py_file.parts)
34 |         if ".venv" in parts or "site-packages" in parts:
35 |             continue
36 |         try:
37 |             text = py_file.read_text(encoding="utf-8", errors="strict")
38 |         except UnicodeDecodeError:
39 |             # Be tolerant of encoding edge cases in source tree without silently dropping bytes
40 |             text = py_file.read_text(encoding="utf-8", errors="replace")
41 |         try:
42 |             tree = ast.parse(text, filename=str(py_file))
43 |         except SyntaxError:
44 |             syntax_errors.append(py_file.relative_to(SRC))
45 |             continue
46 | 
47 |         class StdoutVisitor(ast.NodeVisitor):
48 |             def __init__(self):
49 |                 self.hit = False
50 | 
51 |             def visit_Call(self, node: ast.Call):
52 |                 # print(...)
53 |                 if isinstance(node.func, ast.Name) and node.func.id == "print":
54 |                     self.hit = True
55 |                 # sys.stdout.write(...)
56 |                 if isinstance(node.func, ast.Attribute) and node.func.attr == "write":
57 |                     val = node.func.value
58 |                     if isinstance(val, ast.Attribute) and val.attr == "stdout":
59 |                         if isinstance(val.value, ast.Name) and val.value.id == "sys":
60 |                             self.hit = True
61 |                 self.generic_visit(node)
62 | 
63 |         v = StdoutVisitor()
64 |         v.visit(tree)
65 |         if v.hit:
66 |             offenders.append(py_file.relative_to(SRC))
67 |     assert not syntax_errors, "syntax errors in: " + \
68 |         ", ".join(str(e) for e in syntax_errors)
69 |     assert not offenders, "stdout writes found in: " + \
70 |         ", ".join(str(o) for o in offenders)
71 | 
```

--------------------------------------------------------------------------------
/tests/test_telemetry_queue_worker.py:
--------------------------------------------------------------------------------

```python
 1 | import sys
 2 | import pathlib
 3 | import importlib.util
 4 | import types
 5 | import threading
 6 | import time
 7 | import queue as q
 8 | 
 9 | 
10 | ROOT = pathlib.Path(__file__).resolve().parents[1]
11 | SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src"
12 | sys.path.insert(0, str(SRC))
13 | 
14 | # Stub mcp.server.fastmcp to satisfy imports without the full dependency
15 | mcp_pkg = types.ModuleType("mcp")
16 | server_pkg = types.ModuleType("mcp.server")
17 | fastmcp_pkg = types.ModuleType("mcp.server.fastmcp")
18 | 
19 | 
20 | class _Dummy:
21 |     pass
22 | 
23 | 
24 | fastmcp_pkg.FastMCP = _Dummy
25 | fastmcp_pkg.Context = _Dummy
26 | server_pkg.fastmcp = fastmcp_pkg
27 | mcp_pkg.server = server_pkg
28 | sys.modules.setdefault("mcp", mcp_pkg)
29 | sys.modules.setdefault("mcp.server", server_pkg)
30 | sys.modules.setdefault("mcp.server.fastmcp", fastmcp_pkg)
31 | 
32 | 
33 | def _load_module(path: pathlib.Path, name: str):
34 |     spec = importlib.util.spec_from_file_location(name, path)
35 |     mod = importlib.util.module_from_spec(spec)
36 |     spec.loader.exec_module(mod)
37 |     return mod
38 | 
39 | 
40 | telemetry = _load_module(SRC / "telemetry.py", "telemetry_mod")
41 | 
42 | 
43 | def test_telemetry_queue_backpressure_and_single_worker(monkeypatch, caplog):
44 |     caplog.set_level("DEBUG")
45 | 
46 |     collector = telemetry.TelemetryCollector()
47 |     # Force-enable telemetry regardless of env settings from conftest
48 |     collector.config.enabled = True
49 | 
50 |     # Wake existing worker once so it observes the new queue on the next loop
51 |     collector.record(telemetry.RecordType.TOOL_EXECUTION, {"i": -1})
52 |     # Replace queue with tiny one to trigger backpressure quickly
53 |     small_q = q.Queue(maxsize=2)
54 |     collector._queue = small_q
55 |     # Give the worker a moment to switch queues
56 |     time.sleep(0.02)
57 | 
58 |     # Make sends slow to build backlog and exercise worker
59 |     def slow_send(self, rec):
60 |         time.sleep(0.05)
61 | 
62 |     collector._send_telemetry = types.MethodType(slow_send, collector)
63 | 
64 |     # Fire many events quickly; record() should not block even when queue fills
65 |     start = time.perf_counter()
66 |     for i in range(50):
67 |         collector.record(telemetry.RecordType.TOOL_EXECUTION, {"i": i})
68 |     elapsed_ms = (time.perf_counter() - start) * 1000.0
69 | 
70 |     # Should be fast despite backpressure (non-blocking enqueue or drop)
71 |     assert elapsed_ms < 80.0
72 | 
73 |     # Allow worker to process some
74 |     time.sleep(0.3)
75 | 
76 |     # Verify drops were logged (queue full backpressure)
77 |     dropped_logs = [
78 |         m for m in caplog.messages if "Telemetry queue full; dropping" in m]
79 |     assert len(dropped_logs) >= 1
80 | 
81 |     # Ensure only one worker thread exists and is alive
82 |     assert collector._worker.is_alive()
83 |     worker_threads = [
84 |         t for t in threading.enumerate() if t is collector._worker]
85 |     assert len(worker_threads) == 1
86 | 
```

--------------------------------------------------------------------------------
/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/ManualConfigJsonBuilderTests.cs:
--------------------------------------------------------------------------------

```csharp
 1 | using Newtonsoft.Json.Linq;
 2 | using NUnit.Framework;
 3 | using MCPForUnity.Editor.Helpers;
 4 | using MCPForUnity.Editor.Models;
 5 | 
 6 | namespace MCPForUnityTests.Editor.Windows
 7 | {
 8 |     public class ManualConfigJsonBuilderTests
 9 |     {
10 |         [Test]
11 |         public void VSCode_ManualJson_HasServers_NoEnv_NoDisabled()
12 |         {
13 |             var client = new McpClient { name = "VSCode", mcpType = McpTypes.VSCode };
14 |             string json = ConfigJsonBuilder.BuildManualConfigJson("/usr/bin/uv", "/path/to/server", client);
15 | 
16 |             var root = JObject.Parse(json);
17 |             var unity = (JObject)root.SelectToken("servers.unityMCP");
18 |             Assert.NotNull(unity, "Expected servers.unityMCP node");
19 |             Assert.AreEqual("/usr/bin/uv", (string)unity["command"]);
20 |             CollectionAssert.AreEqual(new[] { "run", "--directory", "/path/to/server", "server.py" }, unity["args"].ToObject<string[]>());
21 |             Assert.AreEqual("stdio", (string)unity["type"], "VSCode should include type=stdio");
22 |             Assert.IsNull(unity["env"], "env should not be added for VSCode");
23 |             Assert.IsNull(unity["disabled"], "disabled should not be added for VSCode");
24 |         }
25 | 
26 |         [Test]
27 |         public void Windsurf_ManualJson_HasMcpServersEnv_DisabledFalse()
28 |         {
29 |             var client = new McpClient { name = "Windsurf", mcpType = McpTypes.Windsurf };
30 |             string json = ConfigJsonBuilder.BuildManualConfigJson("/usr/bin/uv", "/path/to/server", client);
31 | 
32 |             var root = JObject.Parse(json);
33 |             var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
34 |             Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
35 |             Assert.NotNull(unity["env"], "env should be included");
36 |             Assert.AreEqual(false, (bool)unity["disabled"], "disabled:false should be added for Windsurf");
37 |             Assert.IsNull(unity["type"], "type should not be added for non-VSCode clients");
38 |         }
39 | 
40 |         [Test]
41 |         public void Cursor_ManualJson_HasMcpServers_NoEnv_NoDisabled()
42 |         {
43 |             var client = new McpClient { name = "Cursor", mcpType = McpTypes.Cursor };
44 |             string json = ConfigJsonBuilder.BuildManualConfigJson("/usr/bin/uv", "/path/to/server", client);
45 | 
46 |             var root = JObject.Parse(json);
47 |             var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
48 |             Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
49 |             Assert.IsNull(unity["env"], "env should not be added for Cursor");
50 |             Assert.IsNull(unity["disabled"], "disabled should not be added for Cursor");
51 |             Assert.IsNull(unity["type"], "type should not be added for non-VSCode clients");
52 |         }
53 |     }
54 | }
55 | 
```

--------------------------------------------------------------------------------
/tests/test_resources_api.py:
--------------------------------------------------------------------------------

```python
 1 | from tools.resource_tools import register_resource_tools  # type: ignore
 2 | import pytest
 3 | 
 4 | 
 5 | import sys
 6 | from pathlib import Path
 7 | import pytest
 8 | import types
 9 | 
10 | # locate server src dynamically to avoid hardcoded layout assumptions
11 | ROOT = Path(__file__).resolve().parents[1]
12 | candidates = [
13 |     ROOT / "MCPForUnity" / "UnityMcpServer~" / "src",
14 |     ROOT / "UnityMcpServer~" / "src",
15 | ]
16 | SRC = next((p for p in candidates if p.exists()), None)
17 | if SRC is None:
18 |     searched = "\n".join(str(p) for p in candidates)
19 |     pytest.skip(
20 |         "MCP for Unity server source not found. Tried:\n" + searched,
21 |         allow_module_level=True,
22 |     )
23 | sys.path.insert(0, str(SRC))
24 | 
25 | 
26 | class DummyMCP:
27 |     def __init__(self):
28 |         self._tools = {}
29 | 
30 |     def tool(self, *args, **kwargs):  # accept kwargs like description
31 |         def deco(fn):
32 |             self._tools[fn.__name__] = fn
33 |             return fn
34 |         return deco
35 | 
36 | 
37 | @pytest.fixture()
38 | def resource_tools():
39 |     mcp = DummyMCP()
40 |     register_resource_tools(mcp)
41 |     return mcp._tools
42 | 
43 | 
44 | def test_resource_list_filters_and_rejects_traversal(resource_tools, tmp_path, monkeypatch):
45 |     # Create fake project structure
46 |     proj = tmp_path
47 |     assets = proj / "Assets" / "Scripts"
48 |     assets.mkdir(parents=True)
49 |     (assets / "A.cs").write_text("// a", encoding="utf-8")
50 |     (assets / "B.txt").write_text("b", encoding="utf-8")
51 |     outside = tmp_path / "Outside.cs"
52 |     outside.write_text("// outside", encoding="utf-8")
53 |     # Symlink attempting to escape
54 |     sneaky_link = assets / "link_out"
55 |     try:
56 |         sneaky_link.symlink_to(outside)
57 |     except Exception:
58 |         # Some platforms may not allow symlinks in tests; ignore
59 |         pass
60 | 
61 |     list_resources = resource_tools["list_resources"]
62 |     # Only .cs under Assets should be listed
63 |     import asyncio
64 |     resp = asyncio.get_event_loop().run_until_complete(
65 |         list_resources(ctx=None, pattern="*.cs", under="Assets",
66 |                        limit=50, project_root=str(proj))
67 |     )
68 |     assert resp["success"] is True
69 |     uris = resp["data"]["uris"]
70 |     assert any(u.endswith("Assets/Scripts/A.cs") for u in uris)
71 |     assert not any(u.endswith("B.txt") for u in uris)
72 |     assert not any(u.endswith("Outside.cs") for u in uris)
73 | 
74 | 
75 | def test_resource_list_rejects_outside_paths(resource_tools, tmp_path):
76 |     proj = tmp_path
77 |     # under points outside Assets
78 |     list_resources = resource_tools["list_resources"]
79 |     import asyncio
80 |     resp = asyncio.get_event_loop().run_until_complete(
81 |         list_resources(ctx=None, pattern="*.cs", under="..",
82 |                        limit=10, project_root=str(proj))
83 |     )
84 |     assert resp["success"] is False
85 |     assert "Assets" in resp.get(
86 |         "error", "") or "under project root" in resp.get("error", "")
87 | 
```

--------------------------------------------------------------------------------
/tests/test_telemetry_subaction.py:
--------------------------------------------------------------------------------

```python
 1 | import importlib
 2 | 
 3 | 
 4 | def _get_decorator_module():
 5 |     # Import the telemetry_decorator module from the MCP for Unity server src
 6 |     mod = importlib.import_module(
 7 |         "MCPForUnity.UnityMcpServer~.src.telemetry_decorator")
 8 |     return mod
 9 | 
10 | 
11 | def test_subaction_extracted_from_keyword(monkeypatch):
12 |     td = _get_decorator_module()
13 | 
14 |     captured = {}
15 | 
16 |     def fake_record_tool_usage(tool_name, success, duration_ms, error, sub_action=None):
17 |         captured["tool_name"] = tool_name
18 |         captured["success"] = success
19 |         captured["error"] = error
20 |         captured["sub_action"] = sub_action
21 | 
22 |     # Silence milestones/logging in test
23 |     monkeypatch.setattr(td, "record_tool_usage", fake_record_tool_usage)
24 |     monkeypatch.setattr(td, "record_milestone", lambda *a, **k: None)
25 |     monkeypatch.setattr(td, "_decorator_log_count", 999)
26 | 
27 |     def dummy_tool(ctx, action: str, name: str = ""):
28 |         return {"success": True, "name": name}
29 | 
30 |     wrapped = td.telemetry_tool("manage_scene")(dummy_tool)
31 | 
32 |     resp = wrapped(None, action="get_hierarchy", name="Sample")
33 |     assert resp["success"] is True
34 |     assert captured["tool_name"] == "manage_scene"
35 |     assert captured["success"] is True
36 |     assert captured["error"] is None
37 |     assert captured["sub_action"] == "get_hierarchy"
38 | 
39 | 
40 | def test_subaction_extracted_from_positionals(monkeypatch):
41 |     td = _get_decorator_module()
42 | 
43 |     captured = {}
44 | 
45 |     def fake_record_tool_usage(tool_name, success, duration_ms, error, sub_action=None):
46 |         captured["tool_name"] = tool_name
47 |         captured["sub_action"] = sub_action
48 | 
49 |     monkeypatch.setattr(td, "record_tool_usage", fake_record_tool_usage)
50 |     monkeypatch.setattr(td, "record_milestone", lambda *a, **k: None)
51 |     monkeypatch.setattr(td, "_decorator_log_count", 999)
52 | 
53 |     def dummy_tool(ctx, action: str, name: str = ""):
54 |         return True
55 | 
56 |     wrapped = td.telemetry_tool("manage_scene")(dummy_tool)
57 | 
58 |     _ = wrapped(None, "save", "MyScene")
59 |     assert captured["tool_name"] == "manage_scene"
60 |     assert captured["sub_action"] == "save"
61 | 
62 | 
63 | def test_subaction_none_when_not_present(monkeypatch):
64 |     td = _get_decorator_module()
65 | 
66 |     captured = {}
67 | 
68 |     def fake_record_tool_usage(tool_name, success, duration_ms, error, sub_action=None):
69 |         captured["tool_name"] = tool_name
70 |         captured["sub_action"] = sub_action
71 | 
72 |     monkeypatch.setattr(td, "record_tool_usage", fake_record_tool_usage)
73 |     monkeypatch.setattr(td, "record_milestone", lambda *a, **k: None)
74 |     monkeypatch.setattr(td, "_decorator_log_count", 999)
75 | 
76 |     def dummy_tool_without_action(ctx, name: str):
77 |         return 123
78 | 
79 |     wrapped = td.telemetry_tool("apply_text_edits")(dummy_tool_without_action)
80 |     _ = wrapped(None, name="X")
81 |     assert captured["tool_name"] == "apply_text_edits"
82 |     assert captured["sub_action"] is None
83 | 
```

--------------------------------------------------------------------------------
/tests/test_edit_strict_and_warnings.py:
--------------------------------------------------------------------------------

```python
 1 | import sys
 2 | import pathlib
 3 | import importlib.util
 4 | import types
 5 | 
 6 | 
 7 | ROOT = pathlib.Path(__file__).resolve().parents[1]
 8 | SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src"
 9 | sys.path.insert(0, str(SRC))
10 | 
11 | # stub mcp.server.fastmcp
12 | mcp_pkg = types.ModuleType("mcp")
13 | server_pkg = types.ModuleType("mcp.server")
14 | fastmcp_pkg = types.ModuleType("mcp.server.fastmcp")
15 | 
16 | 
17 | class _Dummy:
18 |     pass
19 | 
20 | 
21 | fastmcp_pkg.FastMCP = _Dummy
22 | fastmcp_pkg.Context = _Dummy
23 | server_pkg.fastmcp = fastmcp_pkg
24 | mcp_pkg.server = server_pkg
25 | sys.modules.setdefault("mcp", mcp_pkg)
26 | sys.modules.setdefault("mcp.server", server_pkg)
27 | sys.modules.setdefault("mcp.server.fastmcp", fastmcp_pkg)
28 | 
29 | 
30 | def _load(path: pathlib.Path, name: str):
31 |     spec = importlib.util.spec_from_file_location(name, path)
32 |     mod = importlib.util.module_from_spec(spec)
33 |     spec.loader.exec_module(mod)
34 |     return mod
35 | 
36 | 
37 | manage_script = _load(SRC / "tools" / "manage_script.py", "manage_script_mod3")
38 | 
39 | 
40 | class DummyMCP:
41 |     def __init__(self): self.tools = {}
42 | 
43 |     def tool(self, *args, **kwargs):
44 |         def deco(fn): self.tools[fn.__name__] = fn; return fn
45 |         return deco
46 | 
47 | 
48 | def setup_tools():
49 |     mcp = DummyMCP()
50 |     manage_script.register_manage_script_tools(mcp)
51 |     return mcp.tools
52 | 
53 | 
54 | def test_explicit_zero_based_normalized_warning(monkeypatch):
55 |     tools = setup_tools()
56 |     apply_edits = tools["apply_text_edits"]
57 | 
58 |     def fake_send(cmd, params):
59 |         # Simulate Unity path returning minimal success
60 |         return {"success": True}
61 | 
62 |     monkeypatch.setattr(manage_script, "send_command_with_retry", fake_send)
63 | 
64 |     # Explicit fields given as 0-based (invalid); SDK should normalize and warn
65 |     edits = [{"startLine": 0, "startCol": 0,
66 |               "endLine": 0, "endCol": 0, "newText": "//x"}]
67 |     resp = apply_edits(None, uri="unity://path/Assets/Scripts/F.cs",
68 |                        edits=edits, precondition_sha256="sha")
69 | 
70 |     assert resp["success"] is True
71 |     data = resp.get("data", {})
72 |     assert "normalizedEdits" in data
73 |     assert any(
74 |         w == "zero_based_explicit_fields_normalized" for w in data.get("warnings", []))
75 |     ne = data["normalizedEdits"][0]
76 |     assert ne["startLine"] == 1 and ne["startCol"] == 1 and ne["endLine"] == 1 and ne["endCol"] == 1
77 | 
78 | 
79 | def test_strict_zero_based_error(monkeypatch):
80 |     tools = setup_tools()
81 |     apply_edits = tools["apply_text_edits"]
82 | 
83 |     def fake_send(cmd, params):
84 |         return {"success": True}
85 | 
86 |     monkeypatch.setattr(manage_script, "send_command_with_retry", fake_send)
87 | 
88 |     edits = [{"startLine": 0, "startCol": 0,
89 |               "endLine": 0, "endCol": 0, "newText": "//x"}]
90 |     resp = apply_edits(None, uri="unity://path/Assets/Scripts/F.cs",
91 |                        edits=edits, precondition_sha256="sha", strict=True)
92 |     assert resp["success"] is False
93 |     assert resp.get("code") == "zero_based_explicit_fields"
94 | 
```

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

```python
 1 | from typing import Annotated, Any, Literal
 2 | 
 3 | from mcp.server.fastmcp import Context
 4 | from registry import mcp_for_unity_tool
 5 | from telemetry import is_telemetry_enabled, record_tool_usage
 6 | from unity_connection import send_command_with_retry
 7 | 
 8 | 
 9 | @mcp_for_unity_tool(
10 |     description="Controls and queries the Unity editor's state and settings"
11 | )
12 | def manage_editor(
13 |     ctx: Context,
14 |     action: Annotated[Literal["telemetry_status", "telemetry_ping", "play", "pause", "stop", "get_state", "get_project_root", "get_windows",
15 |                               "get_active_tool", "get_selection", "get_prefab_stage", "set_active_tool", "add_tag", "remove_tag", "get_tags", "add_layer", "remove_layer", "get_layers"], "Get and update the Unity Editor state."],
16 |     wait_for_completion: Annotated[bool,
17 |                                    "Optional. If True, waits for certain actions"] | None = None,
18 |     tool_name: Annotated[str,
19 |                          "Tool name when setting active tool"] | None = None,
20 |     tag_name: Annotated[str,
21 |                         "Tag name when adding and removing tags"] | None = None,
22 |     layer_name: Annotated[str,
23 |                           "Layer name when adding and removing layers"] | None = None,
24 | ) -> dict[str, Any]:
25 |     ctx.info(f"Processing manage_editor: {action}")
26 |     try:
27 |         # Diagnostics: quick telemetry checks
28 |         if action == "telemetry_status":
29 |             return {"success": True, "telemetry_enabled": is_telemetry_enabled()}
30 | 
31 |         if action == "telemetry_ping":
32 |             record_tool_usage("diagnostic_ping", True, 1.0, None)
33 |             return {"success": True, "message": "telemetry ping queued"}
34 |         # Prepare parameters, removing None values
35 |         params = {
36 |             "action": action,
37 |             "waitForCompletion": wait_for_completion,
38 |             "toolName": tool_name,  # Corrected parameter name to match C#
39 |             "tagName": tag_name,   # Pass tag name
40 |             "layerName": layer_name,  # Pass layer name
41 |             # Add other parameters based on the action being performed
42 |             # "width": width,
43 |             # "height": height,
44 |             # etc.
45 |         }
46 |         params = {k: v for k, v in params.items() if v is not None}
47 | 
48 |         # Send command using centralized retry helper
49 |         response = send_command_with_retry("manage_editor", params)
50 | 
51 |         # Preserve structured failure data; unwrap success into a friendlier shape
52 |         if isinstance(response, dict) and response.get("success"):
53 |             return {"success": True, "message": response.get("message", "Editor operation successful."), "data": response.get("data")}
54 |         return response if isinstance(response, dict) else {"success": False, "message": str(response)}
55 | 
56 |     except Exception as e:
57 |         return {"success": False, "message": f"Python error managing editor: {str(e)}"}
58 | 
```

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

```python
 1 | from typing import Annotated, Any, Literal
 2 | 
 3 | from mcp.server.fastmcp import Context
 4 | from registry import mcp_for_unity_tool
 5 | from telemetry import is_telemetry_enabled, record_tool_usage
 6 | from unity_connection import send_command_with_retry
 7 | 
 8 | 
 9 | @mcp_for_unity_tool(
10 |     description="Controls and queries the Unity editor's state and settings"
11 | )
12 | def manage_editor(
13 |     ctx: Context,
14 |     action: Annotated[Literal["telemetry_status", "telemetry_ping", "play", "pause", "stop", "get_state", "get_project_root", "get_windows",
15 |                               "get_active_tool", "get_selection", "get_prefab_stage", "set_active_tool", "add_tag", "remove_tag", "get_tags", "add_layer", "remove_layer", "get_layers"], "Get and update the Unity Editor state."],
16 |     wait_for_completion: Annotated[bool,
17 |                                    "Optional. If True, waits for certain actions"] | None = None,
18 |     tool_name: Annotated[str,
19 |                          "Tool name when setting active tool"] | None = None,
20 |     tag_name: Annotated[str,
21 |                         "Tag name when adding and removing tags"] | None = None,
22 |     layer_name: Annotated[str,
23 |                           "Layer name when adding and removing layers"] | None = None,
24 | ) -> dict[str, Any]:
25 |     ctx.info(f"Processing manage_editor: {action}")
26 |     try:
27 |         # Diagnostics: quick telemetry checks
28 |         if action == "telemetry_status":
29 |             return {"success": True, "telemetry_enabled": is_telemetry_enabled()}
30 | 
31 |         if action == "telemetry_ping":
32 |             record_tool_usage("diagnostic_ping", True, 1.0, None)
33 |             return {"success": True, "message": "telemetry ping queued"}
34 |         # Prepare parameters, removing None values
35 |         params = {
36 |             "action": action,
37 |             "waitForCompletion": wait_for_completion,
38 |             "toolName": tool_name,  # Corrected parameter name to match C#
39 |             "tagName": tag_name,   # Pass tag name
40 |             "layerName": layer_name,  # Pass layer name
41 |             # Add other parameters based on the action being performed
42 |             # "width": width,
43 |             # "height": height,
44 |             # etc.
45 |         }
46 |         params = {k: v for k, v in params.items() if v is not None}
47 | 
48 |         # Send command using centralized retry helper
49 |         response = send_command_with_retry("manage_editor", params)
50 | 
51 |         # Preserve structured failure data; unwrap success into a friendlier shape
52 |         if isinstance(response, dict) and response.get("success"):
53 |             return {"success": True, "message": response.get("message", "Editor operation successful."), "data": response.get("data")}
54 |         return response if isinstance(response, dict) else {"success": False, "message": str(response)}
55 | 
56 |     except Exception as e:
57 |         return {"success": False, "message": f"Python error managing editor: {str(e)}"}
58 | 
```

--------------------------------------------------------------------------------
/test_unity_socket_framing.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | import socket
  3 | import struct
  4 | import json
  5 | import sys
  6 | 
  7 | HOST = "127.0.0.1"
  8 | PORT = 6400
  9 | try:
 10 |     SIZE_MB = int(sys.argv[1])
 11 | except (IndexError, ValueError):
 12 |     SIZE_MB = 5  # e.g., 5 or 10
 13 | FILL = "R"
 14 | MAX_FRAME = 64 * 1024 * 1024
 15 | 
 16 | 
 17 | def recv_exact(sock, n):
 18 |     buf = bytearray(n)
 19 |     view = memoryview(buf)
 20 |     off = 0
 21 |     while off < n:
 22 |         r = sock.recv_into(view[off:])
 23 |         if r == 0:
 24 |             raise RuntimeError("socket closed")
 25 |         off += r
 26 |     return bytes(buf)
 27 | 
 28 | 
 29 | def is_valid_json(b):
 30 |     try:
 31 |         json.loads(b.decode("utf-8"))
 32 |         return True
 33 |     except Exception:
 34 |         return False
 35 | 
 36 | 
 37 | def recv_legacy_json(sock, timeout=60):
 38 |     sock.settimeout(timeout)
 39 |     chunks = []
 40 |     while True:
 41 |         chunk = sock.recv(65536)
 42 |         if not chunk:
 43 |             data = b"".join(chunks)
 44 |             if not data:
 45 |                 raise RuntimeError("no data, socket closed")
 46 |             return data
 47 |         chunks.append(chunk)
 48 |         data = b"".join(chunks)
 49 |         if data.strip() == b"ping":
 50 |             return data
 51 |         if is_valid_json(data):
 52 |             return data
 53 | 
 54 | 
 55 | def main():
 56 |     # Cap filler to stay within framing limit (reserve small overhead for JSON)
 57 |     safe_max = max(1, MAX_FRAME - 4096)
 58 |     filler_len = min(SIZE_MB * 1024 * 1024, safe_max)
 59 |     body = {
 60 |         "type": "read_console",
 61 |         "params": {
 62 |             "action": "get",
 63 |             "types": ["all"],
 64 |             "count": 1000,
 65 |             "format": "detailed",
 66 |             "includeStacktrace": True,
 67 |             "filterText": FILL * filler_len
 68 |         }
 69 |     }
 70 |     body_bytes = json.dumps(body, ensure_ascii=False).encode("utf-8")
 71 | 
 72 |     with socket.create_connection((HOST, PORT), timeout=5) as s:
 73 |         s.settimeout(2)
 74 |         # Read optional greeting
 75 |         try:
 76 |             greeting = s.recv(256)
 77 |         except Exception:
 78 |             greeting = b""
 79 |         greeting_text = greeting.decode("ascii", errors="ignore").strip()
 80 |         print(f"Greeting: {greeting_text or '(none)'}")
 81 | 
 82 |         framing = "FRAMING=1" in greeting_text
 83 |         print(f"Using framing? {framing}")
 84 | 
 85 |         s.settimeout(120)
 86 |         if framing:
 87 |             header = struct.pack(">Q", len(body_bytes))
 88 |             s.sendall(header + body_bytes)
 89 |             resp_len = struct.unpack(">Q", recv_exact(s, 8))[0]
 90 |             print(f"Response framed length: {resp_len}")
 91 |             MAX_RESP = MAX_FRAME
 92 |             if resp_len <= 0 or resp_len > MAX_RESP:
 93 |                 raise RuntimeError(
 94 |                     f"invalid framed length: {resp_len} (max {MAX_RESP})")
 95 |             resp = recv_exact(s, resp_len)
 96 |         else:
 97 |             s.sendall(body_bytes)
 98 |             resp = recv_legacy_json(s)
 99 | 
100 |         print(f"Response bytes: {len(resp)}")
101 |         print(f"Response head: {resp[:120].decode('utf-8', 'ignore')}")
102 | 
103 | 
104 | if __name__ == "__main__":
105 |     main()
106 | 
```

--------------------------------------------------------------------------------
/tests/test_read_console_truncate.py:
--------------------------------------------------------------------------------

```python
  1 | import sys
  2 | import pathlib
  3 | import importlib.util
  4 | import types
  5 | 
  6 | ROOT = pathlib.Path(__file__).resolve().parents[1]
  7 | SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src"
  8 | sys.path.insert(0, str(SRC))
  9 | 
 10 | # stub mcp.server.fastmcp
 11 | mcp_pkg = types.ModuleType("mcp")
 12 | server_pkg = types.ModuleType("mcp.server")
 13 | fastmcp_pkg = types.ModuleType("mcp.server.fastmcp")
 14 | 
 15 | 
 16 | class _Dummy:
 17 |     pass
 18 | 
 19 | 
 20 | fastmcp_pkg.FastMCP = _Dummy
 21 | fastmcp_pkg.Context = _Dummy
 22 | server_pkg.fastmcp = fastmcp_pkg
 23 | mcp_pkg.server = server_pkg
 24 | sys.modules.setdefault("mcp", mcp_pkg)
 25 | sys.modules.setdefault("mcp.server", server_pkg)
 26 | sys.modules.setdefault("mcp.server.fastmcp", fastmcp_pkg)
 27 | 
 28 | 
 29 | def _load_module(path: pathlib.Path, name: str):
 30 |     spec = importlib.util.spec_from_file_location(name, path)
 31 |     mod = importlib.util.module_from_spec(spec)
 32 |     spec.loader.exec_module(mod)
 33 |     return mod
 34 | 
 35 | 
 36 | read_console_mod = _load_module(
 37 |     SRC / "tools" / "read_console.py", "read_console_mod")
 38 | 
 39 | 
 40 | class DummyMCP:
 41 |     def __init__(self):
 42 |         self.tools = {}
 43 | 
 44 |     def tool(self, *args, **kwargs):
 45 |         def deco(fn):
 46 |             self.tools[fn.__name__] = fn
 47 |             return fn
 48 |         return deco
 49 | 
 50 | 
 51 | def setup_tools():
 52 |     mcp = DummyMCP()
 53 |     read_console_mod.register_read_console_tools(mcp)
 54 |     return mcp.tools
 55 | 
 56 | 
 57 | def test_read_console_full_default(monkeypatch):
 58 |     tools = setup_tools()
 59 |     read_console = tools["read_console"]
 60 | 
 61 |     captured = {}
 62 | 
 63 |     def fake_send(cmd, params):
 64 |         captured["params"] = params
 65 |         return {
 66 |             "success": True,
 67 |             "data": {"lines": [{"level": "error", "message": "oops", "stacktrace": "trace", "time": "t"}]},
 68 |         }
 69 | 
 70 |     monkeypatch.setattr(read_console_mod, "send_command_with_retry", fake_send)
 71 |     monkeypatch.setattr(
 72 |         read_console_mod, "get_unity_connection", lambda: object())
 73 | 
 74 |     resp = read_console(ctx=None, count=10)
 75 |     assert resp == {
 76 |         "success": True,
 77 |         "data": {"lines": [{"level": "error", "message": "oops", "stacktrace": "trace", "time": "t"}]},
 78 |     }
 79 |     assert captured["params"]["count"] == 10
 80 |     assert captured["params"]["includeStacktrace"] is True
 81 | 
 82 | 
 83 | def test_read_console_truncated(monkeypatch):
 84 |     tools = setup_tools()
 85 |     read_console = tools["read_console"]
 86 | 
 87 |     captured = {}
 88 | 
 89 |     def fake_send(cmd, params):
 90 |         captured["params"] = params
 91 |         return {
 92 |             "success": True,
 93 |             "data": {"lines": [{"level": "error", "message": "oops", "stacktrace": "trace"}]},
 94 |         }
 95 | 
 96 |     monkeypatch.setattr(read_console_mod, "send_command_with_retry", fake_send)
 97 |     monkeypatch.setattr(
 98 |         read_console_mod, "get_unity_connection", lambda: object())
 99 | 
100 |     resp = read_console(ctx=None, count=10, include_stacktrace=False)
101 |     assert resp == {"success": True, "data": {
102 |         "lines": [{"level": "error", "message": "oops"}]}}
103 |     assert captured["params"]["includeStacktrace"] is False
104 | 
```

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

```csharp
 1 | namespace MCPForUnity.Editor.Services
 2 | {
 3 |     /// <summary>
 4 |     /// Service for resolving paths to required tools and supporting user overrides
 5 |     /// </summary>
 6 |     public interface IPathResolverService
 7 |     {
 8 |         /// <summary>
 9 |         /// Gets the MCP server path (respects override if set)
10 |         /// </summary>
11 |         /// <returns>Path to the MCP server directory containing server.py, or null if not found</returns>
12 |         string GetMcpServerPath();
13 |         
14 |         /// <summary>
15 |         /// Gets the UV package manager path (respects override if set)
16 |         /// </summary>
17 |         /// <returns>Path to the uv executable, or null if not found</returns>
18 |         string GetUvPath();
19 |         
20 |         /// <summary>
21 |         /// Gets the Claude CLI path (respects override if set)
22 |         /// </summary>
23 |         /// <returns>Path to the claude executable, or null if not found</returns>
24 |         string GetClaudeCliPath();
25 |         
26 |         /// <summary>
27 |         /// Checks if Python is detected on the system
28 |         /// </summary>
29 |         /// <returns>True if Python is found</returns>
30 |         bool IsPythonDetected();
31 |         
32 |         /// <summary>
33 |         /// Checks if UV is detected on the system
34 |         /// </summary>
35 |         /// <returns>True if UV is found</returns>
36 |         bool IsUvDetected();
37 |         
38 |         /// <summary>
39 |         /// Checks if Claude CLI is detected on the system
40 |         /// </summary>
41 |         /// <returns>True if Claude CLI is found</returns>
42 |         bool IsClaudeCliDetected();
43 |         
44 |         /// <summary>
45 |         /// Sets an override for the MCP server path
46 |         /// </summary>
47 |         /// <param name="path">Path to override with</param>
48 |         void SetMcpServerOverride(string path);
49 |         
50 |         /// <summary>
51 |         /// Sets an override for the UV path
52 |         /// </summary>
53 |         /// <param name="path">Path to override with</param>
54 |         void SetUvPathOverride(string path);
55 |         
56 |         /// <summary>
57 |         /// Sets an override for the Claude CLI path
58 |         /// </summary>
59 |         /// <param name="path">Path to override with</param>
60 |         void SetClaudeCliPathOverride(string path);
61 |         
62 |         /// <summary>
63 |         /// Clears the MCP server path override
64 |         /// </summary>
65 |         void ClearMcpServerOverride();
66 |         
67 |         /// <summary>
68 |         /// Clears the UV path override
69 |         /// </summary>
70 |         void ClearUvPathOverride();
71 |         
72 |         /// <summary>
73 |         /// Clears the Claude CLI path override
74 |         /// </summary>
75 |         void ClearClaudeCliPathOverride();
76 |         
77 |         /// <summary>
78 |         /// Gets whether a MCP server path override is active
79 |         /// </summary>
80 |         bool HasMcpServerOverride { get; }
81 |         
82 |         /// <summary>
83 |         /// Gets whether a UV path override is active
84 |         /// </summary>
85 |         bool HasUvPathOverride { get; }
86 |         
87 |         /// <summary>
88 |         /// Gets whether a Claude CLI path override is active
89 |         /// </summary>
90 |         bool HasClaudeCliPathOverride { get; }
91 |     }
92 | }
93 | 
```

--------------------------------------------------------------------------------
/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Resources/GetMenuItemsTests.cs:
--------------------------------------------------------------------------------

```csharp
 1 | using NUnit.Framework;
 2 | using Newtonsoft.Json.Linq;
 3 | using MCPForUnity.Editor.Resources.MenuItems;
 4 | using System;
 5 | using System.Linq;
 6 | 
 7 | namespace MCPForUnityTests.Editor.Resources.MenuItems
 8 | {
 9 |     public class GetMenuItemsTests
10 |     {
11 |         private static JObject ToJO(object o) => JObject.FromObject(o);
12 | 
13 |         [Test]
14 |         public void NoSearch_ReturnsSuccessAndArray()
15 |         {
16 |             var res = GetMenuItems.HandleCommand(new JObject { ["search"] = "", ["refresh"] = false });
17 |             var jo = ToJO(res);
18 |             Assert.IsTrue((bool)jo["success"], "Expected success true");
19 |             Assert.IsNotNull(jo["data"], "Expected data field present");
20 |             Assert.AreEqual(JTokenType.Array, jo["data"].Type, "Expected data to be an array");
21 | 
22 |             // Validate list is sorted ascending when there are multiple items
23 |             var arr = (JArray)jo["data"];
24 |             if (arr.Count >= 2)
25 |             {
26 |                 var original = arr.Select(t => (string)t).ToList();
27 |                 var sorted = original.OrderBy(s => s, StringComparer.Ordinal).ToList();
28 |                 CollectionAssert.AreEqual(sorted, original, "Expected menu items to be sorted ascending");
29 |             }
30 |         }
31 | 
32 |         [Test]
33 |         public void SearchNoMatch_ReturnsEmpty()
34 |         {
35 |             var res = GetMenuItems.HandleCommand(new JObject { ["search"] = "___unlikely___term___" });
36 |             var jo = ToJO(res);
37 |             Assert.IsTrue((bool)jo["success"], "Expected success true");
38 |             Assert.AreEqual(JTokenType.Array, jo["data"].Type, "Expected data to be an array");
39 |             Assert.AreEqual(0, jo["data"].Count(), "Expected no results for unlikely search term");
40 |         }
41 | 
42 |         [Test]
43 |         public void SearchMatchesExistingItem_ReturnsContainingItem()
44 |         {
45 |             // Get the full list first
46 |             var listRes = GetMenuItems.HandleCommand(new JObject { ["search"] = "", ["refresh"] = false });
47 |             var listJo = ToJO(listRes);
48 |             if (listJo["data"] is JArray arr && arr.Count > 0)
49 |             {
50 |                 var first = (string)arr[0];
51 |                 // Use a mid-substring (case-insensitive) to avoid edge cases
52 |                 var term = first.Length > 4 ? first.Substring(1, Math.Min(3, first.Length - 2)) : first;
53 |                 term = term.ToLowerInvariant();
54 | 
55 |                 var res = GetMenuItems.HandleCommand(new JObject { ["search"] = term, ["refresh"] = false });
56 |                 var jo = ToJO(res);
57 |                 Assert.IsTrue((bool)jo["success"], "Expected success true");
58 |                 Assert.AreEqual(JTokenType.Array, jo["data"].Type, "Expected data to be an array");
59 |                 // Expect at least the original item to be present
60 |                 var names = ((JArray)jo["data"]).Select(t => (string)t).ToList();
61 |                 CollectionAssert.Contains(names, first, "Expected search results to include the sampled item");
62 |             }
63 |             else
64 |             {
65 |                 Assert.Pass("No menu items available to perform a content-based search assertion.");
66 |             }
67 |         }
68 |     }
69 | }
70 | 
```

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

```csharp
 1 | using System;
 2 | using System.Collections.Generic;
 3 | using System.Linq;
 4 | 
 5 | namespace MCPForUnity.Editor.Dependencies.Models
 6 | {
 7 |     /// <summary>
 8 |     /// Result of a comprehensive dependency check
 9 |     /// </summary>
10 |     [Serializable]
11 |     public class DependencyCheckResult
12 |     {
13 |         /// <summary>
14 |         /// List of all dependency statuses checked
15 |         /// </summary>
16 |         public List<DependencyStatus> Dependencies { get; set; }
17 | 
18 |         /// <summary>
19 |         /// Overall system readiness for MCP operations
20 |         /// </summary>
21 |         public bool IsSystemReady { get; set; }
22 | 
23 |         /// <summary>
24 |         /// Whether all required dependencies are available
25 |         /// </summary>
26 |         public bool AllRequiredAvailable => Dependencies?.Where(d => d.IsRequired).All(d => d.IsAvailable) ?? false;
27 | 
28 |         /// <summary>
29 |         /// Whether any optional dependencies are missing
30 |         /// </summary>
31 |         public bool HasMissingOptional => Dependencies?.Where(d => !d.IsRequired).Any(d => !d.IsAvailable) ?? false;
32 | 
33 |         /// <summary>
34 |         /// Summary message about the dependency state
35 |         /// </summary>
36 |         public string Summary { get; set; }
37 | 
38 |         /// <summary>
39 |         /// Recommended next steps for the user
40 |         /// </summary>
41 |         public List<string> RecommendedActions { get; set; }
42 | 
43 |         /// <summary>
44 |         /// Timestamp when this check was performed
45 |         /// </summary>
46 |         public DateTime CheckedAt { get; set; }
47 | 
48 |         public DependencyCheckResult()
49 |         {
50 |             Dependencies = new List<DependencyStatus>();
51 |             RecommendedActions = new List<string>();
52 |             CheckedAt = DateTime.UtcNow;
53 |         }
54 | 
55 |         /// <summary>
56 |         /// Get dependencies by availability status
57 |         /// </summary>
58 |         public List<DependencyStatus> GetMissingDependencies()
59 |         {
60 |             return Dependencies?.Where(d => !d.IsAvailable).ToList() ?? new List<DependencyStatus>();
61 |         }
62 | 
63 |         /// <summary>
64 |         /// Get missing required dependencies
65 |         /// </summary>
66 |         public List<DependencyStatus> GetMissingRequired()
67 |         {
68 |             return Dependencies?.Where(d => d.IsRequired && !d.IsAvailable).ToList() ?? new List<DependencyStatus>();
69 |         }
70 | 
71 |         /// <summary>
72 |         /// Generate a user-friendly summary of the dependency state
73 |         /// </summary>
74 |         public void GenerateSummary()
75 |         {
76 |             var missing = GetMissingDependencies();
77 |             var missingRequired = GetMissingRequired();
78 | 
79 |             if (missing.Count == 0)
80 |             {
81 |                 Summary = "All dependencies are available and ready.";
82 |                 IsSystemReady = true;
83 |             }
84 |             else if (missingRequired.Count == 0)
85 |             {
86 |                 Summary = $"System is ready. {missing.Count} optional dependencies are missing.";
87 |                 IsSystemReady = true;
88 |             }
89 |             else
90 |             {
91 |                 Summary = $"System is not ready. {missingRequired.Count} required dependencies are missing.";
92 |                 IsSystemReady = false;
93 |             }
94 |         }
95 |     }
96 | }
97 | 
```

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

```csharp
 1 | using System;
 2 | using System.Collections.Generic;
 3 | using System.Linq;
 4 | 
 5 | namespace MCPForUnity.Editor.Dependencies.Models
 6 | {
 7 |     /// <summary>
 8 |     /// Result of a comprehensive dependency check
 9 |     /// </summary>
10 |     [Serializable]
11 |     public class DependencyCheckResult
12 |     {
13 |         /// <summary>
14 |         /// List of all dependency statuses checked
15 |         /// </summary>
16 |         public List<DependencyStatus> Dependencies { get; set; }
17 | 
18 |         /// <summary>
19 |         /// Overall system readiness for MCP operations
20 |         /// </summary>
21 |         public bool IsSystemReady { get; set; }
22 | 
23 |         /// <summary>
24 |         /// Whether all required dependencies are available
25 |         /// </summary>
26 |         public bool AllRequiredAvailable => Dependencies?.Where(d => d.IsRequired).All(d => d.IsAvailable) ?? false;
27 | 
28 |         /// <summary>
29 |         /// Whether any optional dependencies are missing
30 |         /// </summary>
31 |         public bool HasMissingOptional => Dependencies?.Where(d => !d.IsRequired).Any(d => !d.IsAvailable) ?? false;
32 | 
33 |         /// <summary>
34 |         /// Summary message about the dependency state
35 |         /// </summary>
36 |         public string Summary { get; set; }
37 | 
38 |         /// <summary>
39 |         /// Recommended next steps for the user
40 |         /// </summary>
41 |         public List<string> RecommendedActions { get; set; }
42 | 
43 |         /// <summary>
44 |         /// Timestamp when this check was performed
45 |         /// </summary>
46 |         public DateTime CheckedAt { get; set; }
47 | 
48 |         public DependencyCheckResult()
49 |         {
50 |             Dependencies = new List<DependencyStatus>();
51 |             RecommendedActions = new List<string>();
52 |             CheckedAt = DateTime.UtcNow;
53 |         }
54 | 
55 |         /// <summary>
56 |         /// Get dependencies by availability status
57 |         /// </summary>
58 |         public List<DependencyStatus> GetMissingDependencies()
59 |         {
60 |             return Dependencies?.Where(d => !d.IsAvailable).ToList() ?? new List<DependencyStatus>();
61 |         }
62 | 
63 |         /// <summary>
64 |         /// Get missing required dependencies
65 |         /// </summary>
66 |         public List<DependencyStatus> GetMissingRequired()
67 |         {
68 |             return Dependencies?.Where(d => d.IsRequired && !d.IsAvailable).ToList() ?? new List<DependencyStatus>();
69 |         }
70 | 
71 |         /// <summary>
72 |         /// Generate a user-friendly summary of the dependency state
73 |         /// </summary>
74 |         public void GenerateSummary()
75 |         {
76 |             var missing = GetMissingDependencies();
77 |             var missingRequired = GetMissingRequired();
78 | 
79 |             if (missing.Count == 0)
80 |             {
81 |                 Summary = "All dependencies are available and ready.";
82 |                 IsSystemReady = true;
83 |             }
84 |             else if (missingRequired.Count == 0)
85 |             {
86 |                 Summary = $"System is ready. {missing.Count} optional dependencies are missing.";
87 |                 IsSystemReady = true;
88 |             }
89 |             else
90 |             {
91 |                 Summary = $"System is not ready. {missingRequired.Count} required dependencies are missing.";
92 |                 IsSystemReady = false;
93 |             }
94 |         }
95 |     }
96 | }
97 | 
```

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

```csharp
 1 | using MCPForUnity.Editor.Models;
 2 | 
 3 | namespace MCPForUnity.Editor.Services
 4 | {
 5 |     /// <summary>
 6 |     /// Service for configuring MCP clients
 7 |     /// </summary>
 8 |     public interface IClientConfigurationService
 9 |     {
10 |         /// <summary>
11 |         /// Configures a specific MCP client
12 |         /// </summary>
13 |         /// <param name="client">The client to configure</param>
14 |         void ConfigureClient(McpClient client);
15 |         
16 |         /// <summary>
17 |         /// Configures all detected/installed MCP clients (skips clients where CLI/tools not found)
18 |         /// </summary>
19 |         /// <returns>Summary of configuration results</returns>
20 |         ClientConfigurationSummary ConfigureAllDetectedClients();
21 |         
22 |         /// <summary>
23 |         /// Checks the configuration status of a client
24 |         /// </summary>
25 |         /// <param name="client">The client to check</param>
26 |         /// <param name="attemptAutoRewrite">If true, attempts to auto-fix mismatched paths</param>
27 |         /// <returns>True if status changed, false otherwise</returns>
28 |         bool CheckClientStatus(McpClient client, bool attemptAutoRewrite = true);
29 |         
30 |         /// <summary>
31 |         /// Registers MCP for Unity with Claude Code CLI
32 |         /// </summary>
33 |         void RegisterClaudeCode();
34 |         
35 |         /// <summary>
36 |         /// Unregisters MCP for Unity from Claude Code CLI
37 |         /// </summary>
38 |         void UnregisterClaudeCode();
39 |         
40 |         /// <summary>
41 |         /// Gets the configuration file path for a client
42 |         /// </summary>
43 |         /// <param name="client">The client</param>
44 |         /// <returns>Platform-specific config path</returns>
45 |         string GetConfigPath(McpClient client);
46 |         
47 |         /// <summary>
48 |         /// Generates the configuration JSON for a client
49 |         /// </summary>
50 |         /// <param name="client">The client</param>
51 |         /// <returns>JSON configuration string</returns>
52 |         string GenerateConfigJson(McpClient client);
53 |         
54 |         /// <summary>
55 |         /// Gets human-readable installation steps for a client
56 |         /// </summary>
57 |         /// <param name="client">The client</param>
58 |         /// <returns>Installation instructions</returns>
59 |         string GetInstallationSteps(McpClient client);
60 |     }
61 |     
62 |     /// <summary>
63 |     /// Summary of configuration results for multiple clients
64 |     /// </summary>
65 |     public class ClientConfigurationSummary
66 |     {
67 |         /// <summary>
68 |         /// Number of clients successfully configured
69 |         /// </summary>
70 |         public int SuccessCount { get; set; }
71 |         
72 |         /// <summary>
73 |         /// Number of clients that failed to configure
74 |         /// </summary>
75 |         public int FailureCount { get; set; }
76 |         
77 |         /// <summary>
78 |         /// Number of clients skipped (already configured or tool not found)
79 |         /// </summary>
80 |         public int SkippedCount { get; set; }
81 |         
82 |         /// <summary>
83 |         /// Detailed messages for each client
84 |         /// </summary>
85 |         public System.Collections.Generic.List<string> Messages { get; set; } = new();
86 |         
87 |         /// <summary>
88 |         /// Gets a human-readable summary message
89 |         /// </summary>
90 |         public string GetSummaryMessage()
91 |         {
92 |             return $"✓ {SuccessCount} configured, ⚠ {FailureCount} failed, ➜ {SkippedCount} skipped";
93 |         }
94 |     }
95 | }
96 | 
```

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

```csharp
 1 | using System;
 2 | 
 3 | namespace MCPForUnity.Editor.Services
 4 | {
 5 |     /// <summary>
 6 |     /// Service locator for accessing MCP services without dependency injection
 7 |     /// </summary>
 8 |     public static class MCPServiceLocator
 9 |     {
10 |         private static IBridgeControlService _bridgeService;
11 |         private static IClientConfigurationService _clientService;
12 |         private static IPathResolverService _pathService;
13 |         private static IPythonToolRegistryService _pythonToolRegistryService;
14 |         private static ITestRunnerService _testRunnerService;
15 |         private static IToolSyncService _toolSyncService;
16 |         private static IPackageUpdateService _packageUpdateService;
17 | 
18 |         public static IBridgeControlService Bridge => _bridgeService ??= new BridgeControlService();
19 |         public static IClientConfigurationService Client => _clientService ??= new ClientConfigurationService();
20 |         public static IPathResolverService Paths => _pathService ??= new PathResolverService();
21 |         public static IPythonToolRegistryService PythonToolRegistry => _pythonToolRegistryService ??= new PythonToolRegistryService();
22 |         public static ITestRunnerService Tests => _testRunnerService ??= new TestRunnerService();
23 |         public static IToolSyncService ToolSync => _toolSyncService ??= new ToolSyncService();
24 |         public static IPackageUpdateService Updates => _packageUpdateService ??= new PackageUpdateService();
25 | 
26 |         /// <summary>
27 |         /// Registers a custom implementation for a service (useful for testing)
28 |         /// </summary>
29 |         /// <typeparam name="T">The service interface type</typeparam>
30 |         /// <param name="implementation">The implementation to register</param>
31 |         public static void Register<T>(T implementation) where T : class
32 |         {
33 |             if (implementation is IBridgeControlService b)
34 |                 _bridgeService = b;
35 |             else if (implementation is IClientConfigurationService c)
36 |                 _clientService = c;
37 |             else if (implementation is IPathResolverService p)
38 |                 _pathService = p;
39 |             else if (implementation is IPythonToolRegistryService ptr)
40 |                 _pythonToolRegistryService = ptr;
41 |             else if (implementation is ITestRunnerService t)
42 |                 _testRunnerService = t;
43 |             else if (implementation is IToolSyncService ts)
44 |                 _toolSyncService = ts;
45 |             else if (implementation is IPackageUpdateService pu)
46 |                 _packageUpdateService = pu;
47 |         }
48 | 
49 |         /// <summary>
50 |         /// Resets all services to their default implementations (useful for testing)
51 |         /// </summary>
52 |         public static void Reset()
53 |         {
54 |             (_bridgeService as IDisposable)?.Dispose();
55 |             (_clientService as IDisposable)?.Dispose();
56 |             (_pathService as IDisposable)?.Dispose();
57 |             (_pythonToolRegistryService as IDisposable)?.Dispose();
58 |             (_testRunnerService as IDisposable)?.Dispose();
59 |             (_toolSyncService as IDisposable)?.Dispose();
60 |             (_packageUpdateService as IDisposable)?.Dispose();
61 | 
62 |             _bridgeService = null;
63 |             _clientService = null;
64 |             _pathService = null;
65 |             _pythonToolRegistryService = null;
66 |             _testRunnerService = null;
67 |             _toolSyncService = null;
68 |             _packageUpdateService = null;
69 |         }
70 |     }
71 | }
72 | 
```

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

```python
 1 | """
 2 | Defines the manage_asset tool for interacting with Unity assets.
 3 | """
 4 | import asyncio
 5 | from typing import Annotated, Any, Literal
 6 | 
 7 | from mcp.server.fastmcp import Context
 8 | from registry import mcp_for_unity_tool
 9 | from unity_connection import async_send_command_with_retry
10 | 
11 | 
12 | @mcp_for_unity_tool(
13 |     description="Performs asset operations (import, create, modify, delete, etc.) in Unity."
14 | )
15 | async def manage_asset(
16 |     ctx: Context,
17 |     action: Annotated[Literal["import", "create", "modify", "delete", "duplicate", "move", "rename", "search", "get_info", "create_folder", "get_components"], "Perform CRUD operations on assets."],
18 |     path: Annotated[str, "Asset path (e.g., 'Materials/MyMaterial.mat') or search scope."],
19 |     asset_type: Annotated[str,
20 |                           "Asset type (e.g., 'Material', 'Folder') - required for 'create'."] | None = None,
21 |     properties: Annotated[dict[str, Any],
22 |                           "Dictionary of properties for 'create'/'modify'."] | None = None,
23 |     destination: Annotated[str,
24 |                            "Target path for 'duplicate'/'move'."] | None = None,
25 |     generate_preview: Annotated[bool,
26 |                                 "Generate a preview/thumbnail for the asset when supported."] = False,
27 |     search_pattern: Annotated[str,
28 |                               "Search pattern (e.g., '*.prefab')."] | None = None,
29 |     filter_type: Annotated[str, "Filter type for search"] | None = None,
30 |     filter_date_after: Annotated[str,
31 |                                  "Date after which to filter"] | None = None,
32 |     page_size: Annotated[int, "Page size for pagination"] | None = None,
33 |     page_number: Annotated[int, "Page number for pagination"] | None = None
34 | ) -> dict[str, Any]:
35 |     ctx.info(f"Processing manage_asset: {action}")
36 |     # Ensure properties is a dict if None
37 |     if properties is None:
38 |         properties = {}
39 | 
40 |     # Coerce numeric inputs defensively
41 |     def _coerce_int(value, default=None):
42 |         if value is None:
43 |             return default
44 |         try:
45 |             if isinstance(value, bool):
46 |                 return default
47 |             if isinstance(value, int):
48 |                 return int(value)
49 |             s = str(value).strip()
50 |             if s.lower() in ("", "none", "null"):
51 |                 return default
52 |             return int(float(s))
53 |         except Exception:
54 |             return default
55 | 
56 |     page_size = _coerce_int(page_size)
57 |     page_number = _coerce_int(page_number)
58 | 
59 |     # Prepare parameters for the C# handler
60 |     params_dict = {
61 |         "action": action.lower(),
62 |         "path": path,
63 |         "assetType": asset_type,
64 |         "properties": properties,
65 |         "destination": destination,
66 |         "generatePreview": generate_preview,
67 |         "searchPattern": search_pattern,
68 |         "filterType": filter_type,
69 |         "filterDateAfter": filter_date_after,
70 |         "pageSize": page_size,
71 |         "pageNumber": page_number
72 |     }
73 | 
74 |     # Remove None values to avoid sending unnecessary nulls
75 |     params_dict = {k: v for k, v in params_dict.items() if v is not None}
76 | 
77 |     # Get the current asyncio event loop
78 |     loop = asyncio.get_running_loop()
79 | 
80 |     # Use centralized async retry helper to avoid blocking the event loop
81 |     result = await async_send_command_with_retry("manage_asset", params_dict, loop=loop)
82 |     # Return the result obtained from Unity
83 |     return result if isinstance(result, dict) else {"success": False, "message": str(result)}
84 | 
```

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

```python
 1 | """
 2 | Defines the manage_asset tool for interacting with Unity assets.
 3 | """
 4 | import asyncio
 5 | from typing import Annotated, Any, Literal
 6 | 
 7 | from mcp.server.fastmcp import Context
 8 | from registry import mcp_for_unity_tool
 9 | from unity_connection import async_send_command_with_retry
10 | 
11 | 
12 | @mcp_for_unity_tool(
13 |     description="Performs asset operations (import, create, modify, delete, etc.) in Unity."
14 | )
15 | async def manage_asset(
16 |     ctx: Context,
17 |     action: Annotated[Literal["import", "create", "modify", "delete", "duplicate", "move", "rename", "search", "get_info", "create_folder", "get_components"], "Perform CRUD operations on assets."],
18 |     path: Annotated[str, "Asset path (e.g., 'Materials/MyMaterial.mat') or search scope."],
19 |     asset_type: Annotated[str,
20 |                           "Asset type (e.g., 'Material', 'Folder') - required for 'create'."] | None = None,
21 |     properties: Annotated[dict[str, Any],
22 |                           "Dictionary of properties for 'create'/'modify'."] | None = None,
23 |     destination: Annotated[str,
24 |                            "Target path for 'duplicate'/'move'."] | None = None,
25 |     generate_preview: Annotated[bool,
26 |                                 "Generate a preview/thumbnail for the asset when supported."] = False,
27 |     search_pattern: Annotated[str,
28 |                               "Search pattern (e.g., '*.prefab')."] | None = None,
29 |     filter_type: Annotated[str, "Filter type for search"] | None = None,
30 |     filter_date_after: Annotated[str,
31 |                                  "Date after which to filter"] | None = None,
32 |     page_size: Annotated[int, "Page size for pagination"] | None = None,
33 |     page_number: Annotated[int, "Page number for pagination"] | None = None
34 | ) -> dict[str, Any]:
35 |     ctx.info(f"Processing manage_asset: {action}")
36 |     # Ensure properties is a dict if None
37 |     if properties is None:
38 |         properties = {}
39 | 
40 |     # Coerce numeric inputs defensively
41 |     def _coerce_int(value, default=None):
42 |         if value is None:
43 |             return default
44 |         try:
45 |             if isinstance(value, bool):
46 |                 return default
47 |             if isinstance(value, int):
48 |                 return int(value)
49 |             s = str(value).strip()
50 |             if s.lower() in ("", "none", "null"):
51 |                 return default
52 |             return int(float(s))
53 |         except Exception:
54 |             return default
55 | 
56 |     page_size = _coerce_int(page_size)
57 |     page_number = _coerce_int(page_number)
58 | 
59 |     # Prepare parameters for the C# handler
60 |     params_dict = {
61 |         "action": action.lower(),
62 |         "path": path,
63 |         "assetType": asset_type,
64 |         "properties": properties,
65 |         "destination": destination,
66 |         "generatePreview": generate_preview,
67 |         "searchPattern": search_pattern,
68 |         "filterType": filter_type,
69 |         "filterDateAfter": filter_date_after,
70 |         "pageSize": page_size,
71 |         "pageNumber": page_number
72 |     }
73 | 
74 |     # Remove None values to avoid sending unnecessary nulls
75 |     params_dict = {k: v for k, v in params_dict.items() if v is not None}
76 | 
77 |     # Get the current asyncio event loop
78 |     loop = asyncio.get_running_loop()
79 | 
80 |     # Use centralized async retry helper to avoid blocking the event loop
81 |     result = await async_send_command_with_retry("manage_asset", params_dict, loop=loop)
82 |     # Return the result obtained from Unity
83 |     return result if isinstance(result, dict) else {"success": False, "message": str(result)}
84 | 
```

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

```csharp
 1 | using System;
 2 | using System.Collections.Generic;
 3 | using System.Linq;
 4 | using Newtonsoft.Json.Linq;
 5 | using UnityEditor;
 6 | using MCPForUnity.Editor.Helpers;
 7 | 
 8 | namespace MCPForUnity.Editor.Tools.MenuItems
 9 | {
10 |     /// <summary>
11 |     /// Provides read/list/exists capabilities for Unity menu items with caching.
12 |     /// </summary>
13 |     public static class MenuItemsReader
14 |     {
15 |         private static List<string> _cached;
16 | 
17 |         [InitializeOnLoadMethod]
18 |         private static void Build() => Refresh();
19 | 
20 |         /// <summary>
21 |         /// Returns the cached list, refreshing if necessary.
22 |         /// </summary>
23 |         public static IReadOnlyList<string> AllMenuItems() => _cached ??= Refresh();
24 | 
25 |         /// <summary>
26 |         /// Rebuilds the cached list from reflection.
27 |         /// </summary>
28 |         private static List<string> Refresh()
29 |         {
30 |             try
31 |             {
32 |                 var methods = TypeCache.GetMethodsWithAttribute<MenuItem>();
33 |                 _cached = methods
34 |                     // Methods can have multiple [MenuItem] attributes; collect them all
35 |                     .SelectMany(m => m
36 |                         .GetCustomAttributes(typeof(MenuItem), false)
37 |                         .OfType<MenuItem>()
38 |                         .Select(attr => attr.menuItem))
39 |                     .Where(s => !string.IsNullOrEmpty(s))
40 |                     .Distinct(StringComparer.Ordinal) // Ensure no duplicates
41 |                     .OrderBy(s => s, StringComparer.Ordinal) // Ensure consistent ordering
42 |                     .ToList();
43 |                 return _cached;
44 |             }
45 |             catch (Exception e)
46 |             {
47 |                 McpLog.Error($"[MenuItemsReader] Failed to scan menu items: {e}");
48 |                 _cached = _cached ?? new List<string>();
49 |                 return _cached;
50 |             }
51 |         }
52 | 
53 |         /// <summary>
54 |         /// Returns a list of menu items. Optional 'search' param filters results.
55 |         /// </summary>
56 |         public static object List(JObject @params)
57 |         {
58 |             string search = @params["search"]?.ToString();
59 |             bool doRefresh = @params["refresh"]?.ToObject<bool>() ?? false;
60 |             if (doRefresh || _cached == null)
61 |             {
62 |                 Refresh();
63 |             }
64 | 
65 |             IEnumerable<string> result = _cached ?? Enumerable.Empty<string>();
66 |             if (!string.IsNullOrEmpty(search))
67 |             {
68 |                 result = result.Where(s => s.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0);
69 |             }
70 | 
71 |             return Response.Success("Menu items retrieved.", result.ToList());
72 |         }
73 | 
74 |         /// <summary>
75 |         /// Checks if a given menu path exists in the cache.
76 |         /// </summary>
77 |         public static object Exists(JObject @params)
78 |         {
79 |             string menuPath = @params["menu_path"]?.ToString() ?? @params["menuPath"]?.ToString();
80 |             if (string.IsNullOrWhiteSpace(menuPath))
81 |             {
82 |                 return Response.Error("Required parameter 'menu_path' or 'menuPath' is missing or empty.");
83 |             }
84 | 
85 |             bool doRefresh = @params["refresh"]?.ToObject<bool>() ?? false;
86 |             if (doRefresh || _cached == null)
87 |             {
88 |                 Refresh();
89 |             }
90 | 
91 |             bool exists = (_cached ?? new List<string>()).Contains(menuPath);
92 |             return Response.Success($"Exists check completed for '{menuPath}'.", new { exists });
93 |         }
94 |     }
95 | }
96 | 
```

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

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | Test script for Unity MCP Telemetry System
  4 | Run this to verify telemetry is working correctly
  5 | """
  6 | 
  7 | import os
  8 | from pathlib import Path
  9 | import sys
 10 | 
 11 | # Add src to Python path for imports
 12 | sys.path.insert(0, str(Path(__file__).parent))
 13 | 
 14 | 
 15 | def test_telemetry_basic():
 16 |     """Test basic telemetry functionality"""
 17 |     # Avoid stdout noise in tests
 18 | 
 19 |     try:
 20 |         from telemetry import (
 21 |             get_telemetry, record_telemetry, record_milestone,
 22 |             RecordType, MilestoneType, is_telemetry_enabled
 23 |         )
 24 |         pass
 25 |     except ImportError as e:
 26 |         # Silent failure path for tests
 27 |         return False
 28 | 
 29 |     # Test telemetry enabled status
 30 |     _ = is_telemetry_enabled()
 31 | 
 32 |     # Test basic record
 33 |     try:
 34 |         record_telemetry(RecordType.VERSION, {
 35 |             "version": "3.0.2",
 36 |             "test_run": True
 37 |         })
 38 |         pass
 39 |     except Exception as e:
 40 |         # Silent failure path for tests
 41 |         return False
 42 | 
 43 |     # Test milestone recording
 44 |     try:
 45 |         is_first = record_milestone(MilestoneType.FIRST_STARTUP, {
 46 |             "test_mode": True
 47 |         })
 48 |         _ = is_first
 49 |     except Exception as e:
 50 |         # Silent failure path for tests
 51 |         return False
 52 | 
 53 |     # Test telemetry collector
 54 |     try:
 55 |         collector = get_telemetry()
 56 |         _ = collector
 57 |     except Exception as e:
 58 |         # Silent failure path for tests
 59 |         return False
 60 | 
 61 |     return True
 62 | 
 63 | 
 64 | def test_telemetry_disabled():
 65 |     """Test telemetry with disabled state"""
 66 |     # Silent for tests
 67 | 
 68 |     # Set environment variable to disable telemetry
 69 |     os.environ["DISABLE_TELEMETRY"] = "true"
 70 | 
 71 |     # Re-import to get fresh config
 72 |     import importlib
 73 |     import telemetry
 74 |     importlib.reload(telemetry)
 75 | 
 76 |     from telemetry import is_telemetry_enabled, record_telemetry, RecordType
 77 | 
 78 |     _ = is_telemetry_enabled()
 79 | 
 80 |     if not is_telemetry_enabled():
 81 |         pass
 82 | 
 83 |         # Test that records are ignored when disabled
 84 |         record_telemetry(RecordType.USAGE, {"test": "should_be_ignored"})
 85 |         pass
 86 | 
 87 |         return True
 88 |     else:
 89 |         pass
 90 |         return False
 91 | 
 92 | 
 93 | def test_data_storage():
 94 |     """Test data storage functionality"""
 95 |     # Silent for tests
 96 | 
 97 |     try:
 98 |         from telemetry import get_telemetry
 99 | 
100 |         collector = get_telemetry()
101 |         data_dir = collector.config.data_dir
102 | 
103 |         _ = (data_dir, collector.config.uuid_file,
104 |              collector.config.milestones_file)
105 | 
106 |         # Check if files exist
107 |         if collector.config.uuid_file.exists():
108 |             pass
109 |         else:
110 |             pass
111 | 
112 |         if collector.config.milestones_file.exists():
113 |             pass
114 |         else:
115 |             pass
116 | 
117 |         return True
118 | 
119 |     except Exception as e:
120 |         # Silent failure path for tests
121 |         return False
122 | 
123 | 
124 | def main():
125 |     """Run all telemetry tests"""
126 |     # Silent runner for CI
127 | 
128 |     tests = [
129 |         test_telemetry_basic,
130 |         test_data_storage,
131 |         test_telemetry_disabled,
132 |     ]
133 | 
134 |     passed = 0
135 |     failed = 0
136 | 
137 |     for test in tests:
138 |         try:
139 |             if test():
140 |                 passed += 1
141 |                 pass
142 |             else:
143 |                 failed += 1
144 |                 pass
145 |         except Exception as e:
146 |             failed += 1
147 |             pass
148 | 
149 |     _ = (passed, failed)
150 | 
151 |     if failed == 0:
152 |         pass
153 |         return True
154 |     else:
155 |         pass
156 |         return False
157 | 
158 | 
159 | if __name__ == "__main__":
160 |     success = main()
161 |     sys.exit(0 if success else 1)
162 | 
```

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

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | Test script for MCP for Unity Telemetry System
  4 | Run this to verify telemetry is working correctly
  5 | """
  6 | 
  7 | import os
  8 | from pathlib import Path
  9 | import sys
 10 | 
 11 | # Add src to Python path for imports
 12 | sys.path.insert(0, str(Path(__file__).parent))
 13 | 
 14 | 
 15 | def test_telemetry_basic():
 16 |     """Test basic telemetry functionality"""
 17 |     # Avoid stdout noise in tests
 18 | 
 19 |     try:
 20 |         from telemetry import (
 21 |             get_telemetry, record_telemetry, record_milestone,
 22 |             RecordType, MilestoneType, is_telemetry_enabled
 23 |         )
 24 |         pass
 25 |     except ImportError as e:
 26 |         # Silent failure path for tests
 27 |         return False
 28 | 
 29 |     # Test telemetry enabled status
 30 |     _ = is_telemetry_enabled()
 31 | 
 32 |     # Test basic record
 33 |     try:
 34 |         record_telemetry(RecordType.VERSION, {
 35 |             "version": "3.0.2",
 36 |             "test_run": True
 37 |         })
 38 |         pass
 39 |     except Exception as e:
 40 |         # Silent failure path for tests
 41 |         return False
 42 | 
 43 |     # Test milestone recording
 44 |     try:
 45 |         is_first = record_milestone(MilestoneType.FIRST_STARTUP, {
 46 |             "test_mode": True
 47 |         })
 48 |         _ = is_first
 49 |     except Exception as e:
 50 |         # Silent failure path for tests
 51 |         return False
 52 | 
 53 |     # Test telemetry collector
 54 |     try:
 55 |         collector = get_telemetry()
 56 |         _ = collector
 57 |     except Exception as e:
 58 |         # Silent failure path for tests
 59 |         return False
 60 | 
 61 |     return True
 62 | 
 63 | 
 64 | def test_telemetry_disabled():
 65 |     """Test telemetry with disabled state"""
 66 |     # Silent for tests
 67 | 
 68 |     # Set environment variable to disable telemetry
 69 |     os.environ["DISABLE_TELEMETRY"] = "true"
 70 | 
 71 |     # Re-import to get fresh config
 72 |     import importlib
 73 |     import telemetry
 74 |     importlib.reload(telemetry)
 75 | 
 76 |     from telemetry import is_telemetry_enabled, record_telemetry, RecordType
 77 | 
 78 |     _ = is_telemetry_enabled()
 79 | 
 80 |     if not is_telemetry_enabled():
 81 |         pass
 82 | 
 83 |         # Test that records are ignored when disabled
 84 |         record_telemetry(RecordType.USAGE, {"test": "should_be_ignored"})
 85 |         pass
 86 | 
 87 |         return True
 88 |     else:
 89 |         pass
 90 |         return False
 91 | 
 92 | 
 93 | def test_data_storage():
 94 |     """Test data storage functionality"""
 95 |     # Silent for tests
 96 | 
 97 |     try:
 98 |         from telemetry import get_telemetry
 99 | 
100 |         collector = get_telemetry()
101 |         data_dir = collector.config.data_dir
102 | 
103 |         _ = (data_dir, collector.config.uuid_file,
104 |              collector.config.milestones_file)
105 | 
106 |         # Check if files exist
107 |         if collector.config.uuid_file.exists():
108 |             pass
109 |         else:
110 |             pass
111 | 
112 |         if collector.config.milestones_file.exists():
113 |             pass
114 |         else:
115 |             pass
116 | 
117 |         return True
118 | 
119 |     except Exception as e:
120 |         # Silent failure path for tests
121 |         return False
122 | 
123 | 
124 | def main():
125 |     """Run all telemetry tests"""
126 |     # Silent runner for CI
127 | 
128 |     tests = [
129 |         test_telemetry_basic,
130 |         test_data_storage,
131 |         test_telemetry_disabled,
132 |     ]
133 | 
134 |     passed = 0
135 |     failed = 0
136 | 
137 |     for test in tests:
138 |         try:
139 |             if test():
140 |                 passed += 1
141 |                 pass
142 |             else:
143 |                 failed += 1
144 |                 pass
145 |         except Exception as e:
146 |             failed += 1
147 |             pass
148 | 
149 |     _ = (passed, failed)
150 | 
151 |     if failed == 0:
152 |         pass
153 |         return True
154 |     else:
155 |         pass
156 |         return False
157 | 
158 | 
159 | if __name__ == "__main__":
160 |     success = main()
161 |     sys.exit(0 if success else 1)
162 | 
```

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

```python
 1 | """
 2 | Defines the read_console tool for accessing Unity Editor console messages.
 3 | """
 4 | from typing import Annotated, Any, Literal
 5 | 
 6 | from mcp.server.fastmcp import Context
 7 | from registry import mcp_for_unity_tool
 8 | from unity_connection import send_command_with_retry
 9 | 
10 | 
11 | @mcp_for_unity_tool(
12 |     description="Gets messages from or clears the Unity Editor console."
13 | )
14 | def read_console(
15 |     ctx: Context,
16 |     action: Annotated[Literal['get', 'clear'], "Get or clear the Unity Editor console."],
17 |     types: Annotated[list[Literal['error', 'warning',
18 |                                   'log', 'all']], "Message types to get"] | None = None,
19 |     count: Annotated[int, "Max messages to return"] | None = None,
20 |     filter_text: Annotated[str, "Text filter for messages"] | None = None,
21 |     since_timestamp: Annotated[str,
22 |                                "Get messages after this timestamp (ISO 8601)"] | None = None,
23 |     format: Annotated[Literal['plain', 'detailed',
24 |                               'json'], "Output format"] | None = None,
25 |     include_stacktrace: Annotated[bool,
26 |                                   "Include stack traces in output"] | None = None
27 | ) -> dict[str, Any]:
28 |     ctx.info(f"Processing read_console: {action}")
29 |     # Set defaults if values are None
30 |     action = action if action is not None else 'get'
31 |     types = types if types is not None else ['error', 'warning', 'log']
32 |     format = format if format is not None else 'detailed'
33 |     include_stacktrace = include_stacktrace if include_stacktrace is not None else True
34 | 
35 |     # Normalize action if it's a string
36 |     if isinstance(action, str):
37 |         action = action.lower()
38 | 
39 |     # Coerce count defensively (string/float -> int)
40 |     def _coerce_int(value, default=None):
41 |         if value is None:
42 |             return default
43 |         try:
44 |             if isinstance(value, bool):
45 |                 return default
46 |             if isinstance(value, int):
47 |                 return int(value)
48 |             s = str(value).strip()
49 |             if s.lower() in ("", "none", "null"):
50 |                 return default
51 |             return int(float(s))
52 |         except Exception:
53 |             return default
54 | 
55 |     count = _coerce_int(count)
56 | 
57 |     # Prepare parameters for the C# handler
58 |     params_dict = {
59 |         "action": action,
60 |         "types": types,
61 |         "count": count,
62 |         "filterText": filter_text,
63 |         "sinceTimestamp": since_timestamp,
64 |         "format": format.lower() if isinstance(format, str) else format,
65 |         "includeStacktrace": include_stacktrace
66 |     }
67 | 
68 |     # Remove None values unless it's 'count' (as None might mean 'all')
69 |     params_dict = {k: v for k, v in params_dict.items()
70 |                    if v is not None or k == 'count'}
71 | 
72 |     # Add count back if it was None, explicitly sending null might be important for C# logic
73 |     if 'count' not in params_dict:
74 |         params_dict['count'] = None
75 | 
76 |     # Use centralized retry helper
77 |     resp = send_command_with_retry("read_console", params_dict)
78 |     if isinstance(resp, dict) and resp.get("success") and not include_stacktrace:
79 |         # Strip stacktrace fields from returned lines if present
80 |         try:
81 |             lines = resp.get("data", {}).get("lines", [])
82 |             for line in lines:
83 |                 if isinstance(line, dict) and "stacktrace" in line:
84 |                     line.pop("stacktrace", None)
85 |         except Exception:
86 |             pass
87 |     return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
88 | 
```

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

```python
 1 | """
 2 | Defines the read_console tool for accessing Unity Editor console messages.
 3 | """
 4 | from typing import Annotated, Any, Literal
 5 | 
 6 | from mcp.server.fastmcp import Context
 7 | from registry import mcp_for_unity_tool
 8 | from unity_connection import send_command_with_retry
 9 | 
10 | 
11 | @mcp_for_unity_tool(
12 |     description="Gets messages from or clears the Unity Editor console."
13 | )
14 | def read_console(
15 |     ctx: Context,
16 |     action: Annotated[Literal['get', 'clear'], "Get or clear the Unity Editor console."],
17 |     types: Annotated[list[Literal['error', 'warning',
18 |                                   'log', 'all']], "Message types to get"] | None = None,
19 |     count: Annotated[int, "Max messages to return"] | None = None,
20 |     filter_text: Annotated[str, "Text filter for messages"] | None = None,
21 |     since_timestamp: Annotated[str,
22 |                                "Get messages after this timestamp (ISO 8601)"] | None = None,
23 |     format: Annotated[Literal['plain', 'detailed',
24 |                               'json'], "Output format"] | None = None,
25 |     include_stacktrace: Annotated[bool,
26 |                                   "Include stack traces in output"] | None = None
27 | ) -> dict[str, Any]:
28 |     ctx.info(f"Processing read_console: {action}")
29 |     # Set defaults if values are None
30 |     action = action if action is not None else 'get'
31 |     types = types if types is not None else ['error', 'warning', 'log']
32 |     format = format if format is not None else 'detailed'
33 |     include_stacktrace = include_stacktrace if include_stacktrace is not None else True
34 | 
35 |     # Normalize action if it's a string
36 |     if isinstance(action, str):
37 |         action = action.lower()
38 | 
39 |     # Coerce count defensively (string/float -> int)
40 |     def _coerce_int(value, default=None):
41 |         if value is None:
42 |             return default
43 |         try:
44 |             if isinstance(value, bool):
45 |                 return default
46 |             if isinstance(value, int):
47 |                 return int(value)
48 |             s = str(value).strip()
49 |             if s.lower() in ("", "none", "null"):
50 |                 return default
51 |             return int(float(s))
52 |         except Exception:
53 |             return default
54 | 
55 |     count = _coerce_int(count)
56 | 
57 |     # Prepare parameters for the C# handler
58 |     params_dict = {
59 |         "action": action,
60 |         "types": types,
61 |         "count": count,
62 |         "filterText": filter_text,
63 |         "sinceTimestamp": since_timestamp,
64 |         "format": format.lower() if isinstance(format, str) else format,
65 |         "includeStacktrace": include_stacktrace
66 |     }
67 | 
68 |     # Remove None values unless it's 'count' (as None might mean 'all')
69 |     params_dict = {k: v for k, v in params_dict.items()
70 |                    if v is not None or k == 'count'}
71 | 
72 |     # Add count back if it was None, explicitly sending null might be important for C# logic
73 |     if 'count' not in params_dict:
74 |         params_dict['count'] = None
75 | 
76 |     # Use centralized retry helper
77 |     resp = send_command_with_retry("read_console", params_dict)
78 |     if isinstance(resp, dict) and resp.get("success") and not include_stacktrace:
79 |         # Strip stacktrace fields from returned lines if present
80 |         try:
81 |             lines = resp.get("data", {}).get("lines", [])
82 |             for line in lines:
83 |                 if isinstance(line, dict) and "stacktrace" in line:
84 |                     line.pop("stacktrace", None)
85 |         except Exception:
86 |             pass
87 |     return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
88 | 
```

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

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.Threading.Tasks;
  4 | using Newtonsoft.Json.Linq;
  5 | using MCPForUnity.Editor.Helpers;
  6 | using MCPForUnity.Editor.Services;
  7 | using UnityEditor.TestTools.TestRunner.Api;
  8 | 
  9 | namespace MCPForUnity.Editor.Resources.Tests
 10 | {
 11 |     /// <summary>
 12 |     /// Provides access to Unity tests from the Test Framework.
 13 |     /// This is a read-only resource that can be queried by MCP clients.
 14 |     /// </summary>
 15 |     [McpForUnityResource("get_tests")]
 16 |     public static class GetTests
 17 |     {
 18 |         public static async Task<object> HandleCommand(JObject @params)
 19 |         {
 20 |             McpLog.Info("[GetTests] Retrieving tests for all modes");
 21 |             IReadOnlyList<Dictionary<string, string>> result;
 22 | 
 23 |             try
 24 |             {
 25 |                 result = await MCPServiceLocator.Tests.GetTestsAsync(mode: null).ConfigureAwait(true);
 26 |             }
 27 |             catch (Exception ex)
 28 |             {
 29 |                 McpLog.Error($"[GetTests] Error retrieving tests: {ex.Message}\n{ex.StackTrace}");
 30 |                 return Response.Error("Failed to retrieve tests");
 31 |             }
 32 | 
 33 |             string message = $"Retrieved {result.Count} tests";
 34 | 
 35 |             return Response.Success(message, result);
 36 |         }
 37 |     }
 38 | 
 39 |     /// <summary>
 40 |     /// Provides access to Unity tests for a specific mode (EditMode or PlayMode).
 41 |     /// This is a read-only resource that can be queried by MCP clients.
 42 |     /// </summary>
 43 |     [McpForUnityResource("get_tests_for_mode")]
 44 |     public static class GetTestsForMode
 45 |     {
 46 |         public static async Task<object> HandleCommand(JObject @params)
 47 |         {
 48 |             IReadOnlyList<Dictionary<string, string>> result;
 49 |             string modeStr = @params["mode"]?.ToString();
 50 |             if (string.IsNullOrEmpty(modeStr))
 51 |             {
 52 |                 return Response.Error("'mode' parameter is required");
 53 |             }
 54 | 
 55 |             if (!ModeParser.TryParse(modeStr, out var parsedMode, out var parseError))
 56 |             {
 57 |                 return Response.Error(parseError);
 58 |             }
 59 | 
 60 |             McpLog.Info($"[GetTestsForMode] Retrieving tests for mode: {parsedMode.Value}");
 61 | 
 62 |             try
 63 |             {
 64 |                 result = await MCPServiceLocator.Tests.GetTestsAsync(parsedMode).ConfigureAwait(true);
 65 |             }
 66 |             catch (Exception ex)
 67 |             {
 68 |                 McpLog.Error($"[GetTestsForMode] Error retrieving tests: {ex.Message}\n{ex.StackTrace}");
 69 |                 return Response.Error("Failed to retrieve tests");
 70 |             }
 71 | 
 72 |             string message = $"Retrieved {result.Count} {parsedMode.Value} tests";
 73 |             return Response.Success(message, result);
 74 |         }
 75 |     }
 76 | 
 77 |     internal static class ModeParser
 78 |     {
 79 |         internal static bool TryParse(string modeStr, out TestMode? mode, out string error)
 80 |         {
 81 |             error = null;
 82 |             mode = null;
 83 | 
 84 |             if (string.IsNullOrWhiteSpace(modeStr))
 85 |             {
 86 |                 error = "'mode' parameter cannot be empty";
 87 |                 return false;
 88 |             }
 89 | 
 90 |             if (modeStr.Equals("edit", StringComparison.OrdinalIgnoreCase))
 91 |             {
 92 |                 mode = TestMode.EditMode;
 93 |                 return true;
 94 |             }
 95 | 
 96 |             if (modeStr.Equals("play", StringComparison.OrdinalIgnoreCase))
 97 |             {
 98 |                 mode = TestMode.PlayMode;
 99 |                 return true;
100 |             }
101 | 
102 |             error = $"Unknown test mode: '{modeStr}'. Use 'edit' or 'play'";
103 |             return false;
104 |         }
105 |     }
106 | }
107 | 
```

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

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | Post-processes a JUnit XML so that "expected"/environmental failures
  4 | (e.g., permission prompts, empty MCP resources, or schema hiccups)
  5 | are converted to <skipped/>. Leaves real failures intact.
  6 | 
  7 | Usage:
  8 |   python .github/scripts/mark_skipped.py reports/claude-nl-tests.xml
  9 | """
 10 | 
 11 | from __future__ import annotations
 12 | import sys
 13 | import os
 14 | import re
 15 | import xml.etree.ElementTree as ET
 16 | 
 17 | PATTERNS = [
 18 |     r"\bpermission\b",
 19 |     r"\bpermissions\b",
 20 |     r"\bautoApprove\b",
 21 |     r"\bapproval\b",
 22 |     r"\bdenied\b",
 23 |     r"requested\s+permissions",
 24 |     r"^MCP resources list is empty$",
 25 |     r"No MCP resources detected",
 26 |     r"aggregator.*returned\s*\[\s*\]",
 27 |     r"Unknown resource:\s*unity://",
 28 |     r"Input should be a valid dictionary.*ctx",
 29 |     r"validation error .* ctx",
 30 | ]
 31 | 
 32 | 
 33 | def should_skip(msg: str) -> bool:
 34 |     if not msg:
 35 |         return False
 36 |     msg_l = msg.strip()
 37 |     for pat in PATTERNS:
 38 |         if re.search(pat, msg_l, flags=re.IGNORECASE | re.MULTILINE):
 39 |             return True
 40 |     return False
 41 | 
 42 | 
 43 | def summarize_counts(ts: ET.Element):
 44 |     tests = 0
 45 |     failures = 0
 46 |     errors = 0
 47 |     skipped = 0
 48 |     for case in ts.findall("testcase"):
 49 |         tests += 1
 50 |         if case.find("failure") is not None:
 51 |             failures += 1
 52 |         if case.find("error") is not None:
 53 |             errors += 1
 54 |         if case.find("skipped") is not None:
 55 |             skipped += 1
 56 |     return tests, failures, errors, skipped
 57 | 
 58 | 
 59 | def main(path: str) -> int:
 60 |     if not os.path.exists(path):
 61 |         print(f"[mark_skipped] No JUnit at {path}; nothing to do.")
 62 |         return 0
 63 | 
 64 |     try:
 65 |         tree = ET.parse(path)
 66 |     except ET.ParseError as e:
 67 |         print(f"[mark_skipped] Could not parse {path}: {e}")
 68 |         return 0
 69 | 
 70 |     root = tree.getroot()
 71 |     suites = root.findall("testsuite") if root.tag == "testsuites" else [root]
 72 | 
 73 |     changed = False
 74 |     for ts in suites:
 75 |         for case in list(ts.findall("testcase")):
 76 |             nodes = [n for n in list(case) if n.tag in ("failure", "error")]
 77 |             if not nodes:
 78 |                 continue
 79 |             # If any node matches skip patterns, convert the whole case to skipped.
 80 |             first_match_text = None
 81 |             to_skip = False
 82 |             for n in nodes:
 83 |                 msg = (n.get("message") or "") + "\n" + (n.text or "")
 84 |                 if should_skip(msg):
 85 |                     first_match_text = (
 86 |                         n.text or "").strip() or first_match_text
 87 |                     to_skip = True
 88 |             if to_skip:
 89 |                 for n in nodes:
 90 |                     case.remove(n)
 91 |                 reason = "Marked skipped: environment/permission precondition not met"
 92 |                 skip = ET.SubElement(case, "skipped")
 93 |                 skip.set("message", reason)
 94 |                 skip.text = first_match_text or reason
 95 |                 changed = True
 96 |         # Recompute tallies per testsuite
 97 |         tests, failures, errors, skipped = summarize_counts(ts)
 98 |         ts.set("tests", str(tests))
 99 |         ts.set("failures", str(failures))
100 |         ts.set("errors", str(errors))
101 |         ts.set("skipped", str(skipped))
102 | 
103 |     if changed:
104 |         tree.write(path, encoding="utf-8", xml_declaration=True)
105 |         print(
106 |             f"[mark_skipped] Updated {path}: converted environmental failures to skipped.")
107 |     else:
108 |         print(f"[mark_skipped] No environmental failures detected in {path}.")
109 | 
110 |     return 0
111 | 
112 | 
113 | if __name__ == "__main__":
114 |     target = (
115 |         sys.argv[1]
116 |         if len(sys.argv) > 1
117 |         else os.environ.get("JUNIT_OUT", "reports/junit-nl-suite.xml")
118 |     )
119 |     raise SystemExit(main(target))
120 | 
```

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

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.Linq;
  4 | using UnityEngine;
  5 | 
  6 | namespace MCPForUnity.Editor.Data
  7 | {
  8 |     /// <summary>
  9 |     /// Registry of Python tool files to sync to the MCP server.
 10 |     /// Add your Python files here - they can be stored anywhere in your project.
 11 |     /// </summary>
 12 |     [CreateAssetMenu(fileName = "PythonTools", menuName = "MCP For Unity/Python Tools")]
 13 |     public class PythonToolsAsset : ScriptableObject
 14 |     {
 15 |         [Tooltip("Add Python files (.py) to sync to the MCP server. Files can be located anywhere in your project.")]
 16 |         public List<TextAsset> pythonFiles = new List<TextAsset>();
 17 | 
 18 |         [Header("Sync Options")]
 19 |         [Tooltip("Use content hashing to detect changes (recommended). If false, always copies on startup.")]
 20 |         public bool useContentHashing = true;
 21 | 
 22 |         [Header("Sync State (Read-only)")]
 23 |         [Tooltip("Internal tracking - do not modify")]
 24 |         public List<PythonFileState> fileStates = new List<PythonFileState>();
 25 | 
 26 |         /// <summary>
 27 |         /// Gets all valid Python files (filters out null/missing references)
 28 |         /// </summary>
 29 |         public IEnumerable<TextAsset> GetValidFiles()
 30 |         {
 31 |             return pythonFiles.Where(f => f != null);
 32 |         }
 33 | 
 34 |         /// <summary>
 35 |         /// Checks if a file needs syncing
 36 |         /// </summary>
 37 |         public bool NeedsSync(TextAsset file, string currentHash)
 38 |         {
 39 |             if (!useContentHashing) return true; // Always sync if hashing disabled
 40 | 
 41 |             var state = fileStates.FirstOrDefault(s => s.assetGuid == GetAssetGuid(file));
 42 |             return state == null || state.contentHash != currentHash;
 43 |         }
 44 | 
 45 |         /// <summary>
 46 |         /// Records that a file was synced
 47 |         /// </summary>
 48 |         public void RecordSync(TextAsset file, string hash)
 49 |         {
 50 |             string guid = GetAssetGuid(file);
 51 |             var state = fileStates.FirstOrDefault(s => s.assetGuid == guid);
 52 | 
 53 |             if (state == null)
 54 |             {
 55 |                 state = new PythonFileState { assetGuid = guid };
 56 |                 fileStates.Add(state);
 57 |             }
 58 | 
 59 |             state.contentHash = hash;
 60 |             state.lastSyncTime = DateTime.UtcNow;
 61 |             state.fileName = file.name;
 62 |         }
 63 | 
 64 |         /// <summary>
 65 |         /// Removes state entries for files no longer in the list
 66 |         /// </summary>
 67 |         public void CleanupStaleStates()
 68 |         {
 69 |             var validGuids = new HashSet<string>(GetValidFiles().Select(GetAssetGuid));
 70 |             fileStates.RemoveAll(s => !validGuids.Contains(s.assetGuid));
 71 |         }
 72 | 
 73 |         private string GetAssetGuid(TextAsset asset)
 74 |         {
 75 |             return UnityEditor.AssetDatabase.AssetPathToGUID(UnityEditor.AssetDatabase.GetAssetPath(asset));
 76 |         }
 77 | 
 78 |         /// <summary>
 79 |         /// Called when the asset is modified in the Inspector
 80 |         /// Triggers sync to handle file additions/removals
 81 |         /// </summary>
 82 |         private void OnValidate()
 83 |         {
 84 |             // Cleanup stale states immediately
 85 |             CleanupStaleStates();
 86 |             
 87 |             // Trigger sync after a delay to handle file removals
 88 |             // Delay ensures the asset is saved before sync runs
 89 |             UnityEditor.EditorApplication.delayCall += () =>
 90 |             {
 91 |                 if (this != null) // Check if asset still exists
 92 |                 {
 93 |                     MCPForUnity.Editor.Helpers.PythonToolSyncProcessor.SyncAllTools();
 94 |                 }
 95 |             };
 96 |         }
 97 |     }
 98 | 
 99 |     [Serializable]
100 |     public class PythonFileState
101 |     {
102 |         public string assetGuid;
103 |         public string fileName;
104 |         public string contentHash;
105 |         public DateTime lastSyncTime;
106 |     }
107 | }
```

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

```python
  1 | import tools.manage_script as manage_script  # type: ignore
  2 | import sys
  3 | import types
  4 | from pathlib import Path
  5 | 
  6 | import pytest
  7 | 
  8 | 
  9 | # Locate server src dynamically to avoid hardcoded layout assumptions (same as other tests)
 10 | ROOT = Path(__file__).resolve().parents[1]
 11 | candidates = [
 12 |     ROOT / "MCPForUnity" / "UnityMcpServer~" / "src",
 13 |     ROOT / "UnityMcpServer~" / "src",
 14 | ]
 15 | SRC = next((p for p in candidates if p.exists()), None)
 16 | if SRC is None:
 17 |     searched = "\n".join(str(p) for p in candidates)
 18 |     pytest.skip(
 19 |         "MCP for Unity server source not found. Tried:\n" + searched,
 20 |         allow_module_level=True,
 21 |     )
 22 | sys.path.insert(0, str(SRC))
 23 | 
 24 | # Stub mcp.server.fastmcp to satisfy imports without full package
 25 | mcp_pkg = types.ModuleType("mcp")
 26 | server_pkg = types.ModuleType("mcp.server")
 27 | fastmcp_pkg = types.ModuleType("mcp.server.fastmcp")
 28 | 
 29 | 
 30 | class _Dummy:
 31 |     pass
 32 | 
 33 | 
 34 | fastmcp_pkg.FastMCP = _Dummy
 35 | fastmcp_pkg.Context = _Dummy
 36 | server_pkg.fastmcp = fastmcp_pkg
 37 | mcp_pkg.server = server_pkg
 38 | sys.modules.setdefault("mcp", mcp_pkg)
 39 | sys.modules.setdefault("mcp.server", server_pkg)
 40 | sys.modules.setdefault("mcp.server.fastmcp", fastmcp_pkg)
 41 | 
 42 | 
 43 | # Import target module after path injection
 44 | 
 45 | 
 46 | class DummyMCP:
 47 |     def __init__(self):
 48 |         self.tools = {}
 49 | 
 50 |     def tool(self, *args, **kwargs):  # ignore decorator kwargs like description
 51 |         def _decorator(fn):
 52 |             self.tools[fn.__name__] = fn
 53 |             return fn
 54 |         return _decorator
 55 | 
 56 | 
 57 | class DummyCtx:  # FastMCP Context placeholder
 58 |     pass
 59 | 
 60 | 
 61 | def _register_tools():
 62 |     mcp = DummyMCP()
 63 |     manage_script.register_manage_script_tools(mcp)  # populates mcp.tools
 64 |     return mcp.tools
 65 | 
 66 | 
 67 | def test_split_uri_unity_path(monkeypatch):
 68 |     tools = _register_tools()
 69 |     captured = {}
 70 | 
 71 |     def fake_send(cmd, params):  # capture params and return success
 72 |         captured['cmd'] = cmd
 73 |         captured['params'] = params
 74 |         return {"success": True, "message": "ok"}
 75 | 
 76 |     monkeypatch.setattr(manage_script, "send_command_with_retry", fake_send)
 77 | 
 78 |     fn = tools['apply_text_edits']
 79 |     uri = "unity://path/Assets/Scripts/MyScript.cs"
 80 |     fn(DummyCtx(), uri=uri, edits=[], precondition_sha256=None)
 81 | 
 82 |     assert captured['cmd'] == 'manage_script'
 83 |     assert captured['params']['name'] == 'MyScript'
 84 |     assert captured['params']['path'] == 'Assets/Scripts'
 85 | 
 86 | 
 87 | @pytest.mark.parametrize(
 88 |     "uri, expected_name, expected_path",
 89 |     [
 90 |         ("file:///Users/alex/Project/Assets/Scripts/Foo%20Bar.cs",
 91 |          "Foo Bar", "Assets/Scripts"),
 92 |         ("file://localhost/Users/alex/Project/Assets/Hello.cs", "Hello", "Assets"),
 93 |         ("file:///C:/Users/Alex/Proj/Assets/Scripts/Hello.cs",
 94 |          "Hello", "Assets/Scripts"),
 95 |         # outside Assets → fall back to normalized dir
 96 |         ("file:///tmp/Other.cs", "Other", "tmp"),
 97 |     ],
 98 | )
 99 | def test_split_uri_file_urls(monkeypatch, uri, expected_name, expected_path):
100 |     tools = _register_tools()
101 |     captured = {}
102 | 
103 |     def fake_send(cmd, params):
104 |         captured['cmd'] = cmd
105 |         captured['params'] = params
106 |         return {"success": True, "message": "ok"}
107 | 
108 |     monkeypatch.setattr(manage_script, "send_command_with_retry", fake_send)
109 | 
110 |     fn = tools['apply_text_edits']
111 |     fn(DummyCtx(), uri=uri, edits=[], precondition_sha256=None)
112 | 
113 |     assert captured['params']['name'] == expected_name
114 |     assert captured['params']['path'] == expected_path
115 | 
116 | 
117 | def test_split_uri_plain_path(monkeypatch):
118 |     tools = _register_tools()
119 |     captured = {}
120 | 
121 |     def fake_send(cmd, params):
122 |         captured['params'] = params
123 |         return {"success": True, "message": "ok"}
124 | 
125 |     monkeypatch.setattr(manage_script, "send_command_with_retry", fake_send)
126 | 
127 |     fn = tools['apply_text_edits']
128 |     fn(DummyCtx(), uri="Assets/Scripts/Thing.cs",
129 |        edits=[], precondition_sha256=None)
130 | 
131 |     assert captured['params']['name'] == 'Thing'
132 |     assert captured['params']['path'] == 'Assets/Scripts'
133 | 
```

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

```csharp
  1 | using NUnit.Framework;
  2 | using MCPForUnity.Editor.Helpers;
  3 | 
  4 | namespace MCPForUnityTests.Editor.Helpers
  5 | {
  6 |     public class CodexConfigHelperTests
  7 |     {
  8 |         [Test]
  9 |         public void TryParseCodexServer_SingleLineArgs_ParsesSuccessfully()
 10 |         {
 11 |             string toml = string.Join("\n", new[]
 12 |             {
 13 |                 "[mcp_servers.unityMCP]",
 14 |                 "command = \"uv\"",
 15 |                 "args = [\"run\", \"--directory\", \"/abs/path\", \"server.py\"]"
 16 |             });
 17 | 
 18 |             bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);
 19 | 
 20 |             Assert.IsTrue(result, "Parser should detect server definition");
 21 |             Assert.AreEqual("uv", command);
 22 |             CollectionAssert.AreEqual(new[] { "run", "--directory", "/abs/path", "server.py" }, args);
 23 |         }
 24 | 
 25 |         [Test]
 26 |         public void TryParseCodexServer_MultiLineArgsWithTrailingComma_ParsesSuccessfully()
 27 |         {
 28 |             string toml = string.Join("\n", new[]
 29 |             {
 30 |                 "[mcp_servers.unityMCP]",
 31 |                 "command = \"uv\"",
 32 |                 "args = [",
 33 |                 "  \"run\",",
 34 |                 "  \"--directory\",",
 35 |                 "  \"/abs/path\",",
 36 |                 "  \"server.py\",",
 37 |                 "]"
 38 |             });
 39 | 
 40 |             bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);
 41 | 
 42 |             Assert.IsTrue(result, "Parser should handle multi-line arrays with trailing comma");
 43 |             Assert.AreEqual("uv", command);
 44 |             CollectionAssert.AreEqual(new[] { "run", "--directory", "/abs/path", "server.py" }, args);
 45 |         }
 46 | 
 47 |         [Test]
 48 |         public void TryParseCodexServer_MultiLineArgsWithComments_IgnoresComments()
 49 |         {
 50 |             string toml = string.Join("\n", new[]
 51 |             {
 52 |                 "[mcp_servers.unityMCP]",
 53 |                 "command = \"uv\"",
 54 |                 "args = [",
 55 |                 "  \"run\", # launch command",
 56 |                 "  \"--directory\",",
 57 |                 "  \"/abs/path\",",
 58 |                 "  \"server.py\"",
 59 |                 "]"
 60 |             });
 61 | 
 62 |             bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);
 63 | 
 64 |             Assert.IsTrue(result, "Parser should tolerate comments within the array block");
 65 |             Assert.AreEqual("uv", command);
 66 |             CollectionAssert.AreEqual(new[] { "run", "--directory", "/abs/path", "server.py" }, args);
 67 |         }
 68 | 
 69 |         [Test]
 70 |         public void TryParseCodexServer_HeaderWithComment_StillDetected()
 71 |         {
 72 |             string toml = string.Join("\n", new[]
 73 |             {
 74 |                 "[mcp_servers.unityMCP] # annotated header",
 75 |                 "command = \"uv\"",
 76 |                 "args = [\"run\", \"--directory\", \"/abs/path\", \"server.py\"]"
 77 |             });
 78 | 
 79 |             bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);
 80 | 
 81 |             Assert.IsTrue(result, "Parser should recognize section headers even with inline comments");
 82 |             Assert.AreEqual("uv", command);
 83 |             CollectionAssert.AreEqual(new[] { "run", "--directory", "/abs/path", "server.py" }, args);
 84 |         }
 85 | 
 86 |         [Test]
 87 |         public void TryParseCodexServer_SingleQuotedArgsWithApostrophes_ParsesSuccessfully()
 88 |         {
 89 |             string toml = string.Join("\n", new[]
 90 |             {
 91 |                 "[mcp_servers.unityMCP]",
 92 |                 "command = 'uv'",
 93 |                 "args = ['run', '--directory', '/Users/O''Connor/codex', 'server.py']"
 94 |             });
 95 | 
 96 |             bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);
 97 | 
 98 |             Assert.IsTrue(result, "Parser should accept single-quoted arrays with escaped apostrophes");
 99 |             Assert.AreEqual("uv", command);
100 |             CollectionAssert.AreEqual(new[] { "run", "--directory", "/Users/O'Connor/codex", "server.py" }, args);
101 |         }
102 |     }
103 | }
104 | 
```

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

```
  1 | @echo off
  2 | setlocal enabledelayedexpansion
  3 | 
  4 | echo ===============================================
  5 | echo MCP for Unity Development Deployment Script
  6 | echo ===============================================
  7 | echo.
  8 | 
  9 | :: Configuration
 10 | set "SCRIPT_DIR=%~dp0"
 11 | set "BRIDGE_SOURCE=%SCRIPT_DIR%MCPForUnity"
 12 | set "SERVER_SOURCE=%SCRIPT_DIR%MCPForUnity\UnityMcpServer~\src"
 13 | set "DEFAULT_BACKUP_DIR=%USERPROFILE%\Desktop\unity-mcp-backup"
 14 | set "DEFAULT_SERVER_PATH=%LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer\src"
 15 | 
 16 | :: Get user inputs
 17 | echo Please provide the following paths:
 18 | echo.
 19 | 
 20 | :: Package cache location
 21 | echo Unity Package Cache Location:
 22 | echo Example: X:\UnityProject\Library\PackageCache\[email protected]
 23 | set /p "PACKAGE_CACHE_PATH=Enter Unity package cache path: "
 24 | 
 25 | if "%PACKAGE_CACHE_PATH%"=="" (
 26 |     echo Error: Package cache path cannot be empty!
 27 |     pause
 28 |     exit /b 1
 29 | )
 30 | 
 31 | :: Server installation path (with default)
 32 | echo.
 33 | echo Server Installation Path:
 34 | echo Default: %DEFAULT_SERVER_PATH%
 35 | set /p "SERVER_PATH=Enter server path (or press Enter for default): "
 36 | if "%SERVER_PATH%"=="" set "SERVER_PATH=%DEFAULT_SERVER_PATH%"
 37 | 
 38 | :: Backup location (with default)
 39 | echo.
 40 | echo Backup Location:
 41 | echo Default: %DEFAULT_BACKUP_DIR%
 42 | set /p "BACKUP_DIR=Enter backup directory (or press Enter for default): "
 43 | if "%BACKUP_DIR%"=="" set "BACKUP_DIR=%DEFAULT_BACKUP_DIR%"
 44 | 
 45 | :: Validation
 46 | echo.
 47 | echo ===============================================
 48 | echo Validating paths...
 49 | echo ===============================================
 50 | 
 51 | if not exist "%BRIDGE_SOURCE%" (
 52 |     echo Error: Bridge source not found: %BRIDGE_SOURCE%
 53 |     pause
 54 |     exit /b 1
 55 | )
 56 | 
 57 | if not exist "%SERVER_SOURCE%" (
 58 |     echo Error: Server source not found: %SERVER_SOURCE%
 59 |     pause
 60 |     exit /b 1
 61 | )
 62 | 
 63 | if not exist "%PACKAGE_CACHE_PATH%" (
 64 |     echo Error: Package cache path not found: %PACKAGE_CACHE_PATH%
 65 |     pause
 66 |     exit /b 1
 67 | )
 68 | 
 69 | if not exist "%SERVER_PATH%" (
 70 |     echo Error: Server installation path not found: %SERVER_PATH%
 71 |     pause
 72 |     exit /b 1
 73 | )
 74 | 
 75 | :: Create backup directory
 76 | if not exist "%BACKUP_DIR%" (
 77 |     echo Creating backup directory: %BACKUP_DIR%
 78 |     mkdir "%BACKUP_DIR%"
 79 | )
 80 | 
 81 | :: Create timestamped backup subdirectory
 82 | set "TIMESTAMP=%date:~-4,4%%date:~-10,2%%date:~-7,2%_%time:~0,2%%time:~3,2%%time:~6,2%"
 83 | set "TIMESTAMP=%TIMESTAMP: =0%"
 84 | set "TIMESTAMP=%TIMESTAMP::=-%"
 85 | set "TIMESTAMP=%TIMESTAMP:/=-%"
 86 | set "BACKUP_SUBDIR=%BACKUP_DIR%\backup_%TIMESTAMP%"
 87 | mkdir "%BACKUP_SUBDIR%"
 88 | 
 89 | echo.
 90 | echo ===============================================
 91 | echo Starting deployment...
 92 | echo ===============================================
 93 | 
 94 | :: Backup original files
 95 | echo Creating backup of original files...
 96 | if exist "%PACKAGE_CACHE_PATH%\Editor" (
 97 |     echo Backing up Unity Bridge files...
 98 |     xcopy "%PACKAGE_CACHE_PATH%\Editor" "%BACKUP_SUBDIR%\UnityBridge\Editor\" /E /I /Y > nul
 99 |     if !errorlevel! neq 0 (
100 |         echo Error: Failed to backup Unity Bridge files
101 |         pause
102 |         exit /b 1
103 |     )
104 | )
105 | 
106 | if exist "%SERVER_PATH%" (
107 |     echo Backing up Python Server files...
108 |     xcopy "%SERVER_PATH%\*" "%BACKUP_SUBDIR%\PythonServer\" /E /I /Y > nul
109 |     if !errorlevel! neq 0 (
110 |         echo Error: Failed to backup Python Server files
111 |         pause
112 |         exit /b 1
113 |     )
114 | )
115 | 
116 | :: Deploy Unity Bridge
117 | echo.
118 | echo Deploying Unity Bridge code...
119 | xcopy "%BRIDGE_SOURCE%\Editor\*" "%PACKAGE_CACHE_PATH%\Editor\" /E /Y > nul
120 | if !errorlevel! neq 0 (
121 |     echo Error: Failed to deploy Unity Bridge code
122 |     pause
123 |     exit /b 1
124 | )
125 | 
126 | :: Deploy Python Server
127 | echo Deploying Python Server code...
128 | xcopy "%SERVER_SOURCE%\*" "%SERVER_PATH%\" /E /Y > nul
129 | if !errorlevel! neq 0 (
130 |     echo Error: Failed to deploy Python Server code
131 |     pause
132 |     exit /b 1
133 | )
134 | 
135 | :: Success
136 | echo.
137 | echo ===============================================
138 | echo Deployment completed successfully!
139 | echo ===============================================
140 | echo.
141 | echo Backup created at: %BACKUP_SUBDIR%
142 | echo.
143 | echo Next steps:
144 | echo 1. Restart Unity Editor to load new Bridge code
145 | echo 2. Restart any MCP clients to use new Server code
146 | echo 3. Use restore-dev.bat to rollback if needed
147 | echo.
148 | pause
```

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

```csharp
  1 | using UnityEditor;
  2 | using UnityEngine;
  3 | 
  4 | namespace MCPForUnity.Editor.Helpers
  5 | {
  6 |     /// <summary>
  7 |     /// Auto-runs legacy/older install detection on package load/update (log-only).
  8 |     /// Runs once per embedded server version using an EditorPrefs version-scoped key.
  9 |     /// </summary>
 10 |     [InitializeOnLoad]
 11 |     public static class PackageDetector
 12 |     {
 13 |         private const string DetectOnceFlagKeyPrefix = "MCPForUnity.LegacyDetectLogged:";
 14 | 
 15 |         static PackageDetector()
 16 |         {
 17 |             try
 18 |             {
 19 |                 string pkgVer = ReadPackageVersionOrFallback();
 20 |                 string key = DetectOnceFlagKeyPrefix + pkgVer;
 21 | 
 22 |                 // Always force-run if legacy roots exist or canonical install is missing
 23 |                 bool legacyPresent = LegacyRootsExist();
 24 |                 bool canonicalMissing = !System.IO.File.Exists(System.IO.Path.Combine(ServerInstaller.GetServerPath(), "server.py"));
 25 | 
 26 |                 if (!EditorPrefs.GetBool(key, false) || legacyPresent || canonicalMissing)
 27 |                 {
 28 |                     // Marshal the entire flow to the main thread. EnsureServerInstalled may touch Unity APIs.
 29 |                     EditorApplication.delayCall += () =>
 30 |                     {
 31 |                         string error = null;
 32 |                         System.Exception capturedEx = null;
 33 |                         try
 34 |                         {
 35 |                             // Ensure any UnityEditor API usage inside runs on the main thread
 36 |                             ServerInstaller.EnsureServerInstalled();
 37 |                         }
 38 |                         catch (System.Exception ex)
 39 |                         {
 40 |                             error = ex.Message;
 41 |                             capturedEx = ex;
 42 |                         }
 43 | 
 44 |                         // Unity APIs must stay on main thread
 45 |                         try { EditorPrefs.SetBool(key, true); } catch { }
 46 |                         // Ensure prefs cleanup happens on main thread
 47 |                         try { EditorPrefs.DeleteKey("MCPForUnity.ServerSrc"); } catch { }
 48 |                         try { EditorPrefs.DeleteKey("MCPForUnity.PythonDirOverride"); } catch { }
 49 | 
 50 |                         if (!string.IsNullOrEmpty(error))
 51 |                         {
 52 |                             McpLog.Info($"Server check: {error}. Download via Window > MCP For Unity if needed.", always: false);
 53 |                         }
 54 |                     };
 55 |                 }
 56 |             }
 57 |             catch { /* ignore */ }
 58 |         }
 59 | 
 60 |         private static string ReadEmbeddedVersionOrFallback()
 61 |         {
 62 |             try
 63 |             {
 64 |                 if (ServerPathResolver.TryFindEmbeddedServerSource(out var embeddedSrc))
 65 |                 {
 66 |                     var p = System.IO.Path.Combine(embeddedSrc, "server_version.txt");
 67 |                     if (System.IO.File.Exists(p))
 68 |                         return (System.IO.File.ReadAllText(p)?.Trim() ?? "unknown");
 69 |                 }
 70 |             }
 71 |             catch { }
 72 |             return "unknown";
 73 |         }
 74 | 
 75 |         private static string ReadPackageVersionOrFallback()
 76 |         {
 77 |             try
 78 |             {
 79 |                 var info = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(PackageDetector).Assembly);
 80 |                 if (info != null && !string.IsNullOrEmpty(info.version)) return info.version;
 81 |             }
 82 |             catch { }
 83 |             // Fallback to embedded server version if package info unavailable
 84 |             return ReadEmbeddedVersionOrFallback();
 85 |         }
 86 | 
 87 |         private static bool LegacyRootsExist()
 88 |         {
 89 |             try
 90 |             {
 91 |                 string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile) ?? string.Empty;
 92 |                 string[] roots =
 93 |                 {
 94 |                     System.IO.Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer", "src"),
 95 |                     System.IO.Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer", "src")
 96 |                 };
 97 |                 foreach (var r in roots)
 98 |                 {
 99 |                     try { if (System.IO.File.Exists(System.IO.Path.Combine(r, "server.py"))) return true; } catch { }
100 |                 }
101 |             }
102 |             catch { }
103 |             return false;
104 |         }
105 |     }
106 | }
107 | 
```

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

```csharp
  1 | using UnityEditor;
  2 | using UnityEngine;
  3 | 
  4 | namespace MCPForUnity.Editor.Helpers
  5 | {
  6 |     /// <summary>
  7 |     /// Auto-runs legacy/older install detection on package load/update (log-only).
  8 |     /// Runs once per embedded server version using an EditorPrefs version-scoped key.
  9 |     /// </summary>
 10 |     [InitializeOnLoad]
 11 |     public static class PackageDetector
 12 |     {
 13 |         private const string DetectOnceFlagKeyPrefix = "MCPForUnity.LegacyDetectLogged:";
 14 | 
 15 |         static PackageDetector()
 16 |         {
 17 |             try
 18 |             {
 19 |                 string pkgVer = ReadPackageVersionOrFallback();
 20 |                 string key = DetectOnceFlagKeyPrefix + pkgVer;
 21 | 
 22 |                 // Always force-run if legacy roots exist or canonical install is missing
 23 |                 bool legacyPresent = LegacyRootsExist();
 24 |                 bool canonicalMissing = !System.IO.File.Exists(System.IO.Path.Combine(ServerInstaller.GetServerPath(), "server.py"));
 25 | 
 26 |                 if (!EditorPrefs.GetBool(key, false) || legacyPresent || canonicalMissing)
 27 |                 {
 28 |                     // Marshal the entire flow to the main thread. EnsureServerInstalled may touch Unity APIs.
 29 |                     EditorApplication.delayCall += () =>
 30 |                     {
 31 |                         string error = null;
 32 |                         System.Exception capturedEx = null;
 33 |                         try
 34 |                         {
 35 |                             // Ensure any UnityEditor API usage inside runs on the main thread
 36 |                             ServerInstaller.EnsureServerInstalled();
 37 |                         }
 38 |                         catch (System.Exception ex)
 39 |                         {
 40 |                             error = ex.Message;
 41 |                             capturedEx = ex;
 42 |                         }
 43 | 
 44 |                         // Unity APIs must stay on main thread
 45 |                         try { EditorPrefs.SetBool(key, true); } catch { }
 46 |                         // Ensure prefs cleanup happens on main thread
 47 |                         try { EditorPrefs.DeleteKey("MCPForUnity.ServerSrc"); } catch { }
 48 |                         try { EditorPrefs.DeleteKey("MCPForUnity.PythonDirOverride"); } catch { }
 49 | 
 50 |                         if (!string.IsNullOrEmpty(error))
 51 |                         {
 52 |                             Debug.LogWarning($"MCP for Unity: Auto-detect on load failed: {capturedEx}");
 53 |                             // Alternatively: Debug.LogException(capturedEx);
 54 |                         }
 55 |                     };
 56 |                 }
 57 |             }
 58 |             catch { /* ignore */ }
 59 |         }
 60 | 
 61 |         private static string ReadEmbeddedVersionOrFallback()
 62 |         {
 63 |             try
 64 |             {
 65 |                 if (ServerPathResolver.TryFindEmbeddedServerSource(out var embeddedSrc))
 66 |                 {
 67 |                     var p = System.IO.Path.Combine(embeddedSrc, "server_version.txt");
 68 |                     if (System.IO.File.Exists(p))
 69 |                         return (System.IO.File.ReadAllText(p)?.Trim() ?? "unknown");
 70 |                 }
 71 |             }
 72 |             catch { }
 73 |             return "unknown";
 74 |         }
 75 | 
 76 |         private static string ReadPackageVersionOrFallback()
 77 |         {
 78 |             try
 79 |             {
 80 |                 var info = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(PackageDetector).Assembly);
 81 |                 if (info != null && !string.IsNullOrEmpty(info.version)) return info.version;
 82 |             }
 83 |             catch { }
 84 |             // Fallback to embedded server version if package info unavailable
 85 |             return ReadEmbeddedVersionOrFallback();
 86 |         }
 87 | 
 88 |         private static bool LegacyRootsExist()
 89 |         {
 90 |             try
 91 |             {
 92 |                 string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile) ?? string.Empty;
 93 |                 string[] roots =
 94 |                 {
 95 |                     System.IO.Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer", "src"),
 96 |                     System.IO.Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer", "src")
 97 |                 };
 98 |                 foreach (var r in roots)
 99 |                 {
100 |                     try { if (System.IO.File.Exists(System.IO.Path.Combine(r, "server.py"))) return true; } catch { }
101 |                 }
102 |             }
103 |             catch { }
104 |             return false;
105 |         }
106 |     }
107 | }
108 | 
```

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

```yaml
  1 | name: Bump Version
  2 | 
  3 | on:
  4 |   workflow_dispatch:
  5 |     inputs:
  6 |       version_bump:
  7 |         description: "Version bump type"
  8 |         type: choice
  9 |         options:
 10 |           - patch
 11 |           - minor
 12 |           - major
 13 |         default: patch
 14 |         required: true
 15 | 
 16 | jobs:
 17 |   bump:
 18 |     name: "Bump version and tag"
 19 |     runs-on: ubuntu-latest
 20 |     permissions:
 21 |       contents: write
 22 |     steps:
 23 |       - name: Checkout repository
 24 |         uses: actions/checkout@v4
 25 |         with:
 26 |           fetch-depth: 0
 27 | 
 28 |       - name: Compute new version
 29 |         id: compute
 30 |         shell: bash
 31 |         run: |
 32 |           set -euo pipefail
 33 |           BUMP="${{ inputs.version_bump }}"
 34 |           CURRENT_VERSION=$(jq -r '.version' "MCPForUnity/package.json")
 35 |           echo "Current version: $CURRENT_VERSION"
 36 | 
 37 |           IFS='.' read -r MA MI PA <<< "$CURRENT_VERSION"
 38 |           case "$BUMP" in
 39 |             major)
 40 |               ((MA+=1)); MI=0; PA=0
 41 |               ;;
 42 |             minor)
 43 |               ((MI+=1)); PA=0
 44 |               ;;
 45 |             patch)
 46 |               ((PA+=1))
 47 |               ;;
 48 |             *)
 49 |               echo "Unknown version_bump: $BUMP" >&2
 50 |               exit 1
 51 |               ;;
 52 |           esac
 53 | 
 54 |           NEW_VERSION="$MA.$MI.$PA"
 55 |           echo "New version: $NEW_VERSION"
 56 |           echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
 57 |           echo "current_version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
 58 | 
 59 |       - name: Update files to new version
 60 |         env:
 61 |           NEW_VERSION: ${{ steps.compute.outputs.new_version }}
 62 |         shell: bash
 63 |         run: |
 64 |           set -euo pipefail
 65 | 
 66 |           echo "Updating MCPForUnity/package.json to $NEW_VERSION"
 67 |           jq ".version = \"${NEW_VERSION}\"" MCPForUnity/package.json > MCPForUnity/package.json.tmp
 68 |           mv MCPForUnity/package.json.tmp MCPForUnity/package.json
 69 | 
 70 |           echo "Updating MCPForUnity/UnityMcpServer~/src/pyproject.toml to $NEW_VERSION"
 71 |           sed -i '0,/^version = ".*"/s//version = "'"$NEW_VERSION"'"/' "MCPForUnity/UnityMcpServer~/src/pyproject.toml"
 72 | 
 73 |           echo "Updating MCPForUnity/UnityMcpServer~/src/server_version.txt to $NEW_VERSION"
 74 |           echo "$NEW_VERSION" > "MCPForUnity/UnityMcpServer~/src/server_version.txt"
 75 | 
 76 |       - name: Commit and push changes
 77 |         env:
 78 |           NEW_VERSION: ${{ steps.compute.outputs.new_version }}
 79 |         shell: bash
 80 |         run: |
 81 |           set -euo pipefail
 82 |           git config user.name "GitHub Actions"
 83 |           git config user.email "[email protected]"
 84 |           git add MCPForUnity/package.json "MCPForUnity/UnityMcpServer~/src/pyproject.toml" "MCPForUnity/UnityMcpServer~/src/server_version.txt"
 85 |           if git diff --cached --quiet; then
 86 |             echo "No version changes to commit."
 87 |           else
 88 |             git commit -m "chore: bump version to ${NEW_VERSION}"
 89 |           fi
 90 | 
 91 |           BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
 92 |           echo "Pushing to branch: $BRANCH"
 93 |           git push origin "$BRANCH"
 94 | 
 95 |       - name: Create and push tag
 96 |         env:
 97 |           NEW_VERSION: ${{ steps.compute.outputs.new_version }}
 98 |         shell: bash
 99 |         run: |
100 |           set -euo pipefail
101 |           TAG="v${NEW_VERSION}"
102 |           echo "Preparing to create tag $TAG"
103 | 
104 |           if git ls-remote --tags origin | grep -q "refs/tags/$TAG$"; then
105 |             echo "Tag $TAG already exists on remote. Skipping tag creation."
106 |             exit 0
107 |           fi
108 | 
109 |           git tag -a "$TAG" -m "Version ${NEW_VERSION}"
110 |           git push origin "$TAG"
111 | 
112 |       - name: Package server for release
113 |         env:
114 |           NEW_VERSION: ${{ steps.compute.outputs.new_version }}
115 |         shell: bash
116 |         run: |
117 |           set -euo pipefail
118 |           cd MCPForUnity
119 |           zip -r ../mcp-for-unity-server-v${NEW_VERSION}.zip UnityMcpServer~
120 |           cd ..
121 |           ls -lh mcp-for-unity-server-v${NEW_VERSION}.zip
122 |           echo "Server package created: mcp-for-unity-server-v${NEW_VERSION}.zip"
123 | 
124 |       - name: Create GitHub release with server artifact
125 |         env:
126 |           NEW_VERSION: ${{ steps.compute.outputs.new_version }}
127 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
128 |         shell: bash
129 |         run: |
130 |           set -euo pipefail
131 |           TAG="v${NEW_VERSION}"
132 | 
133 |           # Create release
134 |           gh release create "$TAG" \
135 |             --title "v${NEW_VERSION}" \
136 |             --notes "Release v${NEW_VERSION}" \
137 |             "mcp-for-unity-server-v${NEW_VERSION}.zip#MCP Server v${NEW_VERSION}"
138 | 
```

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

```python
  1 | """
  2 | Telemetry decorator for Unity MCP 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_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 | 
```

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

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.Linq;
  4 | using System.Reflection;
  5 | using System.Text.RegularExpressions;
  6 | using MCPForUnity.Editor.Helpers;
  7 | using Newtonsoft.Json.Linq;
  8 | 
  9 | namespace MCPForUnity.Editor.Tools
 10 | {
 11 |     /// <summary>
 12 |     /// Registry for all MCP command handlers via reflection.
 13 |     /// </summary>
 14 |     public static class CommandRegistry
 15 |     {
 16 |         private static readonly Dictionary<string, Func<JObject, object>> _handlers = new();
 17 |         private static bool _initialized = false;
 18 | 
 19 |         /// <summary>
 20 |         /// Initialize and auto-discover all tools marked with [McpForUnityTool]
 21 |         /// </summary>
 22 |         public static void Initialize()
 23 |         {
 24 |             if (_initialized) return;
 25 | 
 26 |             AutoDiscoverTools();
 27 |             _initialized = true;
 28 |         }
 29 | 
 30 |         /// <summary>
 31 |         /// Convert PascalCase or camelCase to snake_case
 32 |         /// </summary>
 33 |         private static string ToSnakeCase(string name)
 34 |         {
 35 |             if (string.IsNullOrEmpty(name)) return name;
 36 | 
 37 |             // Insert underscore before uppercase letters (except first)
 38 |             var s1 = Regex.Replace(name, "(.)([A-Z][a-z]+)", "$1_$2");
 39 |             var s2 = Regex.Replace(s1, "([a-z0-9])([A-Z])", "$1_$2");
 40 |             return s2.ToLower();
 41 |         }
 42 | 
 43 |         /// <summary>
 44 |         /// Auto-discover all types with [McpForUnityTool] attribute
 45 |         /// </summary>
 46 |         private static void AutoDiscoverTools()
 47 |         {
 48 |             try
 49 |             {
 50 |                 var toolTypes = AppDomain.CurrentDomain.GetAssemblies()
 51 |                     .Where(a => !a.IsDynamic)
 52 |                     .SelectMany(a =>
 53 |                     {
 54 |                         try { return a.GetTypes(); }
 55 |                         catch { return new Type[0]; }
 56 |                     })
 57 |                     .Where(t => t.GetCustomAttribute<McpForUnityToolAttribute>() != null);
 58 | 
 59 |                 foreach (var type in toolTypes)
 60 |                 {
 61 |                     RegisterToolType(type);
 62 |                 }
 63 | 
 64 |                 McpLog.Info($"Auto-discovered {_handlers.Count} tools");
 65 |             }
 66 |             catch (Exception ex)
 67 |             {
 68 |                 McpLog.Error($"Failed to auto-discover MCP tools: {ex.Message}");
 69 |             }
 70 |         }
 71 | 
 72 |         private static void RegisterToolType(Type type)
 73 |         {
 74 |             var attr = type.GetCustomAttribute<McpForUnityToolAttribute>();
 75 | 
 76 |             // Get command name (explicit or auto-generated)
 77 |             string commandName = attr.CommandName;
 78 |             if (string.IsNullOrEmpty(commandName))
 79 |             {
 80 |                 commandName = ToSnakeCase(type.Name);
 81 |             }
 82 | 
 83 |             // Check for duplicate command names
 84 |             if (_handlers.ContainsKey(commandName))
 85 |             {
 86 |                 McpLog.Warn(
 87 |                     $"Duplicate command name '{commandName}' detected. " +
 88 |                     $"Tool {type.Name} will override previously registered handler."
 89 |                 );
 90 |             }
 91 | 
 92 |             // Find HandleCommand method
 93 |             var method = type.GetMethod(
 94 |                 "HandleCommand",
 95 |                 BindingFlags.Public | BindingFlags.Static,
 96 |                 null,
 97 |                 new[] { typeof(JObject) },
 98 |                 null
 99 |             );
100 | 
101 |             if (method == null)
102 |             {
103 |                 McpLog.Warn(
104 |                     $"MCP tool {type.Name} is marked with [McpForUnityTool] " +
105 |                     $"but has no public static HandleCommand(JObject) method"
106 |                 );
107 |                 return;
108 |             }
109 | 
110 |             try
111 |             {
112 |                 var handler = (Func<JObject, object>)Delegate.CreateDelegate(
113 |                     typeof(Func<JObject, object>),
114 |                     method
115 |                 );
116 |                 _handlers[commandName] = handler;
117 |             }
118 |             catch (Exception ex)
119 |             {
120 |                 McpLog.Error($"Failed to register tool {type.Name}: {ex.Message}");
121 |             }
122 |         }
123 | 
124 |         /// <summary>
125 |         /// Get a command handler by name
126 |         /// </summary>
127 |         public static Func<JObject, object> GetHandler(string commandName)
128 |         {
129 |             if (!_handlers.TryGetValue(commandName, out var handler))
130 |             {
131 |                 throw new InvalidOperationException(
132 |                     $"Unknown or unsupported command type: {commandName}"
133 |                 );
134 |             }
135 |             return handler;
136 |         }
137 |     }
138 | }
139 | 
```

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

```csharp
  1 | using System.Collections.Generic;
  2 | using System.Linq;
  3 | using NUnit.Framework;
  4 | using UnityEngine;
  5 | using MCPForUnity.Editor.Data;
  6 | using MCPForUnity.Editor.Services;
  7 | 
  8 | namespace MCPForUnityTests.Editor.Services
  9 | {
 10 |     public class PythonToolRegistryServiceTests
 11 |     {
 12 |         private PythonToolRegistryService _service;
 13 | 
 14 |         [SetUp]
 15 |         public void SetUp()
 16 |         {
 17 |             _service = new PythonToolRegistryService();
 18 |         }
 19 | 
 20 |         [Test]
 21 |         public void GetAllRegistries_ReturnsEmptyList_WhenNoPythonToolsAssetsExist()
 22 |         {
 23 |             var registries = _service.GetAllRegistries().ToList();
 24 | 
 25 |             // Note: This might find assets in the test project, so we just verify it doesn't throw
 26 |             Assert.IsNotNull(registries, "Should return a non-null list");
 27 |         }
 28 | 
 29 |         [Test]
 30 |         public void NeedsSync_ReturnsTrue_WhenHashingDisabled()
 31 |         {
 32 |             var asset = ScriptableObject.CreateInstance<PythonToolsAsset>();
 33 |             asset.useContentHashing = false;
 34 | 
 35 |             var textAsset = new TextAsset("print('test')");
 36 | 
 37 |             bool needsSync = _service.NeedsSync(asset, textAsset);
 38 | 
 39 |             Assert.IsTrue(needsSync, "Should always need sync when hashing is disabled");
 40 | 
 41 |             Object.DestroyImmediate(asset);
 42 |         }
 43 | 
 44 |         [Test]
 45 |         public void NeedsSync_ReturnsTrue_WhenFileNotPreviouslySynced()
 46 |         {
 47 |             var asset = ScriptableObject.CreateInstance<PythonToolsAsset>();
 48 |             asset.useContentHashing = true;
 49 | 
 50 |             var textAsset = new TextAsset("print('test')");
 51 | 
 52 |             bool needsSync = _service.NeedsSync(asset, textAsset);
 53 | 
 54 |             Assert.IsTrue(needsSync, "Should need sync for new file");
 55 | 
 56 |             Object.DestroyImmediate(asset);
 57 |         }
 58 | 
 59 |         [Test]
 60 |         public void NeedsSync_ReturnsFalse_WhenHashMatches()
 61 |         {
 62 |             var asset = ScriptableObject.CreateInstance<PythonToolsAsset>();
 63 |             asset.useContentHashing = true;
 64 | 
 65 |             var textAsset = new TextAsset("print('test')");
 66 | 
 67 |             // First sync
 68 |             _service.RecordSync(asset, textAsset);
 69 | 
 70 |             // Check if needs sync again
 71 |             bool needsSync = _service.NeedsSync(asset, textAsset);
 72 | 
 73 |             Assert.IsFalse(needsSync, "Should not need sync when hash matches");
 74 | 
 75 |             Object.DestroyImmediate(asset);
 76 |         }
 77 | 
 78 |         [Test]
 79 |         public void RecordSync_StoresFileState()
 80 |         {
 81 |             var asset = ScriptableObject.CreateInstance<PythonToolsAsset>();
 82 |             var textAsset = new TextAsset("print('test')");
 83 | 
 84 |             _service.RecordSync(asset, textAsset);
 85 | 
 86 |             Assert.AreEqual(1, asset.fileStates.Count, "Should have one file state recorded");
 87 |             Assert.IsNotNull(asset.fileStates[0].contentHash, "Hash should be stored");
 88 |             Assert.IsNotNull(asset.fileStates[0].assetGuid, "GUID should be stored");
 89 | 
 90 |             Object.DestroyImmediate(asset);
 91 |         }
 92 | 
 93 |         [Test]
 94 |         public void RecordSync_UpdatesExistingState_WhenFileAlreadyRecorded()
 95 |         {
 96 |             var asset = ScriptableObject.CreateInstance<PythonToolsAsset>();
 97 |             var textAsset = new TextAsset("print('test')");
 98 | 
 99 |             // Record twice
100 |             _service.RecordSync(asset, textAsset);
101 |             var firstHash = asset.fileStates[0].contentHash;
102 | 
103 |             _service.RecordSync(asset, textAsset);
104 | 
105 |             Assert.AreEqual(1, asset.fileStates.Count, "Should still have only one state");
106 |             Assert.AreEqual(firstHash, asset.fileStates[0].contentHash, "Hash should remain the same");
107 | 
108 |             Object.DestroyImmediate(asset);
109 |         }
110 | 
111 |         [Test]
112 |         public void ComputeHash_ReturnsSameHash_ForSameContent()
113 |         {
114 |             var textAsset1 = new TextAsset("print('hello')");
115 |             var textAsset2 = new TextAsset("print('hello')");
116 | 
117 |             string hash1 = _service.ComputeHash(textAsset1);
118 |             string hash2 = _service.ComputeHash(textAsset2);
119 | 
120 |             Assert.AreEqual(hash1, hash2, "Same content should produce same hash");
121 |         }
122 | 
123 |         [Test]
124 |         public void ComputeHash_ReturnsDifferentHash_ForDifferentContent()
125 |         {
126 |             var textAsset1 = new TextAsset("print('hello')");
127 |             var textAsset2 = new TextAsset("print('world')");
128 | 
129 |             string hash1 = _service.ComputeHash(textAsset1);
130 |             string hash2 = _service.ComputeHash(textAsset2);
131 | 
132 |             Assert.AreNotEqual(hash1, hash2, "Different content should produce different hash");
133 |         }
134 |     }
135 | }
136 | 
```

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

```
  1 | @echo off
  2 | setlocal enabledelayedexpansion
  3 | 
  4 | echo ===============================================
  5 | echo MCP for Unity Development Restore Script
  6 | echo ===============================================
  7 | echo.
  8 | echo Note: The Python server is bundled under MCPForUnity\UnityMcpServer~ in the package.
  9 | echo       This script restores your installed server path from backups, not the repo copy.
 10 | echo.
 11 | 
 12 | :: Configuration
 13 | set "DEFAULT_BACKUP_DIR=%USERPROFILE%\Desktop\unity-mcp-backup"
 14 | set "DEFAULT_SERVER_PATH=%LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer\src"
 15 | 
 16 | :: Get user inputs
 17 | echo Please provide the following paths:
 18 | echo.
 19 | 
 20 | :: Package cache location
 21 | echo Unity Package Cache Location:
 22 | echo Example: X:\UnityProject\Library\PackageCache\[email protected]
 23 | set /p "PACKAGE_CACHE_PATH=Enter Unity package cache path: "
 24 | 
 25 | if "%PACKAGE_CACHE_PATH%"=="" (
 26 |     echo Error: Package cache path cannot be empty!
 27 |     pause
 28 |     exit /b 1
 29 | )
 30 | 
 31 | :: Server installation path (with default)
 32 | echo.
 33 | echo Server Installation Path:
 34 | echo Default: %DEFAULT_SERVER_PATH%
 35 | set /p "SERVER_PATH=Enter server path (or press Enter for default): "
 36 | if "%SERVER_PATH%"=="" set "SERVER_PATH=%DEFAULT_SERVER_PATH%"
 37 | 
 38 | :: Backup location (with default)
 39 | echo.
 40 | echo Backup Location:
 41 | echo Default: %DEFAULT_BACKUP_DIR%
 42 | set /p "BACKUP_DIR=Enter backup directory (or press Enter for default): "
 43 | if "%BACKUP_DIR%"=="" set "BACKUP_DIR=%DEFAULT_BACKUP_DIR%"
 44 | 
 45 | :: List available backups
 46 | echo.
 47 | echo ===============================================
 48 | echo Available backups:
 49 | echo ===============================================
 50 | set "counter=0"
 51 | for /d %%d in ("%BACKUP_DIR%\backup_*") do (
 52 |     set /a counter+=1
 53 |     set "backup!counter!=%%d"
 54 |     echo !counter!. %%~nxd
 55 | )
 56 | 
 57 | if %counter%==0 (
 58 |     echo No backups found in %BACKUP_DIR%
 59 |     pause
 60 |     exit /b 1
 61 | )
 62 | 
 63 | echo.
 64 | set /p "choice=Select backup to restore (1-%counter%): "
 65 | 
 66 | :: Validate choice
 67 | if "%choice%"=="" goto :invalid_choice
 68 | if %choice% lss 1 goto :invalid_choice
 69 | if %choice% gtr %counter% goto :invalid_choice
 70 | 
 71 | set "SELECTED_BACKUP=!backup%choice%!"
 72 | echo.
 73 | echo Selected backup: %SELECTED_BACKUP%
 74 | 
 75 | :: Validation
 76 | echo.
 77 | echo ===============================================
 78 | echo Validating paths...
 79 | echo ===============================================
 80 | 
 81 | if not exist "%SELECTED_BACKUP%" (
 82 |     echo Error: Selected backup not found: %SELECTED_BACKUP%
 83 |     pause
 84 |     exit /b 1
 85 | )
 86 | 
 87 | if not exist "%PACKAGE_CACHE_PATH%" (
 88 |     echo Error: Package cache path not found: %PACKAGE_CACHE_PATH%
 89 |     pause
 90 |     exit /b 1
 91 | )
 92 | 
 93 | if not exist "%SERVER_PATH%" (
 94 |     echo Error: Server installation path not found: %SERVER_PATH%
 95 |     pause
 96 |     exit /b 1
 97 | )
 98 | 
 99 | :: Confirm restore
100 | echo.
101 | echo ===============================================
102 | echo WARNING: This will overwrite current files!
103 | echo ===============================================
104 | echo Restoring from: %SELECTED_BACKUP%
105 | echo Unity Bridge target: %PACKAGE_CACHE_PATH%\Editor
106 | echo Python Server target: %SERVER_PATH%
107 | echo.
108 | set /p "confirm=Continue with restore? (y/N): "
109 | if /i not "%confirm%"=="y" (
110 |     echo Restore cancelled.
111 |     pause
112 |     exit /b 0
113 | )
114 | 
115 | echo.
116 | echo ===============================================
117 | echo Starting restore...
118 | echo ===============================================
119 | 
120 | :: Restore Unity Bridge
121 | if exist "%SELECTED_BACKUP%\UnityBridge\Editor" (
122 |     echo Restoring Unity Bridge files...
123 |     rd /s /q "%PACKAGE_CACHE_PATH%\Editor" 2>nul
124 |     xcopy "%SELECTED_BACKUP%\UnityBridge\Editor\*" "%PACKAGE_CACHE_PATH%\Editor\" /E /I /Y > nul
125 |     if !errorlevel! neq 0 (
126 |         echo Error: Failed to restore Unity Bridge files
127 |         pause
128 |         exit /b 1
129 |     )
130 | ) else (
131 |     echo Warning: No Unity Bridge backup found, skipping...
132 | )
133 | 
134 | :: Restore Python Server
135 | if exist "%SELECTED_BACKUP%\PythonServer" (
136 |     echo Restoring Python Server files...
137 |     rd /s /q "%SERVER_PATH%" 2>nul
138 |     mkdir "%SERVER_PATH%"
139 |     xcopy "%SELECTED_BACKUP%\PythonServer\*" "%SERVER_PATH%\" /E /I /Y > nul
140 |     if !errorlevel! neq 0 (
141 |         echo Error: Failed to restore Python Server files
142 |         pause
143 |         exit /b 1
144 |     )
145 | ) else (
146 |     echo Warning: No Python Server backup found, skipping...
147 | )
148 | 
149 | :: Success
150 | echo.
151 | echo ===============================================
152 | echo Restore completed successfully!
153 | echo ===============================================
154 | echo.
155 | echo Next steps:
156 | echo 1. Restart Unity Editor to load restored Bridge code
157 | echo 2. Restart any MCP clients to use restored Server code
158 | echo.
159 | pause
160 | exit /b 0
161 | 
162 | :invalid_choice
163 | echo Invalid choice. Please enter a number between 1 and %counter%.
164 | pause
165 | exit /b 1
```

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

```python
  1 | import sys
  2 | import pathlib
  3 | import importlib.util
  4 | import types
  5 | 
  6 | 
  7 | ROOT = pathlib.Path(__file__).resolve().parents[1]
  8 | SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src"
  9 | sys.path.insert(0, str(SRC))
 10 | 
 11 | # stub mcp.server.fastmcp
 12 | mcp_pkg = types.ModuleType("mcp")
 13 | server_pkg = types.ModuleType("mcp.server")
 14 | fastmcp_pkg = types.ModuleType("mcp.server.fastmcp")
 15 | 
 16 | 
 17 | class _Dummy:
 18 |     pass
 19 | 
 20 | 
 21 | fastmcp_pkg.FastMCP = _Dummy
 22 | fastmcp_pkg.Context = _Dummy
 23 | server_pkg.fastmcp = fastmcp_pkg
 24 | mcp_pkg.server = server_pkg
 25 | sys.modules.setdefault("mcp", mcp_pkg)
 26 | sys.modules.setdefault("mcp.server", server_pkg)
 27 | sys.modules.setdefault("mcp.server.fastmcp", fastmcp_pkg)
 28 | 
 29 | 
 30 | def _load(path: pathlib.Path, name: str):
 31 |     spec = importlib.util.spec_from_file_location(name, path)
 32 |     mod = importlib.util.module_from_spec(spec)
 33 |     spec.loader.exec_module(mod)
 34 |     return mod
 35 | 
 36 | 
 37 | manage_script = _load(SRC / "tools" / "manage_script.py", "manage_script_mod2")
 38 | manage_script_edits = _load(
 39 |     SRC / "tools" / "manage_script_edits.py", "manage_script_edits_mod2")
 40 | 
 41 | 
 42 | class DummyMCP:
 43 |     def __init__(self): self.tools = {}
 44 | 
 45 |     def tool(self, *args, **kwargs):
 46 |         def deco(fn): self.tools[fn.__name__] = fn; return fn
 47 |         return deco
 48 | 
 49 | 
 50 | def setup_tools():
 51 |     mcp = DummyMCP()
 52 |     manage_script.register_manage_script_tools(mcp)
 53 |     return mcp.tools
 54 | 
 55 | 
 56 | def test_normalizes_lsp_and_index_ranges(monkeypatch):
 57 |     tools = setup_tools()
 58 |     apply = tools["apply_text_edits"]
 59 |     calls = []
 60 | 
 61 |     def fake_send(cmd, params):
 62 |         calls.append(params)
 63 |         return {"success": True}
 64 | 
 65 |     monkeypatch.setattr(manage_script, "send_command_with_retry", fake_send)
 66 | 
 67 |     # LSP-style
 68 |     edits = [{
 69 |         "range": {"start": {"line": 10, "character": 2}, "end": {"line": 10, "character": 2}},
 70 |         "newText": "// lsp\n"
 71 |     }]
 72 |     apply(None, uri="unity://path/Assets/Scripts/F.cs",
 73 |           edits=edits, precondition_sha256="x")
 74 |     p = calls[-1]
 75 |     e = p["edits"][0]
 76 |     assert e["startLine"] == 11 and e["startCol"] == 3
 77 | 
 78 |     # Index pair
 79 |     calls.clear()
 80 |     edits = [{"range": [0, 0], "text": "// idx\n"}]
 81 |     # fake read to provide contents length
 82 | 
 83 |     def fake_read(cmd, params):
 84 |         if params.get("action") == "read":
 85 |             return {"success": True, "data": {"contents": "hello\n"}}
 86 |         return {"success": True}
 87 |     monkeypatch.setattr(manage_script, "send_command_with_retry", fake_read)
 88 |     apply(None, uri="unity://path/Assets/Scripts/F.cs",
 89 |           edits=edits, precondition_sha256="x")
 90 |     # last call is apply_text_edits
 91 | 
 92 | 
 93 | def test_noop_evidence_shape(monkeypatch):
 94 |     tools = setup_tools()
 95 |     apply = tools["apply_text_edits"]
 96 |     # Route response from Unity indicating no-op
 97 | 
 98 |     def fake_send(cmd, params):
 99 |         return {"success": True, "data": {"no_op": True, "evidence": {"reason": "identical_content"}}}
100 |     monkeypatch.setattr(manage_script, "send_command_with_retry", fake_send)
101 | 
102 |     resp = apply(None, uri="unity://path/Assets/Scripts/F.cs", edits=[
103 |                  {"startLine": 1, "startCol": 1, "endLine": 1, "endCol": 1, "newText": ""}], precondition_sha256="x")
104 |     assert resp["success"] is True
105 |     assert resp.get("data", {}).get("no_op") is True
106 | 
107 | 
108 | def test_atomic_multi_span_and_relaxed(monkeypatch):
109 |     tools_text = setup_tools()
110 |     apply_text = tools_text["apply_text_edits"]
111 |     tools_struct = DummyMCP()
112 |     manage_script_edits.register_manage_script_edits_tools(tools_struct)
113 |     # Fake send for read and write; verify atomic applyMode and validate=relaxed passes through
114 |     sent = {}
115 | 
116 |     def fake_send(cmd, params):
117 |         if params.get("action") == "read":
118 |             return {"success": True, "data": {"contents": "public class C{\nvoid M(){ int x=2; }\n}\n"}}
119 |         sent.setdefault("calls", []).append(params)
120 |         return {"success": True}
121 |     monkeypatch.setattr(manage_script, "send_command_with_retry", fake_send)
122 | 
123 |     edits = [
124 |         {"startLine": 2, "startCol": 14, "endLine": 2, "endCol": 15, "newText": "3"},
125 |         {"startLine": 3, "startCol": 2, "endLine": 3,
126 |             "endCol": 2, "newText": "// tail\n"}
127 |     ]
128 |     resp = apply_text(None, uri="unity://path/Assets/Scripts/C.cs", edits=edits,
129 |                       precondition_sha256="sha", options={"validate": "relaxed", "applyMode": "atomic"})
130 |     assert resp["success"] is True
131 |     # Last manage_script call should include options with applyMode atomic and validate relaxed
132 |     last = sent["calls"][-1]
133 |     assert last.get("options", {}).get("applyMode") == "atomic"
134 |     assert last.get("options", {}).get("validate") == "relaxed"
135 | 
```
Page 2/18FirstPrevNextLast