#
tokens: 48715/50000 21/263 files (page 4/18)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 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

--------------------------------------------------------------------------------
/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ComponentResolverTests.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using NUnit.Framework;
  3 | using UnityEngine;
  4 | using MCPForUnity.Editor.Tools;
  5 | using static MCPForUnity.Editor.Tools.ManageGameObject;
  6 | 
  7 | namespace MCPForUnityTests.Editor.Tools
  8 | {
  9 |     public class ComponentResolverTests
 10 |     {
 11 |         [Test]
 12 |         public void TryResolve_ReturnsTrue_ForBuiltInComponentShortName()
 13 |         {
 14 |             bool result = ComponentResolver.TryResolve("Transform", out Type type, out string error);
 15 | 
 16 |             Assert.IsTrue(result, "Should resolve Transform component");
 17 |             Assert.AreEqual(typeof(Transform), type, "Should return correct Transform type");
 18 |             Assert.IsEmpty(error, "Should have no error message");
 19 |         }
 20 | 
 21 |         [Test]
 22 |         public void TryResolve_ReturnsTrue_ForBuiltInComponentFullyQualifiedName()
 23 |         {
 24 |             bool result = ComponentResolver.TryResolve("UnityEngine.Rigidbody", out Type type, out string error);
 25 | 
 26 |             Assert.IsTrue(result, "Should resolve UnityEngine.Rigidbody component");
 27 |             Assert.AreEqual(typeof(Rigidbody), type, "Should return correct Rigidbody type");
 28 |             Assert.IsEmpty(error, "Should have no error message");
 29 |         }
 30 | 
 31 |         [Test]
 32 |         public void TryResolve_ReturnsTrue_ForCustomComponentShortName()
 33 |         {
 34 |             bool result = ComponentResolver.TryResolve("CustomComponent", out Type type, out string error);
 35 | 
 36 |             Assert.IsTrue(result, "Should resolve CustomComponent");
 37 |             Assert.IsNotNull(type, "Should return valid type");
 38 |             Assert.AreEqual("CustomComponent", type.Name, "Should have correct type name");
 39 |             Assert.IsTrue(typeof(Component).IsAssignableFrom(type), "Should be a Component type");
 40 |             Assert.IsEmpty(error, "Should have no error message");
 41 |         }
 42 | 
 43 |         [Test]
 44 |         public void TryResolve_ReturnsTrue_ForCustomComponentFullyQualifiedName()
 45 |         {
 46 |             bool result = ComponentResolver.TryResolve("TestNamespace.CustomComponent", out Type type, out string error);
 47 | 
 48 |             Assert.IsTrue(result, "Should resolve TestNamespace.CustomComponent");
 49 |             Assert.IsNotNull(type, "Should return valid type");
 50 |             Assert.AreEqual("CustomComponent", type.Name, "Should have correct type name");
 51 |             Assert.AreEqual("TestNamespace.CustomComponent", type.FullName, "Should have correct full name");
 52 |             Assert.IsTrue(typeof(Component).IsAssignableFrom(type), "Should be a Component type");
 53 |             Assert.IsEmpty(error, "Should have no error message");
 54 |         }
 55 | 
 56 |         [Test]
 57 |         public void TryResolve_ReturnsFalse_ForNonExistentComponent()
 58 |         {
 59 |             bool result = ComponentResolver.TryResolve("NonExistentComponent", out Type type, out string error);
 60 | 
 61 |             Assert.IsFalse(result, "Should not resolve non-existent component");
 62 |             Assert.IsNull(type, "Should return null type");
 63 |             Assert.IsNotEmpty(error, "Should have error message");
 64 |             Assert.That(error, Does.Contain("not found"), "Error should mention component not found");
 65 |         }
 66 | 
 67 |         [Test]
 68 |         public void TryResolve_ReturnsFalse_ForEmptyString()
 69 |         {
 70 |             bool result = ComponentResolver.TryResolve("", out Type type, out string error);
 71 | 
 72 |             Assert.IsFalse(result, "Should not resolve empty string");
 73 |             Assert.IsNull(type, "Should return null type");
 74 |             Assert.IsNotEmpty(error, "Should have error message");
 75 |         }
 76 | 
 77 |         [Test]
 78 |         public void TryResolve_ReturnsFalse_ForNullString()
 79 |         {
 80 |             bool result = ComponentResolver.TryResolve(null, out Type type, out string error);
 81 | 
 82 |             Assert.IsFalse(result, "Should not resolve null string");
 83 |             Assert.IsNull(type, "Should return null type");
 84 |             Assert.IsNotEmpty(error, "Should have error message");
 85 |             Assert.That(error, Does.Contain("null or empty"), "Error should mention null or empty");
 86 |         }
 87 | 
 88 |         [Test]
 89 |         public void TryResolve_CachesResolvedTypes()
 90 |         {
 91 |             // First call
 92 |             bool result1 = ComponentResolver.TryResolve("Transform", out Type type1, out string error1);
 93 | 
 94 |             // Second call should use cache
 95 |             bool result2 = ComponentResolver.TryResolve("Transform", out Type type2, out string error2);
 96 | 
 97 |             Assert.IsTrue(result1, "First call should succeed");
 98 |             Assert.IsTrue(result2, "Second call should succeed");
 99 |             Assert.AreSame(type1, type2, "Should return same type instance (cached)");
100 |             Assert.IsEmpty(error1, "First call should have no error");
101 |             Assert.IsEmpty(error2, "Second call should have no error");
102 |         }
103 | 
104 |         [Test]
105 |         public void TryResolve_PrefersPlayerAssemblies()
106 |         {
107 |             // Test that custom user scripts (in Player assemblies) are found
108 |             bool result = ComponentResolver.TryResolve("CustomComponent", out Type type, out string error);
109 | 
110 |             Assert.IsTrue(result, "Should resolve user script from Player assembly");
111 |             Assert.IsNotNull(type, "Should return valid type");
112 | 
113 |             // Verify it's not from an Editor assembly by checking the assembly name
114 |             string assemblyName = type.Assembly.GetName().Name;
115 |             Assert.That(assemblyName, Does.Not.Contain("Editor"),
116 |                 "User script should come from Player assembly, not Editor assembly");
117 | 
118 |             // Verify it's from the TestAsmdef assembly (which is a Player assembly)
119 |             Assert.AreEqual("TestAsmdef", assemblyName,
120 |                 "CustomComponent should be resolved from TestAsmdef assembly");
121 |         }
122 | 
123 |         [Test]
124 |         public void TryResolve_HandlesDuplicateNames_WithAmbiguityError()
125 |         {
126 |             // This test would need duplicate component names to be meaningful
127 |             // For now, test with a built-in component that should not have duplicates
128 |             bool result = ComponentResolver.TryResolve("Transform", out Type type, out string error);
129 | 
130 |             Assert.IsTrue(result, "Transform should resolve uniquely");
131 |             Assert.AreEqual(typeof(Transform), type, "Should return correct type");
132 |             Assert.IsEmpty(error, "Should have no ambiguity error");
133 |         }
134 | 
135 |         [Test]
136 |         public void ResolvedType_IsValidComponent()
137 |         {
138 |             bool result = ComponentResolver.TryResolve("Rigidbody", out Type type, out string error);
139 | 
140 |             Assert.IsTrue(result, "Should resolve Rigidbody");
141 |             Assert.IsTrue(typeof(Component).IsAssignableFrom(type), "Resolved type should be assignable from Component");
142 |             Assert.IsTrue(typeof(MonoBehaviour).IsAssignableFrom(type) ||
143 |                          typeof(Component).IsAssignableFrom(type), "Should be a valid Unity component");
144 |         }
145 |     }
146 | }
147 | 
```

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

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.IO;
  4 | using System.Linq;
  5 | using MCPForUnity.External.Tommy;
  6 | 
  7 | namespace MCPForUnity.Editor.Helpers
  8 | {
  9 |     /// <summary>
 10 |     /// Codex CLI specific configuration helpers. Handles TOML snippet
 11 |     /// generation and lightweight parsing so Codex can join the auto-setup
 12 |     /// flow alongside JSON-based clients.
 13 |     /// </summary>
 14 |     public static class CodexConfigHelper
 15 |     {
 16 |         public static bool IsCodexConfigured(string pythonDir)
 17 |         {
 18 |             try
 19 |             {
 20 |                 string basePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
 21 |                 if (string.IsNullOrEmpty(basePath)) return false;
 22 | 
 23 |                 string configPath = Path.Combine(basePath, ".codex", "config.toml");
 24 |                 if (!File.Exists(configPath)) return false;
 25 | 
 26 |                 string toml = File.ReadAllText(configPath);
 27 |                 if (!TryParseCodexServer(toml, out _, out var args)) return false;
 28 | 
 29 |                 string dir = McpConfigFileHelper.ExtractDirectoryArg(args);
 30 |                 if (string.IsNullOrEmpty(dir)) return false;
 31 | 
 32 |                 return McpConfigFileHelper.PathsEqual(dir, pythonDir);
 33 |             }
 34 |             catch
 35 |             {
 36 |                 return false;
 37 |             }
 38 |         }
 39 | 
 40 |         public static string BuildCodexServerBlock(string uvPath, string serverSrc)
 41 |         {
 42 |             var table = new TomlTable();
 43 |             var mcpServers = new TomlTable();
 44 | 
 45 |             mcpServers["unityMCP"] = CreateUnityMcpTable(uvPath, serverSrc);
 46 |             table["mcp_servers"] = mcpServers;
 47 | 
 48 |             using var writer = new StringWriter();
 49 |             table.WriteTo(writer);
 50 |             return writer.ToString();
 51 |         }
 52 | 
 53 |         public static string UpsertCodexServerBlock(string existingToml, string uvPath, string serverSrc)
 54 |         {
 55 |             // Parse existing TOML or create new root table
 56 |             var root = TryParseToml(existingToml) ?? new TomlTable();
 57 | 
 58 |             // Ensure mcp_servers table exists
 59 |             if (!root.TryGetNode("mcp_servers", out var mcpServersNode) || !(mcpServersNode is TomlTable))
 60 |             {
 61 |                 root["mcp_servers"] = new TomlTable();
 62 |             }
 63 |             var mcpServers = root["mcp_servers"] as TomlTable;
 64 | 
 65 |             // Create or update unityMCP table
 66 |             mcpServers["unityMCP"] = CreateUnityMcpTable(uvPath, serverSrc);
 67 | 
 68 |             // Serialize back to TOML
 69 |             using var writer = new StringWriter();
 70 |             root.WriteTo(writer);
 71 |             return writer.ToString();
 72 |         }
 73 | 
 74 |         public static bool TryParseCodexServer(string toml, out string command, out string[] args)
 75 |         {
 76 |             command = null;
 77 |             args = null;
 78 | 
 79 |             var root = TryParseToml(toml);
 80 |             if (root == null) return false;
 81 | 
 82 |             if (!TryGetTable(root, "mcp_servers", out var servers)
 83 |                 && !TryGetTable(root, "mcpServers", out servers))
 84 |             {
 85 |                 return false;
 86 |             }
 87 | 
 88 |             if (!TryGetTable(servers, "unityMCP", out var unity))
 89 |             {
 90 |                 return false;
 91 |             }
 92 | 
 93 |             command = GetTomlString(unity, "command");
 94 |             args = GetTomlStringArray(unity, "args");
 95 | 
 96 |             return !string.IsNullOrEmpty(command) && args != null;
 97 |         }
 98 | 
 99 |         /// <summary>
100 |         /// Safely parses TOML string, returning null on failure
101 |         /// </summary>
102 |         private static TomlTable TryParseToml(string toml)
103 |         {
104 |             if (string.IsNullOrWhiteSpace(toml)) return null;
105 | 
106 |             try
107 |             {
108 |                 using var reader = new StringReader(toml);
109 |                 return TOML.Parse(reader);
110 |             }
111 |             catch (TomlParseException)
112 |             {
113 |                 return null;
114 |             }
115 |             catch (TomlSyntaxException)
116 |             {
117 |                 return null;
118 |             }
119 |             catch (FormatException)
120 |             {
121 |                 return null;
122 |             }
123 |         }
124 | 
125 |         /// <summary>
126 |         /// Creates a TomlTable for the unityMCP server configuration
127 |         /// </summary>
128 |         private static TomlTable CreateUnityMcpTable(string uvPath, string serverSrc)
129 |         {
130 |             var unityMCP = new TomlTable();
131 |             unityMCP["command"] = new TomlString { Value = uvPath };
132 | 
133 |             var argsArray = new TomlArray();
134 |             argsArray.Add(new TomlString { Value = "run" });
135 |             argsArray.Add(new TomlString { Value = "--directory" });
136 |             argsArray.Add(new TomlString { Value = serverSrc });
137 |             argsArray.Add(new TomlString { Value = "server.py" });
138 |             unityMCP["args"] = argsArray;
139 | 
140 |             return unityMCP;
141 |         }
142 | 
143 |         private static bool TryGetTable(TomlTable parent, string key, out TomlTable table)
144 |         {
145 |             table = null;
146 |             if (parent == null) return false;
147 | 
148 |             if (parent.TryGetNode(key, out var node))
149 |             {
150 |                 if (node is TomlTable tbl)
151 |                 {
152 |                     table = tbl;
153 |                     return true;
154 |                 }
155 | 
156 |                 if (node is TomlArray array)
157 |                 {
158 |                     var firstTable = array.Children.OfType<TomlTable>().FirstOrDefault();
159 |                     if (firstTable != null)
160 |                     {
161 |                         table = firstTable;
162 |                         return true;
163 |                     }
164 |                 }
165 |             }
166 | 
167 |             return false;
168 |         }
169 | 
170 |         private static string GetTomlString(TomlTable table, string key)
171 |         {
172 |             if (table != null && table.TryGetNode(key, out var node))
173 |             {
174 |                 if (node is TomlString str) return str.Value;
175 |                 if (node.HasValue) return node.ToString();
176 |             }
177 |             return null;
178 |         }
179 | 
180 |         private static string[] GetTomlStringArray(TomlTable table, string key)
181 |         {
182 |             if (table == null) return null;
183 |             if (!table.TryGetNode(key, out var node)) return null;
184 | 
185 |             if (node is TomlArray array)
186 |             {
187 |                 List<string> values = new List<string>();
188 |                 foreach (TomlNode element in array.Children)
189 |                 {
190 |                     if (element is TomlString str)
191 |                     {
192 |                         values.Add(str.Value);
193 |                     }
194 |                     else if (element.HasValue)
195 |                     {
196 |                         values.Add(element.ToString());
197 |                     }
198 |                 }
199 | 
200 |                 return values.Count > 0 ? values.ToArray() : Array.Empty<string>();
201 |             }
202 | 
203 |             if (node is TomlString single)
204 |             {
205 |                 return new[] { single.Value };
206 |             }
207 | 
208 |             return null;
209 |         }
210 |     }
211 | }
212 | 
```

--------------------------------------------------------------------------------
/tests/test_transport_framing.py:
--------------------------------------------------------------------------------

```python
  1 | from unity_connection import UnityConnection
  2 | import sys
  3 | import json
  4 | import struct
  5 | import socket
  6 | import threading
  7 | import time
  8 | import select
  9 | from pathlib import Path
 10 | 
 11 | import pytest
 12 | 
 13 | # locate server src dynamically to avoid hardcoded layout assumptions
 14 | ROOT = Path(__file__).resolve().parents[1]
 15 | candidates = [
 16 |     ROOT / "MCPForUnity" / "UnityMcpServer~" / "src",
 17 |     ROOT / "UnityMcpServer~" / "src",
 18 | ]
 19 | SRC = next((p for p in candidates if p.exists()), None)
 20 | if SRC is None:
 21 |     searched = "\n".join(str(p) for p in candidates)
 22 |     pytest.skip(
 23 |         "MCP for Unity server source not found. Tried:\n" + searched,
 24 |         allow_module_level=True,
 25 |     )
 26 | sys.path.insert(0, str(SRC))
 27 | 
 28 | 
 29 | def start_dummy_server(greeting: bytes, respond_ping: bool = False):
 30 |     """Start a minimal TCP server for handshake tests."""
 31 |     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 32 |     sock.bind(("127.0.0.1", 0))
 33 |     sock.listen(1)
 34 |     port = sock.getsockname()[1]
 35 |     ready = threading.Event()
 36 | 
 37 |     def _run():
 38 |         ready.set()
 39 |         conn, _ = sock.accept()
 40 |         conn.settimeout(1.0)
 41 |         if greeting:
 42 |             conn.sendall(greeting)
 43 |         if respond_ping:
 44 |             try:
 45 |                 # Read exactly n bytes helper
 46 |                 def _read_exact(n: int) -> bytes:
 47 |                     buf = b""
 48 |                     while len(buf) < n:
 49 |                         chunk = conn.recv(n - len(buf))
 50 |                         if not chunk:
 51 |                             break
 52 |                         buf += chunk
 53 |                     return buf
 54 | 
 55 |                 header = _read_exact(8)
 56 |                 if len(header) == 8:
 57 |                     length = struct.unpack(">Q", header)[0]
 58 |                     payload = _read_exact(length)
 59 |                     if payload == b'{"type":"ping"}':
 60 |                         resp = b'{"type":"pong"}'
 61 |                         conn.sendall(struct.pack(">Q", len(resp)) + resp)
 62 |             except Exception:
 63 |                 pass
 64 |         time.sleep(0.1)
 65 |         try:
 66 |             conn.close()
 67 |         except Exception:
 68 |             pass
 69 |         finally:
 70 |             sock.close()
 71 | 
 72 |     threading.Thread(target=_run, daemon=True).start()
 73 |     ready.wait()
 74 |     return port
 75 | 
 76 | 
 77 | def start_handshake_enforcing_server():
 78 |     """Server that drops connection if client sends data before handshake."""
 79 |     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 80 |     sock.bind(("127.0.0.1", 0))
 81 |     sock.listen(1)
 82 |     port = sock.getsockname()[1]
 83 |     ready = threading.Event()
 84 | 
 85 |     def _run():
 86 |         ready.set()
 87 |         conn, _ = sock.accept()
 88 |         # If client sends any data before greeting, disconnect (poll briefly)
 89 |         try:
 90 |             conn.setblocking(False)
 91 |             deadline = time.time() + 0.15  # short, reduces race with legitimate clients
 92 |             while time.time() < deadline:
 93 |                 r, _, _ = select.select([conn], [], [], 0.01)
 94 |                 if r:
 95 |                     try:
 96 |                         peek = conn.recv(1, socket.MSG_PEEK)
 97 |                     except BlockingIOError:
 98 |                         peek = b""
 99 |                     except Exception:
100 |                         peek = b"\x00"
101 |                     if peek:
102 |                         conn.close()
103 |                         sock.close()
104 |                         return
105 |             # No pre-handshake data observed; send greeting
106 |             conn.setblocking(True)
107 |             conn.sendall(b"MCP/0.1 FRAMING=1\n")
108 |             time.sleep(0.1)
109 |         finally:
110 |             try:
111 |                 conn.close()
112 |             finally:
113 |                 sock.close()
114 | 
115 |     threading.Thread(target=_run, daemon=True).start()
116 |     ready.wait()
117 |     return port
118 | 
119 | 
120 | def test_handshake_requires_framing():
121 |     port = start_dummy_server(b"MCP/0.1\n")
122 |     conn = UnityConnection(host="127.0.0.1", port=port)
123 |     assert conn.connect() is False
124 |     assert conn.sock is None
125 | 
126 | 
127 | def test_small_frame_ping_pong():
128 |     port = start_dummy_server(b"MCP/0.1 FRAMING=1\n", respond_ping=True)
129 |     conn = UnityConnection(host="127.0.0.1", port=port)
130 |     try:
131 |         assert conn.connect() is True
132 |         assert conn.use_framing is True
133 |         payload = b'{"type":"ping"}'
134 |         conn.sock.sendall(struct.pack(">Q", len(payload)) + payload)
135 |         resp = conn.receive_full_response(conn.sock)
136 |         assert json.loads(resp.decode("utf-8"))["type"] == "pong"
137 |     finally:
138 |         conn.disconnect()
139 | 
140 | 
141 | def test_unframed_data_disconnect():
142 |     port = start_handshake_enforcing_server()
143 |     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
144 |     sock.connect(("127.0.0.1", port))
145 |     sock.settimeout(1.0)
146 |     sock.sendall(b"BAD")
147 |     time.sleep(0.4)
148 |     try:
149 |         data = sock.recv(1024)
150 |         assert data == b""
151 |     except (ConnectionResetError, ConnectionAbortedError):
152 |         # Some platforms raise instead of returning empty bytes when the
153 |         # server closes the connection after detecting pre-handshake data.
154 |         pass
155 |     finally:
156 |         sock.close()
157 | 
158 | 
159 | def test_zero_length_payload_heartbeat():
160 |     # Server that sends handshake and a zero-length heartbeat frame followed by a pong payload
161 |     import socket
162 |     import struct
163 |     import threading
164 |     import time
165 | 
166 |     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
167 |     sock.bind(("127.0.0.1", 0))
168 |     sock.listen(1)
169 |     port = sock.getsockname()[1]
170 |     ready = threading.Event()
171 | 
172 |     def _run():
173 |         ready.set()
174 |         conn, _ = sock.accept()
175 |         try:
176 |             conn.sendall(b"MCP/0.1 FRAMING=1\n")
177 |             time.sleep(0.02)
178 |             # Heartbeat frame (length=0)
179 |             conn.sendall(struct.pack(">Q", 0))
180 |             time.sleep(0.02)
181 |             # Real payload frame
182 |             payload = b'{"type":"pong"}'
183 |             conn.sendall(struct.pack(">Q", len(payload)) + payload)
184 |             time.sleep(0.02)
185 |         finally:
186 |             try:
187 |                 conn.close()
188 |             except Exception:
189 |                 pass
190 |             sock.close()
191 | 
192 |     threading.Thread(target=_run, daemon=True).start()
193 |     ready.wait()
194 | 
195 |     conn = UnityConnection(host="127.0.0.1", port=port)
196 |     try:
197 |         assert conn.connect() is True
198 |         # Receive should skip heartbeat and return the pong payload (or empty if only heartbeats seen)
199 |         resp = conn.receive_full_response(conn.sock)
200 |         assert resp in (b'{"type":"pong"}', b"")
201 |     finally:
202 |         conn.disconnect()
203 | 
204 | 
205 | @pytest.mark.skip(reason="TODO: oversized payload should disconnect")
206 | def test_oversized_payload_rejected():
207 |     pass
208 | 
209 | 
210 | @pytest.mark.skip(reason="TODO: partial header/payload triggers timeout and disconnect")
211 | def test_partial_frame_timeout():
212 |     pass
213 | 
214 | 
215 | @pytest.mark.skip(reason="TODO: concurrency test with parallel tool invocations")
216 | def test_parallel_invocations_no_interleaving():
217 |     pass
218 | 
219 | 
220 | @pytest.mark.skip(reason="TODO: reconnection after drop mid-command")
221 | def test_reconnect_mid_command():
222 |     pass
223 | 
```

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

```csharp
  1 | using System;
  2 | using System.Diagnostics;
  3 | using System.IO;
  4 | using System.Runtime.InteropServices;
  5 | using MCPForUnity.Editor.Dependencies.Models;
  6 | using MCPForUnity.Editor.Helpers;
  7 | 
  8 | namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
  9 | {
 10 |     /// <summary>
 11 |     /// Windows-specific dependency detection
 12 |     /// </summary>
 13 |     public class WindowsPlatformDetector : PlatformDetectorBase
 14 |     {
 15 |         public override string PlatformName => "Windows";
 16 | 
 17 |         public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
 18 | 
 19 |         public override DependencyStatus DetectPython()
 20 |         {
 21 |             var status = new DependencyStatus("Python", isRequired: true)
 22 |             {
 23 |                 InstallationHint = GetPythonInstallUrl()
 24 |             };
 25 | 
 26 |             try
 27 |             {
 28 |                 // Check common Python installation paths
 29 |                 var candidates = new[]
 30 |                 {
 31 |                     "python.exe",
 32 |                     "python3.exe",
 33 |                     Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
 34 |                         "Programs", "Python", "Python313", "python.exe"),
 35 |                     Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
 36 |                         "Programs", "Python", "Python312", "python.exe"),
 37 |                     Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
 38 |                         "Programs", "Python", "Python311", "python.exe"),
 39 |                     Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
 40 |                         "Python313", "python.exe"),
 41 |                     Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
 42 |                         "Python312", "python.exe")
 43 |                 };
 44 | 
 45 |                 foreach (var candidate in candidates)
 46 |                 {
 47 |                     if (TryValidatePython(candidate, out string version, out string fullPath))
 48 |                     {
 49 |                         status.IsAvailable = true;
 50 |                         status.Version = version;
 51 |                         status.Path = fullPath;
 52 |                         status.Details = $"Found Python {version} at {fullPath}";
 53 |                         return status;
 54 |                     }
 55 |                 }
 56 | 
 57 |                 // Try PATH resolution using 'where' command
 58 |                 if (TryFindInPath("python.exe", out string pathResult) ||
 59 |                     TryFindInPath("python3.exe", out pathResult))
 60 |                 {
 61 |                     if (TryValidatePython(pathResult, out string version, out string fullPath))
 62 |                     {
 63 |                         status.IsAvailable = true;
 64 |                         status.Version = version;
 65 |                         status.Path = fullPath;
 66 |                         status.Details = $"Found Python {version} in PATH at {fullPath}";
 67 |                         return status;
 68 |                     }
 69 |                 }
 70 | 
 71 |                 status.ErrorMessage = "Python not found. Please install Python 3.10 or later.";
 72 |                 status.Details = "Checked common installation paths and PATH environment variable.";
 73 |             }
 74 |             catch (Exception ex)
 75 |             {
 76 |                 status.ErrorMessage = $"Error detecting Python: {ex.Message}";
 77 |             }
 78 | 
 79 |             return status;
 80 |         }
 81 | 
 82 |         public override string GetPythonInstallUrl()
 83 |         {
 84 |             return "https://apps.microsoft.com/store/detail/python-313/9NCVDN91XZQP";
 85 |         }
 86 | 
 87 |         public override string GetUVInstallUrl()
 88 |         {
 89 |             return "https://docs.astral.sh/uv/getting-started/installation/#windows";
 90 |         }
 91 | 
 92 |         public override string GetInstallationRecommendations()
 93 |         {
 94 |             return @"Windows Installation Recommendations:
 95 | 
 96 | 1. Python: Install from Microsoft Store or python.org
 97 |    - Microsoft Store: Search for 'Python 3.12' or 'Python 3.13'
 98 |    - Direct download: https://python.org/downloads/windows/
 99 | 
100 | 2. UV Package Manager: Install via PowerShell
101 |    - Run: powershell -ExecutionPolicy ByPass -c ""irm https://astral.sh/uv/install.ps1 | iex""
102 |    - Or download from: https://github.com/astral-sh/uv/releases
103 | 
104 | 3. MCP Server: Will be installed automatically by Unity MCP Bridge";
105 |         }
106 | 
107 |         private bool TryValidatePython(string pythonPath, out string version, out string fullPath)
108 |         {
109 |             version = null;
110 |             fullPath = null;
111 | 
112 |             try
113 |             {
114 |                 var psi = new ProcessStartInfo
115 |                 {
116 |                     FileName = pythonPath,
117 |                     Arguments = "--version",
118 |                     UseShellExecute = false,
119 |                     RedirectStandardOutput = true,
120 |                     RedirectStandardError = true,
121 |                     CreateNoWindow = true
122 |                 };
123 | 
124 |                 using var process = Process.Start(psi);
125 |                 if (process == null) return false;
126 | 
127 |                 string output = process.StandardOutput.ReadToEnd().Trim();
128 |                 process.WaitForExit(5000);
129 | 
130 |                 if (process.ExitCode == 0 && output.StartsWith("Python "))
131 |                 {
132 |                     version = output.Substring(7); // Remove "Python " prefix
133 |                     fullPath = pythonPath;
134 | 
135 |                     // Validate minimum version (Python 4+ or Python 3.10+)
136 |                     if (TryParseVersion(version, out var major, out var minor))
137 |                     {
138 |                         return major > 3 || (major >= 3 && minor >= 10);
139 |                     }
140 |                 }
141 |             }
142 |             catch
143 |             {
144 |                 // Ignore validation errors
145 |             }
146 | 
147 |             return false;
148 |         }
149 | 
150 |         private bool TryFindInPath(string executable, out string fullPath)
151 |         {
152 |             fullPath = null;
153 | 
154 |             try
155 |             {
156 |                 var psi = new ProcessStartInfo
157 |                 {
158 |                     FileName = "where",
159 |                     Arguments = executable,
160 |                     UseShellExecute = false,
161 |                     RedirectStandardOutput = true,
162 |                     RedirectStandardError = true,
163 |                     CreateNoWindow = true
164 |                 };
165 | 
166 |                 using var process = Process.Start(psi);
167 |                 if (process == null) return false;
168 | 
169 |                 string output = process.StandardOutput.ReadToEnd().Trim();
170 |                 process.WaitForExit(3000);
171 | 
172 |                 if (process.ExitCode == 0 && !string.IsNullOrEmpty(output))
173 |                 {
174 |                     // Take the first result
175 |                     var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
176 |                     if (lines.Length > 0)
177 |                     {
178 |                         fullPath = lines[0].Trim();
179 |                         return File.Exists(fullPath);
180 |                     }
181 |                 }
182 |             }
183 |             catch
184 |             {
185 |                 // Ignore errors
186 |             }
187 | 
188 |             return false;
189 |         }
190 |     }
191 | }
192 | 
```

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

```csharp
  1 | using System;
  2 | using System.Diagnostics;
  3 | using System.IO;
  4 | using System.Runtime.InteropServices;
  5 | using MCPForUnity.Editor.Dependencies.Models;
  6 | using MCPForUnity.Editor.Helpers;
  7 | 
  8 | namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
  9 | {
 10 |     /// <summary>
 11 |     /// Windows-specific dependency detection
 12 |     /// </summary>
 13 |     public class WindowsPlatformDetector : PlatformDetectorBase
 14 |     {
 15 |         public override string PlatformName => "Windows";
 16 | 
 17 |         public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
 18 | 
 19 |         public override DependencyStatus DetectPython()
 20 |         {
 21 |             var status = new DependencyStatus("Python", isRequired: true)
 22 |             {
 23 |                 InstallationHint = GetPythonInstallUrl()
 24 |             };
 25 | 
 26 |             try
 27 |             {
 28 |                 // Check common Python installation paths
 29 |                 var candidates = new[]
 30 |                 {
 31 |                     "python.exe",
 32 |                     "python3.exe",
 33 |                     Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
 34 |                         "Programs", "Python", "Python313", "python.exe"),
 35 |                     Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
 36 |                         "Programs", "Python", "Python312", "python.exe"),
 37 |                     Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
 38 |                         "Programs", "Python", "Python311", "python.exe"),
 39 |                     Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
 40 |                         "Python313", "python.exe"),
 41 |                     Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
 42 |                         "Python312", "python.exe")
 43 |                 };
 44 | 
 45 |                 foreach (var candidate in candidates)
 46 |                 {
 47 |                     if (TryValidatePython(candidate, out string version, out string fullPath))
 48 |                     {
 49 |                         status.IsAvailable = true;
 50 |                         status.Version = version;
 51 |                         status.Path = fullPath;
 52 |                         status.Details = $"Found Python {version} at {fullPath}";
 53 |                         return status;
 54 |                     }
 55 |                 }
 56 | 
 57 |                 // Try PATH resolution using 'where' command
 58 |                 if (TryFindInPath("python.exe", out string pathResult) ||
 59 |                     TryFindInPath("python3.exe", out pathResult))
 60 |                 {
 61 |                     if (TryValidatePython(pathResult, out string version, out string fullPath))
 62 |                     {
 63 |                         status.IsAvailable = true;
 64 |                         status.Version = version;
 65 |                         status.Path = fullPath;
 66 |                         status.Details = $"Found Python {version} in PATH at {fullPath}";
 67 |                         return status;
 68 |                     }
 69 |                 }
 70 | 
 71 |                 status.ErrorMessage = "Python not found. Please install Python 3.10 or later.";
 72 |                 status.Details = "Checked common installation paths and PATH environment variable.";
 73 |             }
 74 |             catch (Exception ex)
 75 |             {
 76 |                 status.ErrorMessage = $"Error detecting Python: {ex.Message}";
 77 |             }
 78 | 
 79 |             return status;
 80 |         }
 81 | 
 82 |         public override string GetPythonInstallUrl()
 83 |         {
 84 |             return "https://apps.microsoft.com/store/detail/python-313/9NCVDN91XZQP";
 85 |         }
 86 | 
 87 |         public override string GetUVInstallUrl()
 88 |         {
 89 |             return "https://docs.astral.sh/uv/getting-started/installation/#windows";
 90 |         }
 91 | 
 92 |         public override string GetInstallationRecommendations()
 93 |         {
 94 |             return @"Windows Installation Recommendations:
 95 | 
 96 | 1. Python: Install from Microsoft Store or python.org
 97 |    - Microsoft Store: Search for 'Python 3.12' or 'Python 3.13'
 98 |    - Direct download: https://python.org/downloads/windows/
 99 | 
100 | 2. UV Package Manager: Install via PowerShell
101 |    - Run: powershell -ExecutionPolicy ByPass -c ""irm https://astral.sh/uv/install.ps1 | iex""
102 |    - Or download from: https://github.com/astral-sh/uv/releases
103 | 
104 | 3. MCP Server: Will be installed automatically by MCP for Unity Bridge";
105 |         }
106 | 
107 |         private bool TryValidatePython(string pythonPath, out string version, out string fullPath)
108 |         {
109 |             version = null;
110 |             fullPath = null;
111 | 
112 |             try
113 |             {
114 |                 var psi = new ProcessStartInfo
115 |                 {
116 |                     FileName = pythonPath,
117 |                     Arguments = "--version",
118 |                     UseShellExecute = false,
119 |                     RedirectStandardOutput = true,
120 |                     RedirectStandardError = true,
121 |                     CreateNoWindow = true
122 |                 };
123 | 
124 |                 using var process = Process.Start(psi);
125 |                 if (process == null) return false;
126 | 
127 |                 string output = process.StandardOutput.ReadToEnd().Trim();
128 |                 process.WaitForExit(5000);
129 | 
130 |                 if (process.ExitCode == 0 && output.StartsWith("Python "))
131 |                 {
132 |                     version = output.Substring(7); // Remove "Python " prefix
133 |                     fullPath = pythonPath;
134 | 
135 |                     // Validate minimum version (Python 4+ or Python 3.10+)
136 |                     if (TryParseVersion(version, out var major, out var minor))
137 |                     {
138 |                         return major > 3 || (major >= 3 && minor >= 10);
139 |                     }
140 |                 }
141 |             }
142 |             catch
143 |             {
144 |                 // Ignore validation errors
145 |             }
146 | 
147 |             return false;
148 |         }
149 | 
150 |         private bool TryFindInPath(string executable, out string fullPath)
151 |         {
152 |             fullPath = null;
153 | 
154 |             try
155 |             {
156 |                 var psi = new ProcessStartInfo
157 |                 {
158 |                     FileName = "where",
159 |                     Arguments = executable,
160 |                     UseShellExecute = false,
161 |                     RedirectStandardOutput = true,
162 |                     RedirectStandardError = true,
163 |                     CreateNoWindow = true
164 |                 };
165 | 
166 |                 using var process = Process.Start(psi);
167 |                 if (process == null) return false;
168 | 
169 |                 string output = process.StandardOutput.ReadToEnd().Trim();
170 |                 process.WaitForExit(3000);
171 | 
172 |                 if (process.ExitCode == 0 && !string.IsNullOrEmpty(output))
173 |                 {
174 |                     // Take the first result
175 |                     var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
176 |                     if (lines.Length > 0)
177 |                     {
178 |                         fullPath = lines[0].Trim();
179 |                         return File.Exists(fullPath);
180 |                     }
181 |                 }
182 |             }
183 |             catch
184 |             {
185 |                 // Ignore errors
186 |             }
187 | 
188 |             return false;
189 |         }
190 |     }
191 | }
192 | 
```

--------------------------------------------------------------------------------
/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptValidationTests.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using NUnit.Framework;
  3 | using UnityEngine;
  4 | using Newtonsoft.Json.Linq;
  5 | using MCPForUnity.Editor.Tools;
  6 | using System.Reflection;
  7 | 
  8 | namespace MCPForUnityTests.Editor.Tools
  9 | {
 10 |     /// <summary>
 11 |     /// In-memory tests for ManageScript validation logic.
 12 |     /// These tests focus on the validation methods directly without creating files.
 13 |     /// </summary>
 14 |     public class ManageScriptValidationTests
 15 |     {
 16 |         [Test]
 17 |         public void HandleCommand_NullParams_ReturnsError()
 18 |         {
 19 |             var result = ManageScript.HandleCommand(null);
 20 |             Assert.IsNotNull(result, "Should handle null parameters gracefully");
 21 |         }
 22 | 
 23 |         [Test]
 24 |         public void HandleCommand_InvalidAction_ReturnsError()
 25 |         {
 26 |             var paramsObj = new JObject
 27 |             {
 28 |                 ["action"] = "invalid_action",
 29 |                 ["name"] = "TestScript",
 30 |                 ["path"] = "Assets/Scripts"
 31 |             };
 32 | 
 33 |             var result = ManageScript.HandleCommand(paramsObj);
 34 |             Assert.IsNotNull(result, "Should return error result for invalid action");
 35 |         }
 36 | 
 37 |         [Test]
 38 |         public void CheckBalancedDelimiters_ValidCode_ReturnsTrue()
 39 |         {
 40 |             string validCode = "using UnityEngine;\n\npublic class TestClass : MonoBehaviour\n{\n    void Start()\n    {\n        Debug.Log(\"test\");\n    }\n}";
 41 | 
 42 |             bool result = CallCheckBalancedDelimiters(validCode, out int line, out char expected);
 43 |             Assert.IsTrue(result, "Valid C# code should pass balance check");
 44 |         }
 45 | 
 46 |         [Test]
 47 |         public void CheckBalancedDelimiters_UnbalancedBraces_ReturnsFalse()
 48 |         {
 49 |             string unbalancedCode = "using UnityEngine;\n\npublic class TestClass : MonoBehaviour\n{\n    void Start()\n    {\n        Debug.Log(\"test\");\n    // Missing closing brace";
 50 | 
 51 |             bool result = CallCheckBalancedDelimiters(unbalancedCode, out int line, out char expected);
 52 |             Assert.IsFalse(result, "Unbalanced code should fail balance check");
 53 |         }
 54 | 
 55 |         [Test]
 56 |         public void CheckBalancedDelimiters_StringWithBraces_ReturnsTrue()
 57 |         {
 58 |             string codeWithStringBraces = "using UnityEngine;\n\npublic class TestClass : MonoBehaviour\n{\n    public string json = \"{key: value}\";\n    void Start() { Debug.Log(json); }\n}";
 59 | 
 60 |             bool result = CallCheckBalancedDelimiters(codeWithStringBraces, out int line, out char expected);
 61 |             Assert.IsTrue(result, "Code with braces in strings should pass balance check");
 62 |         }
 63 | 
 64 |         [Test]
 65 |         public void CheckScopedBalance_ValidCode_ReturnsTrue()
 66 |         {
 67 |             string validCode = "{ Debug.Log(\"test\"); }";
 68 | 
 69 |             bool result = CallCheckScopedBalance(validCode, 0, validCode.Length);
 70 |             Assert.IsTrue(result, "Valid scoped code should pass balance check");
 71 |         }
 72 | 
 73 |         [Test]
 74 |         public void CheckScopedBalance_ShouldTolerateOuterContext_ReturnsTrue()
 75 |         {
 76 |             // This simulates a snippet extracted from a larger context
 77 |             string contextSnippet = "    Debug.Log(\"inside method\");\n}  // This closing brace is from outer context";
 78 | 
 79 |             bool result = CallCheckScopedBalance(contextSnippet, 0, contextSnippet.Length);
 80 | 
 81 |             // Scoped balance should tolerate some imbalance from outer context
 82 |             Assert.IsTrue(result, "Scoped balance should tolerate outer context imbalance");
 83 |         }
 84 | 
 85 |         [Test]
 86 |         public void TicTacToe3D_ValidationScenario_DoesNotCrash()
 87 |         {
 88 |             // Test the scenario that was causing issues without file I/O
 89 |             string ticTacToeCode = "using UnityEngine;\n\npublic class TicTacToe3D : MonoBehaviour\n{\n    public string gameState = \"active\";\n    void Start() { Debug.Log(\"Game started\"); }\n    public void MakeMove(int position) { if (gameState == \"active\") Debug.Log($\"Move {position}\"); }\n}";
 90 | 
 91 |             // Test that the validation methods don't crash on this code
 92 |             bool balanceResult = CallCheckBalancedDelimiters(ticTacToeCode, out int line, out char expected);
 93 |             bool scopedResult = CallCheckScopedBalance(ticTacToeCode, 0, ticTacToeCode.Length);
 94 | 
 95 |             Assert.IsTrue(balanceResult, "TicTacToe3D code should pass balance validation");
 96 |             Assert.IsTrue(scopedResult, "TicTacToe3D code should pass scoped balance validation");
 97 |         }
 98 | 
 99 |         // Helper methods to access private ManageScript methods via reflection
100 |         private bool CallCheckBalancedDelimiters(string contents, out int line, out char expected)
101 |         {
102 |             line = 0;
103 |             expected = ' ';
104 | 
105 |             try
106 |             {
107 |                 var method = typeof(ManageScript).GetMethod("CheckBalancedDelimiters",
108 |                     BindingFlags.NonPublic | BindingFlags.Static);
109 | 
110 |                 if (method != null)
111 |                 {
112 |                     var parameters = new object[] { contents, line, expected };
113 |                     var result = (bool)method.Invoke(null, parameters);
114 |                     line = (int)parameters[1];
115 |                     expected = (char)parameters[2];
116 |                     return result;
117 |                 }
118 |             }
119 |             catch (Exception ex)
120 |             {
121 |                 Debug.LogWarning($"Could not test CheckBalancedDelimiters directly: {ex.Message}");
122 |             }
123 | 
124 |             // Fallback: basic structural check
125 |             return BasicBalanceCheck(contents);
126 |         }
127 | 
128 |         private bool CallCheckScopedBalance(string text, int start, int end)
129 |         {
130 |             try
131 |             {
132 |                 var method = typeof(ManageScript).GetMethod("CheckScopedBalance",
133 |                     BindingFlags.NonPublic | BindingFlags.Static);
134 | 
135 |                 if (method != null)
136 |                 {
137 |                     return (bool)method.Invoke(null, new object[] { text, start, end });
138 |                 }
139 |             }
140 |             catch (Exception ex)
141 |             {
142 |                 Debug.LogWarning($"Could not test CheckScopedBalance directly: {ex.Message}");
143 |             }
144 | 
145 |             return true; // Default to passing if we can't test the actual method
146 |         }
147 | 
148 |         private bool BasicBalanceCheck(string contents)
149 |         {
150 |             // Simple fallback balance check
151 |             int braceCount = 0;
152 |             bool inString = false;
153 |             bool escaped = false;
154 | 
155 |             for (int i = 0; i < contents.Length; i++)
156 |             {
157 |                 char c = contents[i];
158 | 
159 |                 if (escaped)
160 |                 {
161 |                     escaped = false;
162 |                     continue;
163 |                 }
164 | 
165 |                 if (inString)
166 |                 {
167 |                     if (c == '\\') escaped = true;
168 |                     else if (c == '"') inString = false;
169 |                     continue;
170 |                 }
171 | 
172 |                 if (c == '"') inString = true;
173 |                 else if (c == '{') braceCount++;
174 |                 else if (c == '}') braceCount--;
175 | 
176 |                 if (braceCount < 0) return false;
177 |             }
178 | 
179 |             return braceCount == 0;
180 |         }
181 |     }
182 | }
183 | 
```

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

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.IO;
  4 | using MCPForUnity.Editor.Models;
  5 | 
  6 | namespace MCPForUnity.Editor.Data
  7 | {
  8 |     public class McpClients
  9 |     {
 10 |         public List<McpClient> clients = new()
 11 |         {
 12 |             // 1) Cursor
 13 |             new()
 14 |             {
 15 |                 name = "Cursor",
 16 |                 windowsConfigPath = Path.Combine(
 17 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 18 |                     ".cursor",
 19 |                     "mcp.json"
 20 |                 ),
 21 |                 macConfigPath = Path.Combine(
 22 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 23 |                     ".cursor",
 24 |                     "mcp.json"
 25 |                 ),
 26 |                 linuxConfigPath = Path.Combine(
 27 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 28 |                     ".cursor",
 29 |                     "mcp.json"
 30 |                 ),
 31 |                 mcpType = McpTypes.Cursor,
 32 |                 configStatus = "Not Configured",
 33 |             },
 34 |             // 2) Claude Code
 35 |             new()
 36 |             {
 37 |                 name = "Claude Code",
 38 |                 windowsConfigPath = Path.Combine(
 39 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 40 |                     ".claude.json"
 41 |                 ),
 42 |                 macConfigPath = Path.Combine(
 43 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 44 |                     ".claude.json"
 45 |                 ),
 46 |                 linuxConfigPath = Path.Combine(
 47 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 48 |                     ".claude.json"
 49 |                 ),
 50 |                 mcpType = McpTypes.ClaudeCode,
 51 |                 configStatus = "Not Configured",
 52 |             },
 53 |             // 3) Windsurf
 54 |             new()
 55 |             {
 56 |                 name = "Windsurf",
 57 |                 windowsConfigPath = Path.Combine(
 58 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 59 |                     ".codeium",
 60 |                     "windsurf",
 61 |                     "mcp_config.json"
 62 |                 ),
 63 |                 macConfigPath = Path.Combine(
 64 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 65 |                     ".codeium",
 66 |                     "windsurf",
 67 |                     "mcp_config.json"
 68 |                 ),
 69 |                 linuxConfigPath = Path.Combine(
 70 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 71 |                     ".codeium",
 72 |                     "windsurf",
 73 |                     "mcp_config.json"
 74 |                 ),
 75 |                 mcpType = McpTypes.Windsurf,
 76 |                 configStatus = "Not Configured",
 77 |             },
 78 |             // 4) Claude Desktop
 79 |             new()
 80 |             {
 81 |                 name = "Claude Desktop",
 82 |                 windowsConfigPath = Path.Combine(
 83 |                     Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
 84 |                     "Claude",
 85 |                     "claude_desktop_config.json"
 86 |                 ),
 87 | 
 88 |                 macConfigPath = Path.Combine(
 89 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 90 |                     "Library",
 91 |                     "Application Support",
 92 |                     "Claude",
 93 |                     "claude_desktop_config.json"
 94 |                 ),
 95 |                 linuxConfigPath = Path.Combine(
 96 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 97 |                     ".config",
 98 |                     "Claude",
 99 |                     "claude_desktop_config.json"
100 |                 ),
101 | 
102 |                 mcpType = McpTypes.ClaudeDesktop,
103 |                 configStatus = "Not Configured",
104 |             },
105 |             // 5) VSCode GitHub Copilot
106 |             new()
107 |             {
108 |                 name = "VSCode GitHub Copilot",
109 |                 // Windows path is canonical under %AppData%\Code\User
110 |                 windowsConfigPath = Path.Combine(
111 |                     Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
112 |                     "Code",
113 |                     "User",
114 |                     "mcp.json"
115 |                 ),
116 |                 // macOS: ~/Library/Application Support/Code/User/mcp.json
117 |                 macConfigPath = Path.Combine(
118 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
119 |                     "Library",
120 |                     "Application Support",
121 |                     "Code",
122 |                     "User",
123 |                     "mcp.json"
124 |                 ),
125 |                 // Linux: ~/.config/Code/User/mcp.json
126 |                 linuxConfigPath = Path.Combine(
127 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
128 |                     ".config",
129 |                     "Code",
130 |                     "User",
131 |                     "mcp.json"
132 |                 ),
133 |                 mcpType = McpTypes.VSCode,
134 |                 configStatus = "Not Configured",
135 |             },
136 |             // 3) Kiro
137 |             new()
138 |             {
139 |                 name = "Kiro",
140 |                 windowsConfigPath = Path.Combine(
141 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
142 |                     ".kiro",
143 |                     "settings",
144 |                     "mcp.json"
145 |                 ),
146 |                 macConfigPath = Path.Combine(
147 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
148 |                     ".kiro",
149 |                     "settings",
150 |                     "mcp.json"
151 |                 ),
152 |                 linuxConfigPath = Path.Combine(
153 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
154 |                     ".kiro",
155 |                     "settings",
156 |                     "mcp.json"
157 |                 ),
158 |                 mcpType = McpTypes.Kiro,
159 |                 configStatus = "Not Configured",
160 |             },
161 |             // 4) Codex CLI
162 |             new()
163 |             {
164 |                 name = "Codex CLI",
165 |                 windowsConfigPath = Path.Combine(
166 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
167 |                     ".codex",
168 |                     "config.toml"
169 |                 ),
170 |                 macConfigPath = Path.Combine(
171 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
172 |                     ".codex",
173 |                     "config.toml"
174 |                 ),
175 |                 linuxConfigPath = Path.Combine(
176 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
177 |                     ".codex",
178 |                     "config.toml"
179 |                 ),
180 |                 mcpType = McpTypes.Codex,
181 |                 configStatus = "Not Configured",
182 |             },
183 |         };
184 | 
185 |         // Initialize status enums after construction
186 |         public McpClients()
187 |         {
188 |             foreach (var client in clients)
189 |             {
190 |                 if (client.configStatus == "Not Configured")
191 |                 {
192 |                     client.status = McpStatus.NotConfigured;
193 |                 }
194 |             }
195 |         }
196 |     }
197 | }
198 | 
```

--------------------------------------------------------------------------------
/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/AIPropertyMatchingTests.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using NUnit.Framework;
  4 | using UnityEngine;
  5 | using MCPForUnity.Editor.Tools;
  6 | using static MCPForUnity.Editor.Tools.ManageGameObject;
  7 | 
  8 | namespace MCPForUnityTests.Editor.Tools
  9 | {
 10 |     public class AIPropertyMatchingTests
 11 |     {
 12 |         private List<string> sampleProperties;
 13 | 
 14 |         [SetUp]
 15 |         public void SetUp()
 16 |         {
 17 |             sampleProperties = new List<string>
 18 |             {
 19 |                 "maxReachDistance",
 20 |                 "maxHorizontalDistance",
 21 |                 "maxVerticalDistance",
 22 |                 "moveSpeed",
 23 |                 "healthPoints",
 24 |                 "playerName",
 25 |                 "isEnabled",
 26 |                 "mass",
 27 |                 "velocity",
 28 |                 "transform"
 29 |             };
 30 |         }
 31 | 
 32 |         [Test]
 33 |         public void GetAllComponentProperties_ReturnsValidProperties_ForTransform()
 34 |         {
 35 |             var properties = ComponentResolver.GetAllComponentProperties(typeof(Transform));
 36 | 
 37 |             Assert.IsNotEmpty(properties, "Transform should have properties");
 38 |             Assert.Contains("position", properties, "Transform should have position property");
 39 |             Assert.Contains("rotation", properties, "Transform should have rotation property");
 40 |             Assert.Contains("localScale", properties, "Transform should have localScale property");
 41 |         }
 42 | 
 43 |         [Test]
 44 |         public void GetAllComponentProperties_ReturnsEmpty_ForNullType()
 45 |         {
 46 |             var properties = ComponentResolver.GetAllComponentProperties(null);
 47 | 
 48 |             Assert.IsEmpty(properties, "Null type should return empty list");
 49 |         }
 50 | 
 51 |         [Test]
 52 |         public void GetAIPropertySuggestions_ReturnsEmpty_ForNullInput()
 53 |         {
 54 |             var suggestions = ComponentResolver.GetAIPropertySuggestions(null, sampleProperties);
 55 | 
 56 |             Assert.IsEmpty(suggestions, "Null input should return no suggestions");
 57 |         }
 58 | 
 59 |         [Test]
 60 |         public void GetAIPropertySuggestions_ReturnsEmpty_ForEmptyInput()
 61 |         {
 62 |             var suggestions = ComponentResolver.GetAIPropertySuggestions("", sampleProperties);
 63 | 
 64 |             Assert.IsEmpty(suggestions, "Empty input should return no suggestions");
 65 |         }
 66 | 
 67 |         [Test]
 68 |         public void GetAIPropertySuggestions_ReturnsEmpty_ForEmptyPropertyList()
 69 |         {
 70 |             var suggestions = ComponentResolver.GetAIPropertySuggestions("test", new List<string>());
 71 | 
 72 |             Assert.IsEmpty(suggestions, "Empty property list should return no suggestions");
 73 |         }
 74 | 
 75 |         [Test]
 76 |         public void GetAIPropertySuggestions_FindsExactMatch_AfterCleaning()
 77 |         {
 78 |             var suggestions = ComponentResolver.GetAIPropertySuggestions("Max Reach Distance", sampleProperties);
 79 | 
 80 |             Assert.Contains("maxReachDistance", suggestions, "Should find exact match after cleaning spaces");
 81 |             Assert.GreaterOrEqual(suggestions.Count, 1, "Should return at least one match for exact match");
 82 |         }
 83 | 
 84 |         [Test]
 85 |         public void GetAIPropertySuggestions_FindsMultipleWordMatches()
 86 |         {
 87 |             var suggestions = ComponentResolver.GetAIPropertySuggestions("max distance", sampleProperties);
 88 | 
 89 |             Assert.Contains("maxReachDistance", suggestions, "Should match maxReachDistance");
 90 |             Assert.Contains("maxHorizontalDistance", suggestions, "Should match maxHorizontalDistance");
 91 |             Assert.Contains("maxVerticalDistance", suggestions, "Should match maxVerticalDistance");
 92 |         }
 93 | 
 94 |         [Test]
 95 |         public void GetAIPropertySuggestions_FindsSimilarStrings_WithTypos()
 96 |         {
 97 |             var suggestions = ComponentResolver.GetAIPropertySuggestions("movespeed", sampleProperties); // missing capital S
 98 | 
 99 |             Assert.Contains("moveSpeed", suggestions, "Should find moveSpeed despite missing capital");
100 |         }
101 | 
102 |         [Test]
103 |         public void GetAIPropertySuggestions_FindsSemanticMatches_ForCommonTerms()
104 |         {
105 |             var suggestions = ComponentResolver.GetAIPropertySuggestions("weight", sampleProperties);
106 | 
107 |             // Note: Current algorithm might not find "mass" but should handle it gracefully
108 |             Assert.IsNotNull(suggestions, "Should return valid suggestions list");
109 |         }
110 | 
111 |         [Test]
112 |         public void GetAIPropertySuggestions_LimitsResults_ToReasonableNumber()
113 |         {
114 |             // Test with input that might match many properties
115 |             var suggestions = ComponentResolver.GetAIPropertySuggestions("m", sampleProperties);
116 | 
117 |             Assert.LessOrEqual(suggestions.Count, 3, "Should limit suggestions to 3 or fewer");
118 |         }
119 | 
120 |         [Test]
121 |         public void GetAIPropertySuggestions_CachesResults()
122 |         {
123 |             var input = "Max Reach Distance";
124 | 
125 |             // First call
126 |             var suggestions1 = ComponentResolver.GetAIPropertySuggestions(input, sampleProperties);
127 | 
128 |             // Second call should use cache (tested indirectly by ensuring consistency)
129 |             var suggestions2 = ComponentResolver.GetAIPropertySuggestions(input, sampleProperties);
130 | 
131 |             Assert.AreEqual(suggestions1.Count, suggestions2.Count, "Cached results should be consistent");
132 |             CollectionAssert.AreEqual(suggestions1, suggestions2, "Cached results should be identical");
133 |         }
134 | 
135 |         [Test]
136 |         public void GetAIPropertySuggestions_HandlesUnityNamingConventions()
137 |         {
138 |             var unityStyleProperties = new List<string> { "isKinematic", "useGravity", "maxLinearVelocity" };
139 | 
140 |             var suggestions1 = ComponentResolver.GetAIPropertySuggestions("is kinematic", unityStyleProperties);
141 |             var suggestions2 = ComponentResolver.GetAIPropertySuggestions("use gravity", unityStyleProperties);
142 |             var suggestions3 = ComponentResolver.GetAIPropertySuggestions("max linear velocity", unityStyleProperties);
143 | 
144 |             Assert.Contains("isKinematic", suggestions1, "Should handle 'is' prefix convention");
145 |             Assert.Contains("useGravity", suggestions2, "Should handle 'use' prefix convention");
146 |             Assert.Contains("maxLinearVelocity", suggestions3, "Should handle 'max' prefix convention");
147 |         }
148 | 
149 |         [Test]
150 |         public void GetAIPropertySuggestions_PrioritizesExactMatches()
151 |         {
152 |             var properties = new List<string> { "speed", "moveSpeed", "maxSpeed", "speedMultiplier" };
153 |             var suggestions = ComponentResolver.GetAIPropertySuggestions("speed", properties);
154 | 
155 |             Assert.IsNotEmpty(suggestions, "Should find suggestions");
156 |             Assert.Contains("speed", suggestions, "Exact match should be included in results");
157 |             // Note: Implementation may or may not prioritize exact matches first
158 |         }
159 | 
160 |         [Test]
161 |         public void GetAIPropertySuggestions_HandlesCaseInsensitive()
162 |         {
163 |             var suggestions1 = ComponentResolver.GetAIPropertySuggestions("MAXREACHDISTANCE", sampleProperties);
164 |             var suggestions2 = ComponentResolver.GetAIPropertySuggestions("maxreachdistance", sampleProperties);
165 | 
166 |             Assert.Contains("maxReachDistance", suggestions1, "Should handle uppercase input");
167 |             Assert.Contains("maxReachDistance", suggestions2, "Should handle lowercase input");
168 |         }
169 |     }
170 | }
171 | 
```

--------------------------------------------------------------------------------
/UnityMcpBridge/Editor/Data/McpClients.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.IO;
  4 | using System.Runtime.InteropServices;
  5 | using MCPForUnity.Editor.Models;
  6 | 
  7 | namespace MCPForUnity.Editor.Data
  8 | {
  9 |     public class McpClients
 10 |     {
 11 |         public List<McpClient> clients = new()
 12 |         {
 13 |             // 1) Cursor
 14 |             new()
 15 |             {
 16 |                 name = "Cursor",
 17 |                 windowsConfigPath = Path.Combine(
 18 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 19 |                     ".cursor",
 20 |                     "mcp.json"
 21 |                 ),
 22 |                 macConfigPath = Path.Combine(
 23 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 24 |                     ".cursor",
 25 |                     "mcp.json"
 26 |                 ),
 27 |                 linuxConfigPath = Path.Combine(
 28 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 29 |                     ".cursor",
 30 |                     "mcp.json"
 31 |                 ),
 32 |                 mcpType = McpTypes.Cursor,
 33 |                 configStatus = "Not Configured",
 34 |             },
 35 |             // 2) Claude Code
 36 |             new()
 37 |             {
 38 |                 name = "Claude Code",
 39 |                 windowsConfigPath = Path.Combine(
 40 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 41 |                     ".claude.json"
 42 |                 ),
 43 |                 macConfigPath = Path.Combine(
 44 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 45 |                     ".claude.json"
 46 |                 ),
 47 |                 linuxConfigPath = Path.Combine(
 48 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 49 |                     ".claude.json"
 50 |                 ),
 51 |                 mcpType = McpTypes.ClaudeCode,
 52 |                 configStatus = "Not Configured",
 53 |             },
 54 |             // 3) Windsurf
 55 |             new()
 56 |             {
 57 |                 name = "Windsurf",
 58 |                 windowsConfigPath = Path.Combine(
 59 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 60 |                     ".codeium",
 61 |                     "windsurf",
 62 |                     "mcp_config.json"
 63 |                 ),
 64 |                 macConfigPath = Path.Combine(
 65 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 66 |                     ".codeium",
 67 |                     "windsurf",
 68 |                     "mcp_config.json"
 69 |                 ),
 70 |                 linuxConfigPath = Path.Combine(
 71 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 72 |                     ".codeium",
 73 |                     "windsurf",
 74 |                     "mcp_config.json"
 75 |                 ),
 76 |                 mcpType = McpTypes.Windsurf,
 77 |                 configStatus = "Not Configured",
 78 |             },
 79 |             // 4) Claude Desktop
 80 |             new()
 81 |             {
 82 |                 name = "Claude Desktop",
 83 |                 windowsConfigPath = Path.Combine(
 84 |                     Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
 85 |                     "Claude",
 86 |                     "claude_desktop_config.json"
 87 |                 ),
 88 | 
 89 |                 macConfigPath = Path.Combine(
 90 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 91 |                     "Library",
 92 |                     "Application Support",
 93 |                     "Claude",
 94 |                     "claude_desktop_config.json"
 95 |                 ),
 96 |                 linuxConfigPath = Path.Combine(
 97 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 98 |                     ".config",
 99 |                     "Claude",
100 |                     "claude_desktop_config.json"
101 |                 ),
102 | 
103 |                 mcpType = McpTypes.ClaudeDesktop,
104 |                 configStatus = "Not Configured",
105 |             },
106 |             // 5) VSCode GitHub Copilot
107 |             new()
108 |             {
109 |                 name = "VSCode GitHub Copilot",
110 |                 // Windows path is canonical under %AppData%\Code\User
111 |                 windowsConfigPath = Path.Combine(
112 |                     Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
113 |                     "Code",
114 |                     "User",
115 |                     "mcp.json"
116 |                 ),
117 |                 // macOS: ~/Library/Application Support/Code/User/mcp.json
118 |                 macConfigPath = Path.Combine(
119 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
120 |                     "Library",
121 |                     "Application Support",
122 |                     "Code",
123 |                     "User",
124 |                     "mcp.json"
125 |                 ),
126 |                 // Linux: ~/.config/Code/User/mcp.json
127 |                 linuxConfigPath = Path.Combine(
128 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
129 |                     ".config",
130 |                     "Code",
131 |                     "User",
132 |                     "mcp.json"
133 |                 ),
134 |                 mcpType = McpTypes.VSCode,
135 |                 configStatus = "Not Configured",
136 |             },
137 |             // 3) Kiro
138 |             new()
139 |             {
140 |                 name = "Kiro",
141 |                 windowsConfigPath = Path.Combine(
142 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
143 |                     ".kiro",
144 |                     "settings",
145 |                     "mcp.json"
146 |                 ),
147 |                 macConfigPath = Path.Combine(
148 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
149 |                     ".kiro",
150 |                     "settings",
151 |                     "mcp.json"
152 |                 ),
153 |                 linuxConfigPath = Path.Combine(
154 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
155 |                     ".kiro",
156 |                     "settings",
157 |                     "mcp.json"
158 |                 ),
159 |                 mcpType = McpTypes.Kiro,
160 |                 configStatus = "Not Configured",
161 |             },
162 |             // 4) Codex CLI
163 |             new()
164 |             {
165 |                 name = "Codex CLI",
166 |                 windowsConfigPath = Path.Combine(
167 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
168 |                     ".codex",
169 |                     "config.toml"
170 |                 ),
171 |                 macConfigPath = Path.Combine(
172 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
173 |                     ".codex",
174 |                     "config.toml"
175 |                 ),
176 |                 linuxConfigPath = Path.Combine(
177 |                     Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
178 |                     ".codex",
179 |                     "config.toml"
180 |                 ),
181 |                 mcpType = McpTypes.Codex,
182 |                 configStatus = "Not Configured",
183 |             },
184 |         };
185 | 
186 |         // Initialize status enums after construction
187 |         public McpClients()
188 |         {
189 |             foreach (var client in clients)
190 |             {
191 |                 if (client.configStatus == "Not Configured")
192 |                 {
193 |                     client.status = McpStatus.NotConfigured;
194 |                 }
195 |             }
196 |         }
197 |     }
198 | }
199 | 
```

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

```python
  1 | from telemetry import record_telemetry, record_milestone, RecordType, MilestoneType
  2 | from mcp.server.fastmcp import FastMCP
  3 | import logging
  4 | from logging.handlers import RotatingFileHandler
  5 | import os
  6 | from contextlib import asynccontextmanager
  7 | from typing import AsyncIterator, Dict, Any
  8 | from config import config
  9 | from tools import register_all_tools
 10 | from resources import register_all_resources
 11 | from unity_connection import get_unity_connection, UnityConnection
 12 | import time
 13 | 
 14 | # Configure logging using settings from config
 15 | logging.basicConfig(
 16 |     level=getattr(logging, config.log_level),
 17 |     format=config.log_format,
 18 |     stream=None,  # None -> defaults to sys.stderr; avoid stdout used by MCP stdio
 19 |     force=True    # Ensure our handler replaces any prior stdout handlers
 20 | )
 21 | logger = logging.getLogger("mcp-for-unity-server")
 22 | 
 23 | # Also write logs to a rotating file so logs are available when launched via stdio
 24 | try:
 25 |     import os as _os
 26 |     _log_dir = _os.path.join(_os.path.expanduser(
 27 |         "~/Library/Application Support/UnityMCP"), "Logs")
 28 |     _os.makedirs(_log_dir, exist_ok=True)
 29 |     _file_path = _os.path.join(_log_dir, "unity_mcp_server.log")
 30 |     _fh = RotatingFileHandler(
 31 |         _file_path, maxBytes=512*1024, backupCount=2, encoding="utf-8")
 32 |     _fh.setFormatter(logging.Formatter(config.log_format))
 33 |     _fh.setLevel(getattr(logging, config.log_level))
 34 |     logger.addHandler(_fh)
 35 |     # Also route telemetry logger to the same rotating file and normal level
 36 |     try:
 37 |         tlog = logging.getLogger("unity-mcp-telemetry")
 38 |         tlog.setLevel(getattr(logging, config.log_level))
 39 |         tlog.addHandler(_fh)
 40 |     except Exception:
 41 |         # Never let logging setup break startup
 42 |         pass
 43 | except Exception:
 44 |     # Never let logging setup break startup
 45 |     pass
 46 | # Quieten noisy third-party loggers to avoid clutter during stdio handshake
 47 | for noisy in ("httpx", "urllib3"):
 48 |     try:
 49 |         logging.getLogger(noisy).setLevel(
 50 |             max(logging.WARNING, getattr(logging, config.log_level)))
 51 |     except Exception:
 52 |         pass
 53 | 
 54 | # Import telemetry only after logging is configured to ensure its logs use stderr and proper levels
 55 | # Ensure a slightly higher telemetry timeout unless explicitly overridden by env
 56 | try:
 57 | 
 58 |     # Ensure generous timeout unless explicitly overridden by env
 59 |     if not os.environ.get("UNITY_MCP_TELEMETRY_TIMEOUT"):
 60 |         os.environ["UNITY_MCP_TELEMETRY_TIMEOUT"] = "5.0"
 61 | except Exception:
 62 |     pass
 63 | 
 64 | # Global connection state
 65 | _unity_connection: UnityConnection = None
 66 | 
 67 | 
 68 | @asynccontextmanager
 69 | async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
 70 |     """Handle server startup and shutdown."""
 71 |     global _unity_connection
 72 |     logger.info("MCP for Unity Server starting up")
 73 | 
 74 |     # Record server startup telemetry
 75 |     start_time = time.time()
 76 |     start_clk = time.perf_counter()
 77 |     try:
 78 |         from pathlib import Path
 79 |         ver_path = Path(__file__).parent / "server_version.txt"
 80 |         server_version = ver_path.read_text(encoding="utf-8").strip()
 81 |     except Exception:
 82 |         server_version = "unknown"
 83 |     # Defer initial telemetry by 1s to avoid stdio handshake interference
 84 |     import threading
 85 | 
 86 |     def _emit_startup():
 87 |         try:
 88 |             record_telemetry(RecordType.STARTUP, {
 89 |                 "server_version": server_version,
 90 |                 "startup_time": start_time,
 91 |             })
 92 |             record_milestone(MilestoneType.FIRST_STARTUP)
 93 |         except Exception:
 94 |             logger.debug("Deferred startup telemetry failed", exc_info=True)
 95 |     threading.Timer(1.0, _emit_startup).start()
 96 | 
 97 |     try:
 98 |         skip_connect = os.environ.get(
 99 |             "UNITY_MCP_SKIP_STARTUP_CONNECT", "").lower() in ("1", "true", "yes", "on")
100 |         if skip_connect:
101 |             logger.info(
102 |                 "Skipping Unity connection on startup (UNITY_MCP_SKIP_STARTUP_CONNECT=1)")
103 |         else:
104 |             _unity_connection = get_unity_connection()
105 |             logger.info("Connected to Unity on startup")
106 | 
107 |             # Record successful Unity connection (deferred)
108 |             import threading as _t
109 |             _t.Timer(1.0, lambda: record_telemetry(
110 |                 RecordType.UNITY_CONNECTION,
111 |                 {
112 |                     "status": "connected",
113 |                     "connection_time_ms": (time.perf_counter() - start_clk) * 1000,
114 |                 }
115 |             )).start()
116 | 
117 |     except ConnectionError as e:
118 |         logger.warning("Could not connect to Unity on startup: %s", e)
119 |         _unity_connection = None
120 | 
121 |         # Record connection failure (deferred)
122 |         import threading as _t
123 |         _err_msg = str(e)[:200]
124 |         _t.Timer(1.0, lambda: record_telemetry(
125 |             RecordType.UNITY_CONNECTION,
126 |             {
127 |                 "status": "failed",
128 |                 "error": _err_msg,
129 |                 "connection_time_ms": (time.perf_counter() - start_clk) * 1000,
130 |             }
131 |         )).start()
132 |     except Exception as e:
133 |         logger.warning(
134 |             "Unexpected error connecting to Unity on startup: %s", e)
135 |         _unity_connection = None
136 |         import threading as _t
137 |         _err_msg = str(e)[:200]
138 |         _t.Timer(1.0, lambda: record_telemetry(
139 |             RecordType.UNITY_CONNECTION,
140 |             {
141 |                 "status": "failed",
142 |                 "error": _err_msg,
143 |                 "connection_time_ms": (time.perf_counter() - start_clk) * 1000,
144 |             }
145 |         )).start()
146 | 
147 |     try:
148 |         # Yield the connection object so it can be attached to the context
149 |         # The key 'bridge' matches how tools like read_console expect to access it (ctx.bridge)
150 |         yield {"bridge": _unity_connection}
151 |     finally:
152 |         if _unity_connection:
153 |             _unity_connection.disconnect()
154 |             _unity_connection = None
155 |         logger.info("MCP for Unity Server shut down")
156 | 
157 | # Initialize MCP server
158 | mcp = FastMCP(
159 |     name="mcp-for-unity-server",
160 |     lifespan=server_lifespan
161 | )
162 | 
163 | # Register all tools
164 | register_all_tools(mcp)
165 | 
166 | # Register all resources
167 | register_all_resources(mcp)
168 | 
169 | 
170 | @mcp.prompt()
171 | def asset_creation_strategy() -> str:
172 |     """Guide for discovering and using MCP for Unity tools effectively."""
173 |     return (
174 |         "Available MCP for Unity Server Tools:\n\n"
175 |         "- `manage_editor`: Controls editor state and queries info.\n"
176 |         "- `execute_menu_item`: Executes, lists and checks for the existence of Unity Editor menu items.\n"
177 |         "- `read_console`: Reads or clears Unity console messages, with filtering options.\n"
178 |         "- `manage_scene`: Manages scenes.\n"
179 |         "- `manage_gameobject`: Manages GameObjects in the scene.\n"
180 |         "- `manage_script`: Manages C# script files.\n"
181 |         "- `manage_asset`: Manages prefabs and assets.\n"
182 |         "- `manage_shader`: Manages shaders.\n\n"
183 |         "Tips:\n"
184 |         "- Create prefabs for reusable GameObjects.\n"
185 |         "- Always include a camera and main light in your scenes.\n"
186 |         "- Unless specified otherwise, paths are relative to the project's `Assets/` folder.\n"
187 |         "- After creating or modifying scripts with `manage_script`, allow Unity to recompile; use `read_console` to check for compile errors.\n"
188 |         "- Use `execute_menu_item` for interacting with Unity systems and third party tools like a user would.\n"
189 |     )
190 | 
191 | 
192 | # Run the server
193 | if __name__ == "__main__":
194 |     mcp.run(transport='stdio')
195 | 
```

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

```csharp
  1 | using System;
  2 | using System.Diagnostics;
  3 | using System.IO;
  4 | using System.Runtime.InteropServices;
  5 | using MCPForUnity.Editor.Dependencies.Models;
  6 | using MCPForUnity.Editor.Helpers;
  7 | 
  8 | namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
  9 | {
 10 |     /// <summary>
 11 |     /// Linux-specific dependency detection
 12 |     /// </summary>
 13 |     public class LinuxPlatformDetector : PlatformDetectorBase
 14 |     {
 15 |         public override string PlatformName => "Linux";
 16 | 
 17 |         public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
 18 | 
 19 |         public override DependencyStatus DetectPython()
 20 |         {
 21 |             var status = new DependencyStatus("Python", isRequired: true)
 22 |             {
 23 |                 InstallationHint = GetPythonInstallUrl()
 24 |             };
 25 | 
 26 |             try
 27 |             {
 28 |                 // Check common Python installation paths on Linux
 29 |                 var candidates = new[]
 30 |                 {
 31 |                     "python3",
 32 |                     "python",
 33 |                     "/usr/bin/python3",
 34 |                     "/usr/local/bin/python3",
 35 |                     "/opt/python/bin/python3",
 36 |                     "/snap/bin/python3"
 37 |                 };
 38 | 
 39 |                 foreach (var candidate in candidates)
 40 |                 {
 41 |                     if (TryValidatePython(candidate, out string version, out string fullPath))
 42 |                     {
 43 |                         status.IsAvailable = true;
 44 |                         status.Version = version;
 45 |                         status.Path = fullPath;
 46 |                         status.Details = $"Found Python {version} at {fullPath}";
 47 |                         return status;
 48 |                     }
 49 |                 }
 50 | 
 51 |                 // Try PATH resolution using 'which' command
 52 |                 if (TryFindInPath("python3", out string pathResult) ||
 53 |                     TryFindInPath("python", out pathResult))
 54 |                 {
 55 |                     if (TryValidatePython(pathResult, out string version, out string fullPath))
 56 |                     {
 57 |                         status.IsAvailable = true;
 58 |                         status.Version = version;
 59 |                         status.Path = fullPath;
 60 |                         status.Details = $"Found Python {version} in PATH at {fullPath}";
 61 |                         return status;
 62 |                     }
 63 |                 }
 64 | 
 65 |                 status.ErrorMessage = "Python not found. Please install Python 3.10 or later.";
 66 |                 status.Details = "Checked common installation paths including system, snap, and user-local locations.";
 67 |             }
 68 |             catch (Exception ex)
 69 |             {
 70 |                 status.ErrorMessage = $"Error detecting Python: {ex.Message}";
 71 |             }
 72 | 
 73 |             return status;
 74 |         }
 75 | 
 76 |         public override string GetPythonInstallUrl()
 77 |         {
 78 |             return "https://www.python.org/downloads/source/";
 79 |         }
 80 | 
 81 |         public override string GetUVInstallUrl()
 82 |         {
 83 |             return "https://docs.astral.sh/uv/getting-started/installation/#linux";
 84 |         }
 85 | 
 86 |         public override string GetInstallationRecommendations()
 87 |         {
 88 |             return @"Linux Installation Recommendations:
 89 | 
 90 | 1. Python: Install via package manager or pyenv
 91 |    - Ubuntu/Debian: sudo apt install python3 python3-pip
 92 |    - Fedora/RHEL: sudo dnf install python3 python3-pip
 93 |    - Arch: sudo pacman -S python python-pip
 94 |    - Or use pyenv: https://github.com/pyenv/pyenv
 95 | 
 96 | 2. UV Package Manager: Install via curl
 97 |    - Run: curl -LsSf https://astral.sh/uv/install.sh | sh
 98 |    - Or download from: https://github.com/astral-sh/uv/releases
 99 | 
100 | 3. MCP Server: Will be installed automatically by MCP for Unity
101 | 
102 | Note: Make sure ~/.local/bin is in your PATH for user-local installations.";
103 |         }
104 | 
105 |         private bool TryValidatePython(string pythonPath, out string version, out string fullPath)
106 |         {
107 |             version = null;
108 |             fullPath = null;
109 | 
110 |             try
111 |             {
112 |                 var psi = new ProcessStartInfo
113 |                 {
114 |                     FileName = pythonPath,
115 |                     Arguments = "--version",
116 |                     UseShellExecute = false,
117 |                     RedirectStandardOutput = true,
118 |                     RedirectStandardError = true,
119 |                     CreateNoWindow = true
120 |                 };
121 | 
122 |                 // Set PATH to include common locations
123 |                 var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
124 |                 var pathAdditions = new[]
125 |                 {
126 |                     "/usr/local/bin",
127 |                     "/usr/bin",
128 |                     "/bin",
129 |                     "/snap/bin",
130 |                     Path.Combine(homeDir, ".local", "bin")
131 |                 };
132 | 
133 |                 string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
134 |                 psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath;
135 | 
136 |                 using var process = Process.Start(psi);
137 |                 if (process == null) return false;
138 | 
139 |                 string output = process.StandardOutput.ReadToEnd().Trim();
140 |                 process.WaitForExit(5000);
141 | 
142 |                 if (process.ExitCode == 0 && output.StartsWith("Python "))
143 |                 {
144 |                     version = output.Substring(7); // Remove "Python " prefix
145 |                     fullPath = pythonPath;
146 | 
147 |                     // Validate minimum version (Python 4+ or Python 3.10+)
148 |                     if (TryParseVersion(version, out var major, out var minor))
149 |                     {
150 |                         return major > 3 || (major >= 3 && minor >= 10);
151 |                     }
152 |                 }
153 |             }
154 |             catch
155 |             {
156 |                 // Ignore validation errors
157 |             }
158 | 
159 |             return false;
160 |         }
161 | 
162 |         private bool TryFindInPath(string executable, out string fullPath)
163 |         {
164 |             fullPath = null;
165 | 
166 |             try
167 |             {
168 |                 var psi = new ProcessStartInfo
169 |                 {
170 |                     FileName = "/usr/bin/which",
171 |                     Arguments = executable,
172 |                     UseShellExecute = false,
173 |                     RedirectStandardOutput = true,
174 |                     RedirectStandardError = true,
175 |                     CreateNoWindow = true
176 |                 };
177 | 
178 |                 // Enhance PATH for Unity's GUI environment
179 |                 var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
180 |                 var pathAdditions = new[]
181 |                 {
182 |                     "/usr/local/bin",
183 |                     "/usr/bin",
184 |                     "/bin",
185 |                     "/snap/bin",
186 |                     Path.Combine(homeDir, ".local", "bin")
187 |                 };
188 | 
189 |                 string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
190 |                 psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath;
191 | 
192 |                 using var process = Process.Start(psi);
193 |                 if (process == null) return false;
194 | 
195 |                 string output = process.StandardOutput.ReadToEnd().Trim();
196 |                 process.WaitForExit(3000);
197 | 
198 |                 if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
199 |                 {
200 |                     fullPath = output;
201 |                     return true;
202 |                 }
203 |             }
204 |             catch
205 |             {
206 |                 // Ignore errors
207 |             }
208 | 
209 |             return false;
210 |         }
211 |     }
212 | }
213 | 
```

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

```csharp
  1 | using System;
  2 | using System.Diagnostics;
  3 | using System.IO;
  4 | using System.Runtime.InteropServices;
  5 | using MCPForUnity.Editor.Dependencies.Models;
  6 | using MCPForUnity.Editor.Helpers;
  7 | 
  8 | namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
  9 | {
 10 |     /// <summary>
 11 |     /// Linux-specific dependency detection
 12 |     /// </summary>
 13 |     public class LinuxPlatformDetector : PlatformDetectorBase
 14 |     {
 15 |         public override string PlatformName => "Linux";
 16 | 
 17 |         public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
 18 | 
 19 |         public override DependencyStatus DetectPython()
 20 |         {
 21 |             var status = new DependencyStatus("Python", isRequired: true)
 22 |             {
 23 |                 InstallationHint = GetPythonInstallUrl()
 24 |             };
 25 | 
 26 |             try
 27 |             {
 28 |                 // Check common Python installation paths on Linux
 29 |                 var candidates = new[]
 30 |                 {
 31 |                     "python3",
 32 |                     "python",
 33 |                     "/usr/bin/python3",
 34 |                     "/usr/local/bin/python3",
 35 |                     "/opt/python/bin/python3",
 36 |                     "/snap/bin/python3"
 37 |                 };
 38 | 
 39 |                 foreach (var candidate in candidates)
 40 |                 {
 41 |                     if (TryValidatePython(candidate, out string version, out string fullPath))
 42 |                     {
 43 |                         status.IsAvailable = true;
 44 |                         status.Version = version;
 45 |                         status.Path = fullPath;
 46 |                         status.Details = $"Found Python {version} at {fullPath}";
 47 |                         return status;
 48 |                     }
 49 |                 }
 50 | 
 51 |                 // Try PATH resolution using 'which' command
 52 |                 if (TryFindInPath("python3", out string pathResult) ||
 53 |                     TryFindInPath("python", out pathResult))
 54 |                 {
 55 |                     if (TryValidatePython(pathResult, out string version, out string fullPath))
 56 |                     {
 57 |                         status.IsAvailable = true;
 58 |                         status.Version = version;
 59 |                         status.Path = fullPath;
 60 |                         status.Details = $"Found Python {version} in PATH at {fullPath}";
 61 |                         return status;
 62 |                     }
 63 |                 }
 64 | 
 65 |                 status.ErrorMessage = "Python not found. Please install Python 3.10 or later.";
 66 |                 status.Details = "Checked common installation paths including system, snap, and user-local locations.";
 67 |             }
 68 |             catch (Exception ex)
 69 |             {
 70 |                 status.ErrorMessage = $"Error detecting Python: {ex.Message}";
 71 |             }
 72 | 
 73 |             return status;
 74 |         }
 75 | 
 76 |         public override string GetPythonInstallUrl()
 77 |         {
 78 |             return "https://www.python.org/downloads/source/";
 79 |         }
 80 | 
 81 |         public override string GetUVInstallUrl()
 82 |         {
 83 |             return "https://docs.astral.sh/uv/getting-started/installation/#linux";
 84 |         }
 85 | 
 86 |         public override string GetInstallationRecommendations()
 87 |         {
 88 |             return @"Linux Installation Recommendations:
 89 | 
 90 | 1. Python: Install via package manager or pyenv
 91 |    - Ubuntu/Debian: sudo apt install python3 python3-pip
 92 |    - Fedora/RHEL: sudo dnf install python3 python3-pip
 93 |    - Arch: sudo pacman -S python python-pip
 94 |    - Or use pyenv: https://github.com/pyenv/pyenv
 95 | 
 96 | 2. UV Package Manager: Install via curl
 97 |    - Run: curl -LsSf https://astral.sh/uv/install.sh | sh
 98 |    - Or download from: https://github.com/astral-sh/uv/releases
 99 | 
100 | 3. MCP Server: Will be installed automatically by Unity MCP Bridge
101 | 
102 | Note: Make sure ~/.local/bin is in your PATH for user-local installations.";
103 |         }
104 | 
105 |         private bool TryValidatePython(string pythonPath, out string version, out string fullPath)
106 |         {
107 |             version = null;
108 |             fullPath = null;
109 | 
110 |             try
111 |             {
112 |                 var psi = new ProcessStartInfo
113 |                 {
114 |                     FileName = pythonPath,
115 |                     Arguments = "--version",
116 |                     UseShellExecute = false,
117 |                     RedirectStandardOutput = true,
118 |                     RedirectStandardError = true,
119 |                     CreateNoWindow = true
120 |                 };
121 | 
122 |                 // Set PATH to include common locations
123 |                 var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
124 |                 var pathAdditions = new[]
125 |                 {
126 |                     "/usr/local/bin",
127 |                     "/usr/bin",
128 |                     "/bin",
129 |                     "/snap/bin",
130 |                     Path.Combine(homeDir, ".local", "bin")
131 |                 };
132 | 
133 |                 string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
134 |                 psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath;
135 | 
136 |                 using var process = Process.Start(psi);
137 |                 if (process == null) return false;
138 | 
139 |                 string output = process.StandardOutput.ReadToEnd().Trim();
140 |                 process.WaitForExit(5000);
141 | 
142 |                 if (process.ExitCode == 0 && output.StartsWith("Python "))
143 |                 {
144 |                     version = output.Substring(7); // Remove "Python " prefix
145 |                     fullPath = pythonPath;
146 | 
147 |                     // Validate minimum version (Python 4+ or Python 3.10+)
148 |                     if (TryParseVersion(version, out var major, out var minor))
149 |                     {
150 |                         return major > 3 || (major >= 3 && minor >= 10);
151 |                     }
152 |                 }
153 |             }
154 |             catch
155 |             {
156 |                 // Ignore validation errors
157 |             }
158 | 
159 |             return false;
160 |         }
161 | 
162 |         private bool TryFindInPath(string executable, out string fullPath)
163 |         {
164 |             fullPath = null;
165 | 
166 |             try
167 |             {
168 |                 var psi = new ProcessStartInfo
169 |                 {
170 |                     FileName = "/usr/bin/which",
171 |                     Arguments = executable,
172 |                     UseShellExecute = false,
173 |                     RedirectStandardOutput = true,
174 |                     RedirectStandardError = true,
175 |                     CreateNoWindow = true
176 |                 };
177 | 
178 |                 // Enhance PATH for Unity's GUI environment
179 |                 var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
180 |                 var pathAdditions = new[]
181 |                 {
182 |                     "/usr/local/bin",
183 |                     "/usr/bin",
184 |                     "/bin",
185 |                     "/snap/bin",
186 |                     Path.Combine(homeDir, ".local", "bin")
187 |                 };
188 | 
189 |                 string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
190 |                 psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath;
191 | 
192 |                 using var process = Process.Start(psi);
193 |                 if (process == null) return false;
194 | 
195 |                 string output = process.StandardOutput.ReadToEnd().Trim();
196 |                 process.WaitForExit(3000);
197 | 
198 |                 if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
199 |                 {
200 |                     fullPath = output;
201 |                     return true;
202 |                 }
203 |             }
204 |             catch
205 |             {
206 |                 // Ignore errors
207 |             }
208 | 
209 |             return false;
210 |         }
211 |     }
212 | }
213 | 
```

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

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.Threading;
  4 | using UnityEngine;
  5 | 
  6 | namespace MCPForUnity.Editor.Helpers
  7 | {
  8 |     /// <summary>
  9 |     /// Unity Bridge telemetry helper for collecting usage analytics
 10 |     /// Following privacy-first approach with easy opt-out mechanisms
 11 |     /// </summary>
 12 |     public static class TelemetryHelper
 13 |     {
 14 |         private const string TELEMETRY_DISABLED_KEY = "MCPForUnity.TelemetryDisabled";
 15 |         private const string CUSTOMER_UUID_KEY = "MCPForUnity.CustomerUUID";
 16 |         private static Action<Dictionary<string, object>> s_sender;
 17 | 
 18 |         /// <summary>
 19 |         /// Check if telemetry is enabled (can be disabled via Environment Variable or EditorPrefs)
 20 |         /// </summary>
 21 |         public static bool IsEnabled
 22 |         {
 23 |             get
 24 |             {
 25 |                 // Check environment variables first
 26 |                 var envDisable = Environment.GetEnvironmentVariable("DISABLE_TELEMETRY");
 27 |                 if (!string.IsNullOrEmpty(envDisable) &&
 28 |                     (envDisable.ToLower() == "true" || envDisable == "1"))
 29 |                 {
 30 |                     return false;
 31 |                 }
 32 | 
 33 |                 var unityMcpDisable = Environment.GetEnvironmentVariable("UNITY_MCP_DISABLE_TELEMETRY");
 34 |                 if (!string.IsNullOrEmpty(unityMcpDisable) &&
 35 |                     (unityMcpDisable.ToLower() == "true" || unityMcpDisable == "1"))
 36 |                 {
 37 |                     return false;
 38 |                 }
 39 | 
 40 |                 // Honor protocol-wide opt-out as well
 41 |                 var mcpDisable = Environment.GetEnvironmentVariable("MCP_DISABLE_TELEMETRY");
 42 |                 if (!string.IsNullOrEmpty(mcpDisable) &&
 43 |                     (mcpDisable.Equals("true", StringComparison.OrdinalIgnoreCase) || mcpDisable == "1"))
 44 |                 {
 45 |                     return false;
 46 |                 }
 47 | 
 48 |                 // Check EditorPrefs
 49 |                 return !UnityEditor.EditorPrefs.GetBool(TELEMETRY_DISABLED_KEY, false);
 50 |             }
 51 |         }
 52 | 
 53 |         /// <summary>
 54 |         /// Get or generate customer UUID for anonymous tracking
 55 |         /// </summary>
 56 |         public static string GetCustomerUUID()
 57 |         {
 58 |             var uuid = UnityEditor.EditorPrefs.GetString(CUSTOMER_UUID_KEY, "");
 59 |             if (string.IsNullOrEmpty(uuid))
 60 |             {
 61 |                 uuid = System.Guid.NewGuid().ToString();
 62 |                 UnityEditor.EditorPrefs.SetString(CUSTOMER_UUID_KEY, uuid);
 63 |             }
 64 |             return uuid;
 65 |         }
 66 | 
 67 |         /// <summary>
 68 |         /// Disable telemetry (stored in EditorPrefs)
 69 |         /// </summary>
 70 |         public static void DisableTelemetry()
 71 |         {
 72 |             UnityEditor.EditorPrefs.SetBool(TELEMETRY_DISABLED_KEY, true);
 73 |         }
 74 | 
 75 |         /// <summary>
 76 |         /// Enable telemetry (stored in EditorPrefs)
 77 |         /// </summary>
 78 |         public static void EnableTelemetry()
 79 |         {
 80 |             UnityEditor.EditorPrefs.SetBool(TELEMETRY_DISABLED_KEY, false);
 81 |         }
 82 | 
 83 |         /// <summary>
 84 |         /// Send telemetry data to MCP server for processing
 85 |         /// This is a lightweight bridge - the actual telemetry logic is in the MCP server
 86 |         /// </summary>
 87 |         public static void RecordEvent(string eventType, Dictionary<string, object> data = null)
 88 |         {
 89 |             if (!IsEnabled)
 90 |                 return;
 91 | 
 92 |             try
 93 |             {
 94 |                 var telemetryData = new Dictionary<string, object>
 95 |                 {
 96 |                     ["event_type"] = eventType,
 97 |                     ["timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
 98 |                     ["customer_uuid"] = GetCustomerUUID(),
 99 |                     ["unity_version"] = Application.unityVersion,
100 |                     ["platform"] = Application.platform.ToString(),
101 |                     ["source"] = "unity_bridge"
102 |                 };
103 | 
104 |                 if (data != null)
105 |                 {
106 |                     telemetryData["data"] = data;
107 |                 }
108 | 
109 |                 // Send to MCP server via existing bridge communication
110 |                 // The MCP server will handle actual telemetry transmission
111 |                 SendTelemetryToMcpServer(telemetryData);
112 |             }
113 |             catch (Exception e)
114 |             {
115 |                 // Never let telemetry errors interfere with functionality
116 |                 if (IsDebugEnabled())
117 |                 {
118 |                     McpLog.Warn($"Telemetry error (non-blocking): {e.Message}");
119 |                 }
120 |             }
121 |         }
122 | 
123 |         /// <summary>
124 |         /// Allows the bridge to register a concrete sender for telemetry payloads.
125 |         /// </summary>
126 |         public static void RegisterTelemetrySender(Action<Dictionary<string, object>> sender)
127 |         {
128 |             Interlocked.Exchange(ref s_sender, sender);
129 |         }
130 | 
131 |         public static void UnregisterTelemetrySender()
132 |         {
133 |             Interlocked.Exchange(ref s_sender, null);
134 |         }
135 | 
136 |         /// <summary>
137 |         /// Record bridge startup event
138 |         /// </summary>
139 |         public static void RecordBridgeStartup()
140 |         {
141 |             RecordEvent("bridge_startup", new Dictionary<string, object>
142 |             {
143 |                 ["bridge_version"] = "3.0.2",
144 |                 ["auto_connect"] = MCPForUnityBridge.IsAutoConnectMode()
145 |             });
146 |         }
147 | 
148 |         /// <summary>
149 |         /// Record bridge connection event
150 |         /// </summary>
151 |         public static void RecordBridgeConnection(bool success, string error = null)
152 |         {
153 |             var data = new Dictionary<string, object>
154 |             {
155 |                 ["success"] = success
156 |             };
157 | 
158 |             if (!string.IsNullOrEmpty(error))
159 |             {
160 |                 data["error"] = error.Substring(0, Math.Min(200, error.Length));
161 |             }
162 | 
163 |             RecordEvent("bridge_connection", data);
164 |         }
165 | 
166 |         /// <summary>
167 |         /// Record tool execution from Unity side
168 |         /// </summary>
169 |         public static void RecordToolExecution(string toolName, bool success, float durationMs, string error = null)
170 |         {
171 |             var data = new Dictionary<string, object>
172 |             {
173 |                 ["tool_name"] = toolName,
174 |                 ["success"] = success,
175 |                 ["duration_ms"] = Math.Round(durationMs, 2)
176 |             };
177 | 
178 |             if (!string.IsNullOrEmpty(error))
179 |             {
180 |                 data["error"] = error.Substring(0, Math.Min(200, error.Length));
181 |             }
182 | 
183 |             RecordEvent("tool_execution_unity", data);
184 |         }
185 | 
186 |         private static void SendTelemetryToMcpServer(Dictionary<string, object> telemetryData)
187 |         {
188 |             var sender = Volatile.Read(ref s_sender);
189 |             if (sender != null)
190 |             {
191 |                 try
192 |                 {
193 |                     sender(telemetryData);
194 |                     return;
195 |                 }
196 |                 catch (Exception e)
197 |                 {
198 |                     if (IsDebugEnabled())
199 |                     {
200 |                         McpLog.Warn($"Telemetry sender error (non-blocking): {e.Message}");
201 |                     }
202 |                 }
203 |             }
204 | 
205 |             // Fallback: log when debug is enabled
206 |             if (IsDebugEnabled())
207 |             {
208 |                 McpLog.Info($"Telemetry: {telemetryData["event_type"]}");
209 |             }
210 |         }
211 | 
212 |         private static bool IsDebugEnabled()
213 |         {
214 |             try
215 |             {
216 |                 return UnityEditor.EditorPrefs.GetBool("MCPForUnity.DebugLogs", false);
217 |             }
218 |             catch
219 |             {
220 |                 return false;
221 |             }
222 |         }
223 |     }
224 | }
225 | 
```

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

```python
  1 | from telemetry import record_telemetry, record_milestone, RecordType, MilestoneType
  2 | from mcp.server.fastmcp import FastMCP
  3 | import logging
  4 | from logging.handlers import RotatingFileHandler
  5 | import os
  6 | from contextlib import asynccontextmanager
  7 | from typing import AsyncIterator, Dict, Any
  8 | from config import config
  9 | from tools import register_all_tools
 10 | from unity_connection import get_unity_connection, UnityConnection
 11 | import time
 12 | 
 13 | # Configure logging using settings from config
 14 | logging.basicConfig(
 15 |     level=getattr(logging, config.log_level),
 16 |     format=config.log_format,
 17 |     stream=None,  # None -> defaults to sys.stderr; avoid stdout used by MCP stdio
 18 |     force=True    # Ensure our handler replaces any prior stdout handlers
 19 | )
 20 | logger = logging.getLogger("mcp-for-unity-server")
 21 | 
 22 | # Also write logs to a rotating file so logs are available when launched via stdio
 23 | try:
 24 |     import os as _os
 25 |     _log_dir = _os.path.join(_os.path.expanduser(
 26 |         "~/Library/Application Support/UnityMCP"), "Logs")
 27 |     _os.makedirs(_log_dir, exist_ok=True)
 28 |     _file_path = _os.path.join(_log_dir, "unity_mcp_server.log")
 29 |     _fh = RotatingFileHandler(
 30 |         _file_path, maxBytes=512*1024, backupCount=2, encoding="utf-8")
 31 |     _fh.setFormatter(logging.Formatter(config.log_format))
 32 |     _fh.setLevel(getattr(logging, config.log_level))
 33 |     logger.addHandler(_fh)
 34 |     # Also route telemetry logger to the same rotating file and normal level
 35 |     try:
 36 |         tlog = logging.getLogger("unity-mcp-telemetry")
 37 |         tlog.setLevel(getattr(logging, config.log_level))
 38 |         tlog.addHandler(_fh)
 39 |     except Exception:
 40 |         # Never let logging setup break startup
 41 |         pass
 42 | except Exception:
 43 |     # Never let logging setup break startup
 44 |     pass
 45 | # Quieten noisy third-party loggers to avoid clutter during stdio handshake
 46 | for noisy in ("httpx", "urllib3"):
 47 |     try:
 48 |         logging.getLogger(noisy).setLevel(
 49 |             max(logging.WARNING, getattr(logging, config.log_level)))
 50 |     except Exception:
 51 |         pass
 52 | 
 53 | # Import telemetry only after logging is configured to ensure its logs use stderr and proper levels
 54 | # Ensure a slightly higher telemetry timeout unless explicitly overridden by env
 55 | try:
 56 | 
 57 |     # Ensure generous timeout unless explicitly overridden by env
 58 |     if not os.environ.get("UNITY_MCP_TELEMETRY_TIMEOUT"):
 59 |         os.environ["UNITY_MCP_TELEMETRY_TIMEOUT"] = "5.0"
 60 | except Exception:
 61 |     pass
 62 | 
 63 | # Global connection state
 64 | _unity_connection: UnityConnection = None
 65 | 
 66 | 
 67 | @asynccontextmanager
 68 | async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
 69 |     """Handle server startup and shutdown."""
 70 |     global _unity_connection
 71 |     logger.info("MCP for Unity Server starting up")
 72 | 
 73 |     # Record server startup telemetry
 74 |     start_time = time.time()
 75 |     start_clk = time.perf_counter()
 76 |     try:
 77 |         from pathlib import Path
 78 |         ver_path = Path(__file__).parent / "server_version.txt"
 79 |         server_version = ver_path.read_text(encoding="utf-8").strip()
 80 |     except Exception:
 81 |         server_version = "unknown"
 82 |     # Defer initial telemetry by 1s to avoid stdio handshake interference
 83 |     import threading
 84 | 
 85 |     def _emit_startup():
 86 |         try:
 87 |             record_telemetry(RecordType.STARTUP, {
 88 |                 "server_version": server_version,
 89 |                 "startup_time": start_time,
 90 |             })
 91 |             record_milestone(MilestoneType.FIRST_STARTUP)
 92 |         except Exception:
 93 |             logger.debug("Deferred startup telemetry failed", exc_info=True)
 94 |     threading.Timer(1.0, _emit_startup).start()
 95 | 
 96 |     try:
 97 |         skip_connect = os.environ.get(
 98 |             "UNITY_MCP_SKIP_STARTUP_CONNECT", "").lower() in ("1", "true", "yes", "on")
 99 |         if skip_connect:
100 |             logger.info(
101 |                 "Skipping Unity connection on startup (UNITY_MCP_SKIP_STARTUP_CONNECT=1)")
102 |         else:
103 |             _unity_connection = get_unity_connection()
104 |             logger.info("Connected to Unity on startup")
105 | 
106 |             # Record successful Unity connection (deferred)
107 |             import threading as _t
108 |             _t.Timer(1.0, lambda: record_telemetry(
109 |                 RecordType.UNITY_CONNECTION,
110 |                 {
111 |                     "status": "connected",
112 |                     "connection_time_ms": (time.perf_counter() - start_clk) * 1000,
113 |                 }
114 |             )).start()
115 | 
116 |     except ConnectionError as e:
117 |         logger.warning("Could not connect to Unity on startup: %s", e)
118 |         _unity_connection = None
119 | 
120 |         # Record connection failure (deferred)
121 |         import threading as _t
122 |         _err_msg = str(e)[:200]
123 |         _t.Timer(1.0, lambda: record_telemetry(
124 |             RecordType.UNITY_CONNECTION,
125 |             {
126 |                 "status": "failed",
127 |                 "error": _err_msg,
128 |                 "connection_time_ms": (time.perf_counter() - start_clk) * 1000,
129 |             }
130 |         )).start()
131 |     except Exception as e:
132 |         logger.warning(
133 |             "Unexpected error connecting to Unity on startup: %s", e)
134 |         _unity_connection = None
135 |         import threading as _t
136 |         _err_msg = str(e)[:200]
137 |         _t.Timer(1.0, lambda: record_telemetry(
138 |             RecordType.UNITY_CONNECTION,
139 |             {
140 |                 "status": "failed",
141 |                 "error": _err_msg,
142 |                 "connection_time_ms": (time.perf_counter() - start_clk) * 1000,
143 |             }
144 |         )).start()
145 | 
146 |     try:
147 |         # Yield the connection object so it can be attached to the context
148 |         # The key 'bridge' matches how tools like read_console expect to access it (ctx.bridge)
149 |         yield {"bridge": _unity_connection}
150 |     finally:
151 |         if _unity_connection:
152 |             _unity_connection.disconnect()
153 |             _unity_connection = None
154 |         logger.info("MCP for Unity Server shut down")
155 | 
156 | # Initialize MCP server
157 | mcp = FastMCP(
158 |     name="mcp-for-unity-server",
159 |     lifespan=server_lifespan
160 | )
161 | 
162 | # Register all tools
163 | register_all_tools(mcp)
164 | 
165 | # Asset Creation Strategy
166 | 
167 | 
168 | @mcp.prompt()
169 | def asset_creation_strategy() -> str:
170 |     """Guide for discovering and using MCP for Unity tools effectively."""
171 |     return (
172 |         "Available MCP for Unity Server Tools:\n\n"
173 |         "- `manage_editor`: Controls editor state and queries info.\n"
174 |         "- `manage_menu_item`: Executes, lists and checks for the existence of Unity Editor menu items.\n"
175 |         "- `read_console`: Reads or clears Unity console messages, with filtering options.\n"
176 |         "- `manage_scene`: Manages scenes.\n"
177 |         "- `manage_gameobject`: Manages GameObjects in the scene.\n"
178 |         "- `manage_script`: Manages C# script files.\n"
179 |         "- `manage_asset`: Manages prefabs and assets.\n"
180 |         "- `manage_shader`: Manages shaders.\n\n"
181 |         "Tips:\n"
182 |         "- Create prefabs for reusable GameObjects.\n"
183 |         "- Always include a camera and main light in your scenes.\n"
184 |         "- Unless specified otherwise, paths are relative to the project's `Assets/` folder.\n"
185 |         "- After creating or modifying scripts with `manage_script`, allow Unity to recompile; use `read_console` to check for compile errors.\n"
186 |         "- Use `manage_menu_item` for interacting with Unity systems and third party tools like a user would.\n"
187 |         "- List menu items before using them if you are unsure of the menu path.\n"
188 |         "- If a menu item seems missing, refresh the cache: use manage_menu_item with action='list' and refresh=true, or action='refresh'. Avoid refreshing every time; prefer refresh only when the menu set likely changed.\n"
189 |     )
190 | 
191 | 
192 | # Run the server
193 | if __name__ == "__main__":
194 |     mcp.run(transport='stdio')
195 | 
```

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

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.Threading;
  4 | using UnityEngine;
  5 | 
  6 | namespace MCPForUnity.Editor.Helpers
  7 | {
  8 |     /// <summary>
  9 |     /// Unity Bridge telemetry helper for collecting usage analytics
 10 |     /// Following privacy-first approach with easy opt-out mechanisms
 11 |     /// </summary>
 12 |     public static class TelemetryHelper
 13 |     {
 14 |         private const string TELEMETRY_DISABLED_KEY = "MCPForUnity.TelemetryDisabled";
 15 |         private const string CUSTOMER_UUID_KEY = "MCPForUnity.CustomerUUID";
 16 |         private static Action<Dictionary<string, object>> s_sender;
 17 | 
 18 |         /// <summary>
 19 |         /// Check if telemetry is enabled (can be disabled via Environment Variable or EditorPrefs)
 20 |         /// </summary>
 21 |         public static bool IsEnabled
 22 |         {
 23 |             get
 24 |             {
 25 |                 // Check environment variables first
 26 |                 var envDisable = Environment.GetEnvironmentVariable("DISABLE_TELEMETRY");
 27 |                 if (!string.IsNullOrEmpty(envDisable) &&
 28 |                     (envDisable.ToLower() == "true" || envDisable == "1"))
 29 |                 {
 30 |                     return false;
 31 |                 }
 32 | 
 33 |                 var unityMcpDisable = Environment.GetEnvironmentVariable("UNITY_MCP_DISABLE_TELEMETRY");
 34 |                 if (!string.IsNullOrEmpty(unityMcpDisable) &&
 35 |                     (unityMcpDisable.ToLower() == "true" || unityMcpDisable == "1"))
 36 |                 {
 37 |                     return false;
 38 |                 }
 39 | 
 40 |                 // Honor protocol-wide opt-out as well
 41 |                 var mcpDisable = Environment.GetEnvironmentVariable("MCP_DISABLE_TELEMETRY");
 42 |                 if (!string.IsNullOrEmpty(mcpDisable) &&
 43 |                     (mcpDisable.Equals("true", StringComparison.OrdinalIgnoreCase) || mcpDisable == "1"))
 44 |                 {
 45 |                     return false;
 46 |                 }
 47 | 
 48 |                 // Check EditorPrefs
 49 |                 return !UnityEditor.EditorPrefs.GetBool(TELEMETRY_DISABLED_KEY, false);
 50 |             }
 51 |         }
 52 | 
 53 |         /// <summary>
 54 |         /// Get or generate customer UUID for anonymous tracking
 55 |         /// </summary>
 56 |         public static string GetCustomerUUID()
 57 |         {
 58 |             var uuid = UnityEditor.EditorPrefs.GetString(CUSTOMER_UUID_KEY, "");
 59 |             if (string.IsNullOrEmpty(uuid))
 60 |             {
 61 |                 uuid = System.Guid.NewGuid().ToString();
 62 |                 UnityEditor.EditorPrefs.SetString(CUSTOMER_UUID_KEY, uuid);
 63 |             }
 64 |             return uuid;
 65 |         }
 66 | 
 67 |         /// <summary>
 68 |         /// Disable telemetry (stored in EditorPrefs)
 69 |         /// </summary>
 70 |         public static void DisableTelemetry()
 71 |         {
 72 |             UnityEditor.EditorPrefs.SetBool(TELEMETRY_DISABLED_KEY, true);
 73 |         }
 74 | 
 75 |         /// <summary>
 76 |         /// Enable telemetry (stored in EditorPrefs)
 77 |         /// </summary>
 78 |         public static void EnableTelemetry()
 79 |         {
 80 |             UnityEditor.EditorPrefs.SetBool(TELEMETRY_DISABLED_KEY, false);
 81 |         }
 82 | 
 83 |         /// <summary>
 84 |         /// Send telemetry data to Python server for processing
 85 |         /// This is a lightweight bridge - the actual telemetry logic is in Python
 86 |         /// </summary>
 87 |         public static void RecordEvent(string eventType, Dictionary<string, object> data = null)
 88 |         {
 89 |             if (!IsEnabled)
 90 |                 return;
 91 | 
 92 |             try
 93 |             {
 94 |                 var telemetryData = new Dictionary<string, object>
 95 |                 {
 96 |                     ["event_type"] = eventType,
 97 |                     ["timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
 98 |                     ["customer_uuid"] = GetCustomerUUID(),
 99 |                     ["unity_version"] = Application.unityVersion,
100 |                     ["platform"] = Application.platform.ToString(),
101 |                     ["source"] = "unity_bridge"
102 |                 };
103 | 
104 |                 if (data != null)
105 |                 {
106 |                     telemetryData["data"] = data;
107 |                 }
108 | 
109 |                 // Send to Python server via existing bridge communication
110 |                 // The Python server will handle actual telemetry transmission
111 |                 SendTelemetryToPythonServer(telemetryData);
112 |             }
113 |             catch (Exception e)
114 |             {
115 |                 // Never let telemetry errors interfere with functionality
116 |                 if (IsDebugEnabled())
117 |                 {
118 |                     Debug.LogWarning($"Telemetry error (non-blocking): {e.Message}");
119 |                 }
120 |             }
121 |         }
122 | 
123 |         /// <summary>
124 |         /// Allows the bridge to register a concrete sender for telemetry payloads.
125 |         /// </summary>
126 |         public static void RegisterTelemetrySender(Action<Dictionary<string, object>> sender)
127 |         {
128 |             Interlocked.Exchange(ref s_sender, sender);
129 |         }
130 | 
131 |         public static void UnregisterTelemetrySender()
132 |         {
133 |             Interlocked.Exchange(ref s_sender, null);
134 |         }
135 | 
136 |         /// <summary>
137 |         /// Record bridge startup event
138 |         /// </summary>
139 |         public static void RecordBridgeStartup()
140 |         {
141 |             RecordEvent("bridge_startup", new Dictionary<string, object>
142 |             {
143 |                 ["bridge_version"] = "3.0.2",
144 |                 ["auto_connect"] = MCPForUnityBridge.IsAutoConnectMode()
145 |             });
146 |         }
147 | 
148 |         /// <summary>
149 |         /// Record bridge connection event
150 |         /// </summary>
151 |         public static void RecordBridgeConnection(bool success, string error = null)
152 |         {
153 |             var data = new Dictionary<string, object>
154 |             {
155 |                 ["success"] = success
156 |             };
157 | 
158 |             if (!string.IsNullOrEmpty(error))
159 |             {
160 |                 data["error"] = error.Substring(0, Math.Min(200, error.Length));
161 |             }
162 | 
163 |             RecordEvent("bridge_connection", data);
164 |         }
165 | 
166 |         /// <summary>
167 |         /// Record tool execution from Unity side
168 |         /// </summary>
169 |         public static void RecordToolExecution(string toolName, bool success, float durationMs, string error = null)
170 |         {
171 |             var data = new Dictionary<string, object>
172 |             {
173 |                 ["tool_name"] = toolName,
174 |                 ["success"] = success,
175 |                 ["duration_ms"] = Math.Round(durationMs, 2)
176 |             };
177 | 
178 |             if (!string.IsNullOrEmpty(error))
179 |             {
180 |                 data["error"] = error.Substring(0, Math.Min(200, error.Length));
181 |             }
182 | 
183 |             RecordEvent("tool_execution_unity", data);
184 |         }
185 | 
186 |         private static void SendTelemetryToPythonServer(Dictionary<string, object> telemetryData)
187 |         {
188 |             var sender = Volatile.Read(ref s_sender);
189 |             if (sender != null)
190 |             {
191 |                 try
192 |                 {
193 |                     sender(telemetryData);
194 |                     return;
195 |                 }
196 |                 catch (Exception e)
197 |                 {
198 |                     if (IsDebugEnabled())
199 |                     {
200 |                         Debug.LogWarning($"Telemetry sender error (non-blocking): {e.Message}");
201 |                     }
202 |                 }
203 |             }
204 | 
205 |             // Fallback: log when debug is enabled
206 |             if (IsDebugEnabled())
207 |             {
208 |                 Debug.Log($"<b><color=#2EA3FF>MCP-TELEMETRY</color></b>: {telemetryData["event_type"]}");
209 |             }
210 |         }
211 | 
212 |         private static bool IsDebugEnabled()
213 |         {
214 |             try
215 |             {
216 |                 return UnityEditor.EditorPrefs.GetBool("MCPForUnity.DebugLogs", false);
217 |             }
218 |             catch
219 |             {
220 |                 return false;
221 |             }
222 |         }
223 |     }
224 | }
225 | 
```

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

```csharp
  1 | using System;
  2 | using System.Diagnostics;
  3 | using System.IO;
  4 | using System.Runtime.InteropServices;
  5 | using MCPForUnity.Editor.Dependencies.Models;
  6 | using MCPForUnity.Editor.Helpers;
  7 | 
  8 | namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
  9 | {
 10 |     /// <summary>
 11 |     /// macOS-specific dependency detection
 12 |     /// </summary>
 13 |     public class MacOSPlatformDetector : PlatformDetectorBase
 14 |     {
 15 |         public override string PlatformName => "macOS";
 16 | 
 17 |         public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
 18 | 
 19 |         public override DependencyStatus DetectPython()
 20 |         {
 21 |             var status = new DependencyStatus("Python", isRequired: true)
 22 |             {
 23 |                 InstallationHint = GetPythonInstallUrl()
 24 |             };
 25 | 
 26 |             try
 27 |             {
 28 |                 // Check common Python installation paths on macOS
 29 |                 var candidates = new[]
 30 |                 {
 31 |                     "python3",
 32 |                     "python",
 33 |                     "/usr/bin/python3",
 34 |                     "/usr/local/bin/python3",
 35 |                     "/opt/homebrew/bin/python3",
 36 |                     "/Library/Frameworks/Python.framework/Versions/3.13/bin/python3",
 37 |                     "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3",
 38 |                     "/Library/Frameworks/Python.framework/Versions/3.11/bin/python3",
 39 |                     "/Library/Frameworks/Python.framework/Versions/3.10/bin/python3"
 40 |                 };
 41 | 
 42 |                 foreach (var candidate in candidates)
 43 |                 {
 44 |                     if (TryValidatePython(candidate, out string version, out string fullPath))
 45 |                     {
 46 |                         status.IsAvailable = true;
 47 |                         status.Version = version;
 48 |                         status.Path = fullPath;
 49 |                         status.Details = $"Found Python {version} at {fullPath}";
 50 |                         return status;
 51 |                     }
 52 |                 }
 53 | 
 54 |                 // Try PATH resolution using 'which' command
 55 |                 if (TryFindInPath("python3", out string pathResult) ||
 56 |                     TryFindInPath("python", out pathResult))
 57 |                 {
 58 |                     if (TryValidatePython(pathResult, out string version, out string fullPath))
 59 |                     {
 60 |                         status.IsAvailable = true;
 61 |                         status.Version = version;
 62 |                         status.Path = fullPath;
 63 |                         status.Details = $"Found Python {version} in PATH at {fullPath}";
 64 |                         return status;
 65 |                     }
 66 |                 }
 67 | 
 68 |                 status.ErrorMessage = "Python not found. Please install Python 3.10 or later.";
 69 |                 status.Details = "Checked common installation paths including Homebrew, Framework, and system locations.";
 70 |             }
 71 |             catch (Exception ex)
 72 |             {
 73 |                 status.ErrorMessage = $"Error detecting Python: {ex.Message}";
 74 |             }
 75 | 
 76 |             return status;
 77 |         }
 78 | 
 79 |         public override string GetPythonInstallUrl()
 80 |         {
 81 |             return "https://www.python.org/downloads/macos/";
 82 |         }
 83 | 
 84 |         public override string GetUVInstallUrl()
 85 |         {
 86 |             return "https://docs.astral.sh/uv/getting-started/installation/#macos";
 87 |         }
 88 | 
 89 |         public override string GetInstallationRecommendations()
 90 |         {
 91 |             return @"macOS Installation Recommendations:
 92 | 
 93 | 1. Python: Install via Homebrew (recommended) or python.org
 94 |    - Homebrew: brew install python3
 95 |    - Direct download: https://python.org/downloads/macos/
 96 | 
 97 | 2. UV Package Manager: Install via curl or Homebrew
 98 |    - Curl: curl -LsSf https://astral.sh/uv/install.sh | sh
 99 |    - Homebrew: brew install uv
100 | 
101 | 3. MCP Server: Will be installed automatically by Unity MCP Bridge
102 | 
103 | Note: If using Homebrew, make sure /opt/homebrew/bin is in your PATH.";
104 |         }
105 | 
106 |         private bool TryValidatePython(string pythonPath, out string version, out string fullPath)
107 |         {
108 |             version = null;
109 |             fullPath = null;
110 | 
111 |             try
112 |             {
113 |                 var psi = new ProcessStartInfo
114 |                 {
115 |                     FileName = pythonPath,
116 |                     Arguments = "--version",
117 |                     UseShellExecute = false,
118 |                     RedirectStandardOutput = true,
119 |                     RedirectStandardError = true,
120 |                     CreateNoWindow = true
121 |                 };
122 | 
123 |                 // Set PATH to include common locations
124 |                 var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
125 |                 var pathAdditions = new[]
126 |                 {
127 |                     "/opt/homebrew/bin",
128 |                     "/usr/local/bin",
129 |                     "/usr/bin",
130 |                     Path.Combine(homeDir, ".local", "bin")
131 |                 };
132 | 
133 |                 string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
134 |                 psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath;
135 | 
136 |                 using var process = Process.Start(psi);
137 |                 if (process == null) return false;
138 | 
139 |                 string output = process.StandardOutput.ReadToEnd().Trim();
140 |                 process.WaitForExit(5000);
141 | 
142 |                 if (process.ExitCode == 0 && output.StartsWith("Python "))
143 |                 {
144 |                     version = output.Substring(7); // Remove "Python " prefix
145 |                     fullPath = pythonPath;
146 | 
147 |                     // Validate minimum version (Python 4+ or Python 3.10+)
148 |                     if (TryParseVersion(version, out var major, out var minor))
149 |                     {
150 |                         return major > 3 || (major >= 3 && minor >= 10);
151 |                     }
152 |                 }
153 |             }
154 |             catch
155 |             {
156 |                 // Ignore validation errors
157 |             }
158 | 
159 |             return false;
160 |         }
161 | 
162 |         private bool TryFindInPath(string executable, out string fullPath)
163 |         {
164 |             fullPath = null;
165 | 
166 |             try
167 |             {
168 |                 var psi = new ProcessStartInfo
169 |                 {
170 |                     FileName = "/usr/bin/which",
171 |                     Arguments = executable,
172 |                     UseShellExecute = false,
173 |                     RedirectStandardOutput = true,
174 |                     RedirectStandardError = true,
175 |                     CreateNoWindow = true
176 |                 };
177 | 
178 |                 // Enhance PATH for Unity's GUI environment
179 |                 var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
180 |                 var pathAdditions = new[]
181 |                 {
182 |                     "/opt/homebrew/bin",
183 |                     "/usr/local/bin",
184 |                     "/usr/bin",
185 |                     "/bin",
186 |                     Path.Combine(homeDir, ".local", "bin")
187 |                 };
188 | 
189 |                 string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
190 |                 psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath;
191 | 
192 |                 using var process = Process.Start(psi);
193 |                 if (process == null) return false;
194 | 
195 |                 string output = process.StandardOutput.ReadToEnd().Trim();
196 |                 process.WaitForExit(3000);
197 | 
198 |                 if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
199 |                 {
200 |                     fullPath = output;
201 |                     return true;
202 |                 }
203 |             }
204 |             catch
205 |             {
206 |                 // Ignore errors
207 |             }
208 | 
209 |             return false;
210 |         }
211 |     }
212 | }
213 | 
```

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

```csharp
  1 | using System;
  2 | using System.Diagnostics;
  3 | using System.IO;
  4 | using System.Runtime.InteropServices;
  5 | using MCPForUnity.Editor.Dependencies.Models;
  6 | using MCPForUnity.Editor.Helpers;
  7 | 
  8 | namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
  9 | {
 10 |     /// <summary>
 11 |     /// macOS-specific dependency detection
 12 |     /// </summary>
 13 |     public class MacOSPlatformDetector : PlatformDetectorBase
 14 |     {
 15 |         public override string PlatformName => "macOS";
 16 | 
 17 |         public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
 18 | 
 19 |         public override DependencyStatus DetectPython()
 20 |         {
 21 |             var status = new DependencyStatus("Python", isRequired: true)
 22 |             {
 23 |                 InstallationHint = GetPythonInstallUrl()
 24 |             };
 25 | 
 26 |             try
 27 |             {
 28 |                 // Check common Python installation paths on macOS
 29 |                 var candidates = new[]
 30 |                 {
 31 |                     "python3",
 32 |                     "python",
 33 |                     "/usr/bin/python3",
 34 |                     "/usr/local/bin/python3",
 35 |                     "/opt/homebrew/bin/python3",
 36 |                     "/Library/Frameworks/Python.framework/Versions/3.13/bin/python3",
 37 |                     "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3",
 38 |                     "/Library/Frameworks/Python.framework/Versions/3.11/bin/python3",
 39 |                     "/Library/Frameworks/Python.framework/Versions/3.10/bin/python3"
 40 |                 };
 41 | 
 42 |                 foreach (var candidate in candidates)
 43 |                 {
 44 |                     if (TryValidatePython(candidate, out string version, out string fullPath))
 45 |                     {
 46 |                         status.IsAvailable = true;
 47 |                         status.Version = version;
 48 |                         status.Path = fullPath;
 49 |                         status.Details = $"Found Python {version} at {fullPath}";
 50 |                         return status;
 51 |                     }
 52 |                 }
 53 | 
 54 |                 // Try PATH resolution using 'which' command
 55 |                 if (TryFindInPath("python3", out string pathResult) ||
 56 |                     TryFindInPath("python", out pathResult))
 57 |                 {
 58 |                     if (TryValidatePython(pathResult, out string version, out string fullPath))
 59 |                     {
 60 |                         status.IsAvailable = true;
 61 |                         status.Version = version;
 62 |                         status.Path = fullPath;
 63 |                         status.Details = $"Found Python {version} in PATH at {fullPath}";
 64 |                         return status;
 65 |                     }
 66 |                 }
 67 | 
 68 |                 status.ErrorMessage = "Python not found. Please install Python 3.10 or later.";
 69 |                 status.Details = "Checked common installation paths including Homebrew, Framework, and system locations.";
 70 |             }
 71 |             catch (Exception ex)
 72 |             {
 73 |                 status.ErrorMessage = $"Error detecting Python: {ex.Message}";
 74 |             }
 75 | 
 76 |             return status;
 77 |         }
 78 | 
 79 |         public override string GetPythonInstallUrl()
 80 |         {
 81 |             return "https://www.python.org/downloads/macos/";
 82 |         }
 83 | 
 84 |         public override string GetUVInstallUrl()
 85 |         {
 86 |             return "https://docs.astral.sh/uv/getting-started/installation/#macos";
 87 |         }
 88 | 
 89 |         public override string GetInstallationRecommendations()
 90 |         {
 91 |             return @"macOS Installation Recommendations:
 92 | 
 93 | 1. Python: Install via Homebrew (recommended) or python.org
 94 |    - Homebrew: brew install python3
 95 |    - Direct download: https://python.org/downloads/macos/
 96 | 
 97 | 2. UV Package Manager: Install via curl or Homebrew
 98 |    - Curl: curl -LsSf https://astral.sh/uv/install.sh | sh
 99 |    - Homebrew: brew install uv
100 | 
101 | 3. MCP Server: Will be installed automatically by MCP for Unity Bridge
102 | 
103 | Note: If using Homebrew, make sure /opt/homebrew/bin is in your PATH.";
104 |         }
105 | 
106 |         private bool TryValidatePython(string pythonPath, out string version, out string fullPath)
107 |         {
108 |             version = null;
109 |             fullPath = null;
110 | 
111 |             try
112 |             {
113 |                 var psi = new ProcessStartInfo
114 |                 {
115 |                     FileName = pythonPath,
116 |                     Arguments = "--version",
117 |                     UseShellExecute = false,
118 |                     RedirectStandardOutput = true,
119 |                     RedirectStandardError = true,
120 |                     CreateNoWindow = true
121 |                 };
122 | 
123 |                 // Set PATH to include common locations
124 |                 var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
125 |                 var pathAdditions = new[]
126 |                 {
127 |                     "/opt/homebrew/bin",
128 |                     "/usr/local/bin",
129 |                     "/usr/bin",
130 |                     Path.Combine(homeDir, ".local", "bin")
131 |                 };
132 | 
133 |                 string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
134 |                 psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath;
135 | 
136 |                 using var process = Process.Start(psi);
137 |                 if (process == null) return false;
138 | 
139 |                 string output = process.StandardOutput.ReadToEnd().Trim();
140 |                 process.WaitForExit(5000);
141 | 
142 |                 if (process.ExitCode == 0 && output.StartsWith("Python "))
143 |                 {
144 |                     version = output.Substring(7); // Remove "Python " prefix
145 |                     fullPath = pythonPath;
146 | 
147 |                     // Validate minimum version (Python 4+ or Python 3.10+)
148 |                     if (TryParseVersion(version, out var major, out var minor))
149 |                     {
150 |                         return major > 3 || (major >= 3 && minor >= 10);
151 |                     }
152 |                 }
153 |             }
154 |             catch
155 |             {
156 |                 // Ignore validation errors
157 |             }
158 | 
159 |             return false;
160 |         }
161 | 
162 |         private bool TryFindInPath(string executable, out string fullPath)
163 |         {
164 |             fullPath = null;
165 | 
166 |             try
167 |             {
168 |                 var psi = new ProcessStartInfo
169 |                 {
170 |                     FileName = "/usr/bin/which",
171 |                     Arguments = executable,
172 |                     UseShellExecute = false,
173 |                     RedirectStandardOutput = true,
174 |                     RedirectStandardError = true,
175 |                     CreateNoWindow = true
176 |                 };
177 | 
178 |                 // Enhance PATH for Unity's GUI environment
179 |                 var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
180 |                 var pathAdditions = new[]
181 |                 {
182 |                     "/opt/homebrew/bin",
183 |                     "/usr/local/bin",
184 |                     "/usr/bin",
185 |                     "/bin",
186 |                     Path.Combine(homeDir, ".local", "bin")
187 |                 };
188 | 
189 |                 string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
190 |                 psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath;
191 | 
192 |                 using var process = Process.Start(psi);
193 |                 if (process == null) return false;
194 | 
195 |                 string output = process.StandardOutput.ReadToEnd().Trim();
196 |                 process.WaitForExit(3000);
197 | 
198 |                 if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
199 |                 {
200 |                     fullPath = output;
201 |                     return true;
202 |                 }
203 |             }
204 |             catch
205 |             {
206 |                 // Ignore errors
207 |             }
208 | 
209 |             return false;
210 |         }
211 |     }
212 | }
213 | 
```

--------------------------------------------------------------------------------
/MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.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="Manage GameObjects. Note: for 'get_components', the `data` field contains a dictionary of component names and their serialized properties. For 'get_component', specify 'component_name' to retrieve only that component's serialized data."
 10 | )
 11 | def manage_gameobject(
 12 |     ctx: Context,
 13 |     action: Annotated[Literal["create", "modify", "delete", "find", "add_component", "remove_component", "set_component_property", "get_components", "get_component"], "Perform CRUD operations on GameObjects and components."],
 14 |     target: Annotated[str,
 15 |                       "GameObject identifier by name or path for modify/delete/component actions"] | None = None,
 16 |     search_method: Annotated[Literal["by_id", "by_name", "by_path", "by_tag", "by_layer", "by_component"],
 17 |                              "How to find objects. Used with 'find' and some 'target' lookups."] | None = None,
 18 |     name: Annotated[str,
 19 |                     "GameObject name for 'create' (initial name) and 'modify' (rename) actions ONLY. For 'find' action, use 'search_term' instead."] | None = None,
 20 |     tag: Annotated[str,
 21 |                    "Tag name - used for both 'create' (initial tag) and 'modify' (change tag)"] | None = None,
 22 |     parent: Annotated[str,
 23 |                       "Parent GameObject reference - used for both 'create' (initial parent) and 'modify' (change parent)"] | None = None,
 24 |     position: Annotated[list[float],
 25 |                         "Position - used for both 'create' (initial position) and 'modify' (change position)"] | None = None,
 26 |     rotation: Annotated[list[float],
 27 |                         "Rotation - used for both 'create' (initial rotation) and 'modify' (change rotation)"] | None = None,
 28 |     scale: Annotated[list[float],
 29 |                      "Scale - used for both 'create' (initial scale) and 'modify' (change scale)"] | None = None,
 30 |     components_to_add: Annotated[list[str],
 31 |                                  "List of component names to add"] | None = None,
 32 |     primitive_type: Annotated[str,
 33 |                               "Primitive type for 'create' action"] | None = None,
 34 |     save_as_prefab: Annotated[bool,
 35 |                               "If True, saves the created GameObject as a prefab"] | None = None,
 36 |     prefab_path: Annotated[str, "Path for prefab creation"] | None = None,
 37 |     prefab_folder: Annotated[str,
 38 |                              "Folder for prefab creation"] | None = None,
 39 |     # --- Parameters for 'modify' ---
 40 |     set_active: Annotated[bool,
 41 |                           "If True, sets the GameObject active"] | None = None,
 42 |     layer: Annotated[str, "Layer name"] | None = None,
 43 |     components_to_remove: Annotated[list[str],
 44 |                                     "List of component names to remove"] | None = None,
 45 |     component_properties: Annotated[dict[str, dict[str, Any]],
 46 |                                     """Dictionary of component names to their properties to set. For example:
 47 |                                     `{"MyScript": {"otherObject": {"find": "Player", "method": "by_name"}}}` assigns GameObject
 48 |                                     `{"MyScript": {"playerHealth": {"find": "Player", "component": "HealthComponent"}}}` assigns Component
 49 |                                     Example set nested property:
 50 |                                     - Access shared material: `{"MeshRenderer": {"sharedMaterial.color": [1, 0, 0, 1]}}`"""] | None = None,
 51 |     # --- Parameters for 'find' ---
 52 |     search_term: Annotated[str,
 53 |                            "Search term for 'find' action ONLY. Use this (not 'name') when searching for GameObjects."] | None = None,
 54 |     find_all: Annotated[bool,
 55 |                         "If True, finds all GameObjects matching the search term"] | None = None,
 56 |     search_in_children: Annotated[bool,
 57 |                                   "If True, searches in children of the GameObject"] | None = None,
 58 |     search_inactive: Annotated[bool,
 59 |                                "If True, searches inactive GameObjects"] | None = None,
 60 |     # -- Component Management Arguments --
 61 |     component_name: Annotated[str,
 62 |                               "Component name for 'add_component' and 'remove_component' actions"] | None = None,
 63 |     # Controls whether serialization of private [SerializeField] fields is included
 64 |     includeNonPublicSerialized: Annotated[bool,
 65 |                                           "Controls whether serialization of private [SerializeField] fields is included"] | None = None,
 66 | ) -> dict[str, Any]:
 67 |     ctx.info(f"Processing manage_gameobject: {action}")
 68 |     try:
 69 |         # Validate parameter usage to prevent silent failures
 70 |         if action == "find":
 71 |             if name is not None:
 72 |                 return {
 73 |                     "success": False,
 74 |                     "message": "For 'find' action, use 'search_term' parameter, not 'name'. Remove 'name' parameter. Example: search_term='Player', search_method='by_name'"
 75 |                 }
 76 |             if search_term is None:
 77 |                 return {
 78 |                     "success": False,
 79 |                     "message": "For 'find' action, 'search_term' parameter is required. Use search_term (not 'name') to specify what to find."
 80 |                 }
 81 | 
 82 |         if action in ["create", "modify"]:
 83 |             if search_term is not None:
 84 |                 return {
 85 |                     "success": False,
 86 |                     "message": f"For '{action}' action, use 'name' parameter, not 'search_term'."
 87 |                 }
 88 | 
 89 |         # Prepare parameters, removing None values
 90 |         params = {
 91 |             "action": action,
 92 |             "target": target,
 93 |             "searchMethod": search_method,
 94 |             "name": name,
 95 |             "tag": tag,
 96 |             "parent": parent,
 97 |             "position": position,
 98 |             "rotation": rotation,
 99 |             "scale": scale,
100 |             "componentsToAdd": components_to_add,
101 |             "primitiveType": primitive_type,
102 |             "saveAsPrefab": save_as_prefab,
103 |             "prefabPath": prefab_path,
104 |             "prefabFolder": prefab_folder,
105 |             "setActive": set_active,
106 |             "layer": layer,
107 |             "componentsToRemove": components_to_remove,
108 |             "componentProperties": component_properties,
109 |             "searchTerm": search_term,
110 |             "findAll": find_all,
111 |             "searchInChildren": search_in_children,
112 |             "searchInactive": search_inactive,
113 |             "componentName": component_name,
114 |             "includeNonPublicSerialized": includeNonPublicSerialized
115 |         }
116 |         params = {k: v for k, v in params.items() if v is not None}
117 | 
118 |         # --- Handle Prefab Path Logic ---
119 |         # Check if 'saveAsPrefab' is explicitly True in params
120 |         if action == "create" and params.get("saveAsPrefab"):
121 |             if "prefabPath" not in params:
122 |                 if "name" not in params or not params["name"]:
123 |                     return {"success": False, "message": "Cannot create default prefab path: 'name' parameter is missing."}
124 |                 # Use the provided prefab_folder (which has a default) and the name to construct the path
125 |                 constructed_path = f"{prefab_folder}/{params['name']}.prefab"
126 |                 # Ensure clean path separators (Unity prefers '/')
127 |                 params["prefabPath"] = constructed_path.replace("\\", "/")
128 |             elif not params["prefabPath"].lower().endswith(".prefab"):
129 |                 return {"success": False, "message": f"Invalid prefab_path: '{params['prefabPath']}' must end with .prefab"}
130 |         # Ensure prefabFolder itself isn't sent if prefabPath was constructed or provided
131 |         # The C# side only needs the final prefabPath
132 |         params.pop("prefabFolder", None)
133 |         # --------------------------------
134 | 
135 |         # Use centralized retry helper
136 |         response = send_command_with_retry("manage_gameobject", params)
137 | 
138 |         # Check if the response indicates success
139 |         # If the response is not successful, raise an exception with the error message
140 |         if isinstance(response, dict) and response.get("success"):
141 |             return {"success": True, "message": response.get("message", "GameObject operation successful."), "data": response.get("data")}
142 |         return response if isinstance(response, dict) else {"success": False, "message": str(response)}
143 | 
144 |     except Exception as e:
145 |         return {"success": False, "message": f"Python error managing GameObject: {str(e)}"}
```

--------------------------------------------------------------------------------
/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.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="Manage GameObjects. Note: for 'get_components', the `data` field contains a dictionary of component names and their serialized properties. For 'get_component', specify 'component_name' to retrieve only that component's serialized data."
 10 | )
 11 | def manage_gameobject(
 12 |     ctx: Context,
 13 |     action: Annotated[Literal["create", "modify", "delete", "find", "add_component", "remove_component", "set_component_property", "get_components", "get_component"], "Perform CRUD operations on GameObjects and components."],
 14 |     target: Annotated[str,
 15 |                       "GameObject identifier by name or path for modify/delete/component actions"] | None = None,
 16 |     search_method: Annotated[Literal["by_id", "by_name", "by_path", "by_tag", "by_layer", "by_component"],
 17 |                              "How to find objects. Used with 'find' and some 'target' lookups."] | None = None,
 18 |     name: Annotated[str,
 19 |                     "GameObject name for 'create' (initial name) and 'modify' (rename) actions ONLY. For 'find' action, use 'search_term' instead."] | None = None,
 20 |     tag: Annotated[str,
 21 |                    "Tag name - used for both 'create' (initial tag) and 'modify' (change tag)"] | None = None,
 22 |     parent: Annotated[str,
 23 |                       "Parent GameObject reference - used for both 'create' (initial parent) and 'modify' (change parent)"] | None = None,
 24 |     position: Annotated[list[float],
 25 |                         "Position - used for both 'create' (initial position) and 'modify' (change position)"] | None = None,
 26 |     rotation: Annotated[list[float],
 27 |                         "Rotation - used for both 'create' (initial rotation) and 'modify' (change rotation)"] | None = None,
 28 |     scale: Annotated[list[float],
 29 |                      "Scale - used for both 'create' (initial scale) and 'modify' (change scale)"] | None = None,
 30 |     components_to_add: Annotated[list[str],
 31 |                                  "List of component names to add"] | None = None,
 32 |     primitive_type: Annotated[str,
 33 |                               "Primitive type for 'create' action"] | None = None,
 34 |     save_as_prefab: Annotated[bool,
 35 |                               "If True, saves the created GameObject as a prefab"] | None = None,
 36 |     prefab_path: Annotated[str, "Path for prefab creation"] | None = None,
 37 |     prefab_folder: Annotated[str,
 38 |                              "Folder for prefab creation"] | None = None,
 39 |     # --- Parameters for 'modify' ---
 40 |     set_active: Annotated[bool,
 41 |                           "If True, sets the GameObject active"] | None = None,
 42 |     layer: Annotated[str, "Layer name"] | None = None,
 43 |     components_to_remove: Annotated[list[str],
 44 |                                     "List of component names to remove"] | None = None,
 45 |     component_properties: Annotated[dict[str, dict[str, Any]],
 46 |                                     """Dictionary of component names to their properties to set. For example:
 47 |                                     `{"MyScript": {"otherObject": {"find": "Player", "method": "by_name"}}}` assigns GameObject
 48 |                                     `{"MyScript": {"playerHealth": {"find": "Player", "component": "HealthComponent"}}}` assigns Component
 49 |                                     Example set nested property:
 50 |                                     - Access shared material: `{"MeshRenderer": {"sharedMaterial.color": [1, 0, 0, 1]}}`"""] | None = None,
 51 |     # --- Parameters for 'find' ---
 52 |     search_term: Annotated[str,
 53 |                            "Search term for 'find' action ONLY. Use this (not 'name') when searching for GameObjects."] | None = None,
 54 |     find_all: Annotated[bool,
 55 |                         "If True, finds all GameObjects matching the search term"] | None = None,
 56 |     search_in_children: Annotated[bool,
 57 |                                   "If True, searches in children of the GameObject"] | None = None,
 58 |     search_inactive: Annotated[bool,
 59 |                                "If True, searches inactive GameObjects"] | None = None,
 60 |     # -- Component Management Arguments --
 61 |     component_name: Annotated[str,
 62 |                               "Component name for 'add_component' and 'remove_component' actions"] | None = None,
 63 |     # Controls whether serialization of private [SerializeField] fields is included
 64 |     includeNonPublicSerialized: Annotated[bool,
 65 |                                           "Controls whether serialization of private [SerializeField] fields is included"] | None = None,
 66 | ) -> dict[str, Any]:
 67 |     ctx.info(f"Processing manage_gameobject: {action}")
 68 |     try:
 69 |         # Validate parameter usage to prevent silent failures
 70 |         if action == "find":
 71 |             if name is not None:
 72 |                 return {
 73 |                     "success": False,
 74 |                     "message": "For 'find' action, use 'search_term' parameter, not 'name'. Remove 'name' parameter. Example: search_term='Player', search_method='by_name'"
 75 |                 }
 76 |             if search_term is None:
 77 |                 return {
 78 |                     "success": False,
 79 |                     "message": "For 'find' action, 'search_term' parameter is required. Use search_term (not 'name') to specify what to find."
 80 |                 }
 81 | 
 82 |         if action in ["create", "modify"]:
 83 |             if search_term is not None:
 84 |                 return {
 85 |                     "success": False,
 86 |                     "message": f"For '{action}' action, use 'name' parameter, not 'search_term'."
 87 |                 }
 88 | 
 89 |         # Prepare parameters, removing None values
 90 |         params = {
 91 |             "action": action,
 92 |             "target": target,
 93 |             "searchMethod": search_method,
 94 |             "name": name,
 95 |             "tag": tag,
 96 |             "parent": parent,
 97 |             "position": position,
 98 |             "rotation": rotation,
 99 |             "scale": scale,
100 |             "componentsToAdd": components_to_add,
101 |             "primitiveType": primitive_type,
102 |             "saveAsPrefab": save_as_prefab,
103 |             "prefabPath": prefab_path,
104 |             "prefabFolder": prefab_folder,
105 |             "setActive": set_active,
106 |             "layer": layer,
107 |             "componentsToRemove": components_to_remove,
108 |             "componentProperties": component_properties,
109 |             "searchTerm": search_term,
110 |             "findAll": find_all,
111 |             "searchInChildren": search_in_children,
112 |             "searchInactive": search_inactive,
113 |             "componentName": component_name,
114 |             "includeNonPublicSerialized": includeNonPublicSerialized
115 |         }
116 |         params = {k: v for k, v in params.items() if v is not None}
117 | 
118 |         # --- Handle Prefab Path Logic ---
119 |         # Check if 'saveAsPrefab' is explicitly True in params
120 |         if action == "create" and params.get("saveAsPrefab"):
121 |             if "prefabPath" not in params:
122 |                 if "name" not in params or not params["name"]:
123 |                     return {"success": False, "message": "Cannot create default prefab path: 'name' parameter is missing."}
124 |                 # Use the provided prefab_folder (which has a default) and the name to construct the path
125 |                 constructed_path = f"{prefab_folder}/{params['name']}.prefab"
126 |                 # Ensure clean path separators (Unity prefers '/')
127 |                 params["prefabPath"] = constructed_path.replace("\\", "/")
128 |             elif not params["prefabPath"].lower().endswith(".prefab"):
129 |                 return {"success": False, "message": f"Invalid prefab_path: '{params['prefabPath']}' must end with .prefab"}
130 |         # Ensure prefabFolder itself isn't sent if prefabPath was constructed or provided
131 |         # The C# side only needs the final prefabPath
132 |         params.pop("prefabFolder", None)
133 |         # --------------------------------
134 | 
135 |         # Use centralized retry helper
136 |         response = send_command_with_retry("manage_gameobject", params)
137 | 
138 |         # Check if the response indicates success
139 |         # If the response is not successful, raise an exception with the error message
140 |         if isinstance(response, dict) and response.get("success"):
141 |             return {"success": True, "message": response.get("message", "GameObject operation successful."), "data": response.get("data")}
142 |         return response if isinstance(response, dict) else {"success": False, "message": str(response)}
143 | 
144 |     except Exception as e:
145 |         return {"success": False, "message": f"Python error managing GameObject: {str(e)}"}
```

--------------------------------------------------------------------------------
/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsTests.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System.IO;
  2 | using Newtonsoft.Json.Linq;
  3 | using NUnit.Framework;
  4 | using UnityEditor;
  5 | using UnityEditor.SceneManagement;
  6 | using UnityEngine;
  7 | using MCPForUnity.Editor.Tools.Prefabs;
  8 | using MCPForUnity.Editor.Tools;
  9 | 
 10 | namespace MCPForUnityTests.Editor.Tools
 11 | {
 12 |     public class ManagePrefabsTests
 13 |     {
 14 |         private const string TempDirectory = "Assets/Temp/ManagePrefabsTests";
 15 | 
 16 |         [SetUp]
 17 |         public void SetUp()
 18 |         {
 19 |             StageUtility.GoToMainStage();
 20 |             EnsureTempDirectoryExists();
 21 |         }
 22 | 
 23 |         [TearDown]
 24 |         public void TearDown()
 25 |         {
 26 |             StageUtility.GoToMainStage();
 27 |         }
 28 | 
 29 |         [OneTimeTearDown]
 30 |         public void CleanupAll()
 31 |         {
 32 |             StageUtility.GoToMainStage();
 33 |             if (AssetDatabase.IsValidFolder(TempDirectory))
 34 |             {
 35 |                 AssetDatabase.DeleteAsset(TempDirectory);
 36 |             }
 37 |         }
 38 | 
 39 |         [Test]
 40 |         public void OpenStage_OpensPrefabInIsolation()
 41 |         {
 42 |             string prefabPath = CreateTestPrefab("OpenStageCube");
 43 | 
 44 |             try
 45 |             {
 46 |                 var openParams = new JObject
 47 |                 {
 48 |                     ["action"] = "open_stage",
 49 |                     ["prefabPath"] = prefabPath
 50 |                 };
 51 | 
 52 |                 var openResult = ToJObject(ManagePrefabs.HandleCommand(openParams));
 53 | 
 54 |                 Assert.IsTrue(openResult.Value<bool>("success"), "open_stage should succeed for a valid prefab.");
 55 | 
 56 |                 PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
 57 |                 Assert.IsNotNull(stage, "Prefab stage should be open after open_stage.");
 58 |                 Assert.AreEqual(prefabPath, stage.assetPath, "Opened stage should match prefab path.");
 59 | 
 60 |                 var stageInfo = ToJObject(ManageEditor.HandleCommand(new JObject { ["action"] = "get_prefab_stage" }));
 61 |                 Assert.IsTrue(stageInfo.Value<bool>("success"), "get_prefab_stage should succeed when stage is open.");
 62 | 
 63 |                 var data = stageInfo["data"] as JObject;
 64 |                 Assert.IsNotNull(data, "Stage info should include data payload.");
 65 |                 Assert.IsTrue(data.Value<bool>("isOpen"));
 66 |                 Assert.AreEqual(prefabPath, data.Value<string>("assetPath"));
 67 |             }
 68 |             finally
 69 |             {
 70 |                 StageUtility.GoToMainStage();
 71 |                 AssetDatabase.DeleteAsset(prefabPath);
 72 |             }
 73 |         }
 74 | 
 75 |         [Test]
 76 |         public void CloseStage_ReturnsSuccess_WhenNoStageOpen()
 77 |         {
 78 |             StageUtility.GoToMainStage();
 79 |             var closeResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
 80 |             {
 81 |                 ["action"] = "close_stage"
 82 |             }));
 83 | 
 84 |             Assert.IsTrue(closeResult.Value<bool>("success"), "close_stage should succeed even if no stage is open.");
 85 |         }
 86 | 
 87 |         [Test]
 88 |         public void CloseStage_ClosesOpenPrefabStage()
 89 |         {
 90 |             string prefabPath = CreateTestPrefab("CloseStageCube");
 91 | 
 92 |             try
 93 |             {
 94 |                 ManagePrefabs.HandleCommand(new JObject
 95 |                 {
 96 |                     ["action"] = "open_stage",
 97 |                     ["prefabPath"] = prefabPath
 98 |                 });
 99 | 
100 |                 var closeResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
101 |                 {
102 |                     ["action"] = "close_stage"
103 |                 }));
104 | 
105 |                 Assert.IsTrue(closeResult.Value<bool>("success"), "close_stage should succeed when stage is open.");
106 |                 Assert.IsNull(PrefabStageUtility.GetCurrentPrefabStage(), "Prefab stage should be closed after close_stage.");
107 |             }
108 |             finally
109 |             {
110 |                 StageUtility.GoToMainStage();
111 |                 AssetDatabase.DeleteAsset(prefabPath);
112 |             }
113 |         }
114 | 
115 |         [Test]
116 |         public void SaveOpenStage_SavesDirtyChanges()
117 |         {
118 |             string prefabPath = CreateTestPrefab("SaveStageCube");
119 | 
120 |             try
121 |             {
122 |                 ManagePrefabs.HandleCommand(new JObject
123 |                 {
124 |                     ["action"] = "open_stage",
125 |                     ["prefabPath"] = prefabPath
126 |                 });
127 | 
128 |                 PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
129 |                 Assert.IsNotNull(stage, "Stage should be open before modifying.");
130 | 
131 |                 stage.prefabContentsRoot.transform.localScale = new Vector3(2f, 2f, 2f);
132 | 
133 |                 var saveResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
134 |                 {
135 |                     ["action"] = "save_open_stage"
136 |                 }));
137 | 
138 |                 Assert.IsTrue(saveResult.Value<bool>("success"), "save_open_stage should succeed when stage is open.");
139 |                 Assert.IsFalse(stage.scene.isDirty, "Stage scene should not be dirty after saving.");
140 | 
141 |                 GameObject reloaded = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
142 |                 Assert.AreEqual(new Vector3(2f, 2f, 2f), reloaded.transform.localScale, "Saved prefab asset should include changes from open stage.");
143 |             }
144 |             finally
145 |             {
146 |                 StageUtility.GoToMainStage();
147 |                 AssetDatabase.DeleteAsset(prefabPath);
148 |             }
149 |         }
150 | 
151 |         [Test]
152 |         public void SaveOpenStage_ReturnsError_WhenNoStageOpen()
153 |         {
154 |             StageUtility.GoToMainStage();
155 | 
156 |             var saveResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
157 |             {
158 |                 ["action"] = "save_open_stage"
159 |             }));
160 | 
161 |             Assert.IsFalse(saveResult.Value<bool>("success"), "save_open_stage should fail when no stage is open.");
162 |         }
163 | 
164 |         [Test]
165 |         public void CreateFromGameObject_CreatesPrefabAndLinksInstance()
166 |         {
167 |             EnsureTempDirectoryExists();
168 |             StageUtility.GoToMainStage();
169 | 
170 |             string prefabPath = Path.Combine(TempDirectory, "SceneObjectSaved.prefab").Replace('\\', '/');
171 |             GameObject sceneObject = new GameObject("ScenePrefabSource");
172 | 
173 |             try
174 |             {
175 |                 var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
176 |                 {
177 |                     ["action"] = "create_from_gameobject",
178 |                     ["target"] = sceneObject.name,
179 |                     ["prefabPath"] = prefabPath
180 |                 }));
181 | 
182 |                 Assert.IsTrue(result.Value<bool>("success"), "create_from_gameobject should succeed for a valid scene object.");
183 | 
184 |                 var data = result["data"] as JObject;
185 |                 Assert.IsNotNull(data, "Response data should include prefab information.");
186 | 
187 |                 string savedPath = data.Value<string>("prefabPath");
188 |                 Assert.AreEqual(prefabPath, savedPath, "Returned prefab path should match the requested path.");
189 | 
190 |                 GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(savedPath);
191 |                 Assert.IsNotNull(prefabAsset, "Prefab asset should exist at the saved path.");
192 | 
193 |                 int instanceId = data.Value<int>("instanceId");
194 |                 var linkedInstance = EditorUtility.InstanceIDToObject(instanceId) as GameObject;
195 |                 Assert.IsNotNull(linkedInstance, "Linked instance should resolve from instanceId.");
196 |                 Assert.AreEqual(savedPath, PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(linkedInstance), "Instance should be connected to the new prefab.");
197 | 
198 |                 sceneObject = linkedInstance;
199 |             }
200 |             finally
201 |             {
202 |                 if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(prefabPath) != null)
203 |                 {
204 |                     AssetDatabase.DeleteAsset(prefabPath);
205 |                 }
206 | 
207 |                 if (sceneObject != null)
208 |                 {
209 |                     if (PrefabUtility.IsPartOfPrefabInstance(sceneObject))
210 |                     {
211 |                         PrefabUtility.UnpackPrefabInstance(
212 |                             sceneObject,
213 |                             PrefabUnpackMode.Completely,
214 |                             InteractionMode.AutomatedAction
215 |                         );
216 |                     }
217 |                     UnityEngine.Object.DestroyImmediate(sceneObject, true);
218 |                 }
219 |             }
220 |         }
221 | 
222 |         private static string CreateTestPrefab(string name)
223 |         {
224 |             EnsureTempDirectoryExists();
225 | 
226 |             GameObject temp = GameObject.CreatePrimitive(PrimitiveType.Cube);
227 |             temp.name = name;
228 | 
229 |             string path = Path.Combine(TempDirectory, name + ".prefab").Replace('\\', '/');
230 |             PrefabUtility.SaveAsPrefabAsset(temp, path, out bool success);
231 |             UnityEngine.Object.DestroyImmediate(temp);
232 | 
233 |             Assert.IsTrue(success, "PrefabUtility.SaveAsPrefabAsset should succeed for test prefab.");
234 |             return path;
235 |         }
236 | 
237 |         private static void EnsureTempDirectoryExists()
238 |         {
239 |             if (!AssetDatabase.IsValidFolder("Assets/Temp"))
240 |             {
241 |                 AssetDatabase.CreateFolder("Assets", "Temp");
242 |             }
243 | 
244 |             if (!AssetDatabase.IsValidFolder(TempDirectory))
245 |             {
246 |                 AssetDatabase.CreateFolder("Assets/Temp", "ManagePrefabsTests");
247 |             }
248 |         }
249 | 
250 |         private static JObject ToJObject(object result)
251 |         {
252 |             return result as JObject ?? JObject.FromObject(result);
253 |         }
254 |     }
255 | }
256 | 
```

--------------------------------------------------------------------------------
/.claude/prompts/nl-unity-suite-nl.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Unity NL Editing Suite — Additive Test Design
  2 | 
  3 | You are running inside CI for the `unity-mcp` repo. Use only the tools allowed by the workflow. Work autonomously; do not prompt the user. Do NOT spawn subagents.
  4 | 
  5 | **Print this once, verbatim, early in the run:**
  6 | AllowedTools: Write,mcp__unity__manage_editor,mcp__unity__list_resources,mcp__unity__read_resource,mcp__unity__apply_text_edits,mcp__unity__script_apply_edits,mcp__unity__validate_script,mcp__unity__find_in_file,mcp__unity__read_console,mcp__unity__get_sha
  7 | 
  8 | ---
  9 | 
 10 | ## Mission
 11 | 1) Pick target file (prefer):
 12 |    - `unity://path/Assets/Scripts/LongUnityScriptClaudeTest.cs`
 13 | 2) Execute NL tests NL-0..NL-4 in order using minimal, precise edits that build on each other.
 14 | 3) Validate each edit with `mcp__unity__validate_script(level:"standard")`.
 15 | 4) **Report**: write one `<testcase>` XML fragment per test to `reports/<TESTID>_results.xml`. Do **not** read or edit `$JUNIT_OUT`.
 16 | 
 17 | **CRITICAL XML FORMAT REQUIREMENTS:**
 18 | - Each file must contain EXACTLY one `<testcase>` root element
 19 | - NO prologue, epilogue, code fences, or extra characters
 20 | - NO markdown formatting or explanations outside the XML
 21 | - Use this exact format:
 22 | 
 23 | ```xml
 24 | <testcase name="NL-0 — Baseline State Capture" classname="UnityMCP.NL-T">
 25 |   <system-out><![CDATA[
 26 | (evidence of what was accomplished)
 27 |   ]]></system-out>
 28 | </testcase>
 29 | ```
 30 | 
 31 | - If test fails, include: `<failure message="reason"/>`
 32 | - TESTID must be one of: NL-0, NL-1, NL-2, NL-3, NL-4
 33 | 5) **NO RESTORATION** - tests build additively on previous state.
 34 | 6) **STRICT FRAGMENT EMISSION** - After each test, immediately emit a clean XML file under `reports/<TESTID>_results.xml` with exactly one `<testcase>` whose `name` begins with the exact test id. No prologue/epilogue or fences. If the test fails, include a `<failure message="..."/>` and still emit.
 35 | 
 36 | ---
 37 | 
 38 | ## Environment & Paths (CI)
 39 | - Always pass: `project_root: "TestProjects/UnityMCPTests"` and `ctx: {}` on list/read/edit/validate.
 40 | - **Canonical URIs only**:
 41 |   - Primary: `unity://path/Assets/...` (never embed `project_root` in the URI)
 42 |   - Relative (when supported): `Assets/...`
 43 | 
 44 | CI provides:
 45 | - `$JUNIT_OUT=reports/junit-nl-suite.xml` (pre‑created; leave alone)
 46 | - `$MD_OUT=reports/junit-nl-suite.md` (synthesized from JUnit)
 47 | 
 48 | ---
 49 | 
 50 | ## Transcript Minimization Rules
 51 | - Do not restate tool JSON; summarize in ≤ 2 short lines.
 52 | - Never paste full file contents. For matches, include only the matched line and ±1 line.
 53 | - Prefer `mcp__unity__find_in_file` for targeting; avoid `mcp__unity__read_resource` unless strictly necessary. If needed, limit to `head_bytes ≤ 256` or `tail_lines ≤ 10`.
 54 | - Per‑test `system-out` ≤ 400 chars: brief status only (no SHA).
 55 | - Console evidence: fetch the last 10 lines with `include_stacktrace:false` and include ≤ 3 lines in the fragment.
 56 | - Avoid quoting multi‑line diffs; reference markers instead.
 57 | — Console scans: perform two reads — last 10 `log/info` lines and up to 3 `error` entries (use `include_stacktrace:false`); include ≤ 3 lines total in the fragment; if no errors, state "no errors".
 58 | 
 59 | ---
 60 | 
 61 | ## Tool Mapping
 62 | - **Anchors/regex/structured**: `mcp__unity__script_apply_edits`
 63 |   - Allowed ops: `anchor_insert`, `replace_method`, `insert_method`, `delete_method`, `regex_replace`
 64 |   - For `anchor_insert`, always set `"position": "before"` or `"after"`.
 65 | - **Precise ranges / atomic batch**: `mcp__unity__apply_text_edits` (non‑overlapping ranges)
 66 | STRICT OP GUARDRAILS
 67 | - Do not use `anchor_replace`. Structured edits must be one of: `anchor_insert`, `replace_method`, `insert_method`, `delete_method`, `regex_replace`.
 68 | - For multi‑spot textual tweaks in one operation, compute non‑overlapping ranges with `mcp__unity__find_in_file` and use `mcp__unity__apply_text_edits`.
 69 | 
 70 | - **Hash-only**: `mcp__unity__get_sha` — returns `{sha256,lengthBytes,lastModifiedUtc}` without file body
 71 | - **Validation**: `mcp__unity__validate_script(level:"standard")`
 72 | - **Dynamic targeting**: Use `mcp__unity__find_in_file` to locate current positions of methods/markers
 73 | 
 74 | ---
 75 | 
 76 | ## Additive Test Design Principles
 77 | 
 78 | **Key Changes from Reset-Based:**
 79 | 1. **Dynamic Targeting**: Use `find_in_file` to locate methods/content, never hardcode line numbers
 80 | 2. **State Awareness**: Each test expects the file state left by the previous test
 81 | 3. **Content-Based Operations**: Target methods by signature, classes by name, not coordinates
 82 | 4. **Cumulative Validation**: Ensure the file remains structurally sound throughout the sequence
 83 | 5. **Composability**: Tests demonstrate how operations work together in real workflows
 84 | 
 85 | **State Tracking:**
 86 | - Track file SHA after each test (`mcp__unity__get_sha`) for potential preconditions in later passes. Do not include SHA values in report fragments.
 87 | - Use content signatures (method names, comment markers) to verify expected state
 88 | - Validate structural integrity after each major change
 89 | 
 90 | ---
 91 | 
 92 | ## Execution Order & Additive Test Specs
 93 | 
 94 | ### NL-0. Baseline State Capture
 95 | **Goal**: Establish initial file state and verify accessibility
 96 | **Actions**:
 97 | - Read file head and tail to confirm structure
 98 | - Locate key methods: `HasTarget()`, `GetCurrentTarget()`, `Update()`, `ApplyBlend()`
 99 | - Record initial SHA for tracking
100 | - **Expected final state**: Unchanged baseline file
101 | 
102 | ### NL-1. Core Method Operations (Additive State A)
103 | **Goal**: Demonstrate method replacement operations
104 | **Actions**: 
105 | - Replace `HasTarget()` method body: `public bool HasTarget() { return currentTarget != null; }`
106 | - Insert `PrintSeries()` method after `GetCurrentTarget()`: `public void PrintSeries() { Debug.Log("1,2,3"); }`
107 | - Verify both methods exist and are properly formatted
108 | - Delete `PrintSeries()` method (cleanup for next test)
109 | - **Expected final state**: `HasTarget()` modified, file structure intact, no temporary methods
110 | 
111 | ### NL-2. Anchor Comment Insertion (Additive State B) 
112 | **Goal**: Demonstrate anchor-based insertions above methods
113 | **Actions**:
114 | - Use `find_in_file` to locate current position of `Update()` method
115 | - Insert `// Build marker OK` comment line above `Update()` method
116 | - Verify comment exists and `Update()` still functions
117 | - **Expected final state**: State A + build marker comment above `Update()`
118 | 
119 | ### NL-3. End-of-Class Content (Additive State C)
120 | **Goal**: Demonstrate end-of-class insertions with smart brace matching
121 | **Actions**:
122 | - Match the final class-closing brace by scanning from EOF (e.g., last `^\s*}\s*$`)
123 |   or compute via `find_in_file` + ranges; insert immediately before it.
124 | - Insert three comment lines before final class brace:
125 |   ```
126 |   // Tail test A
127 |   // Tail test B  
128 |   // Tail test C
129 |   ```
130 | - **Expected final state**: State B + tail comments before class closing brace
131 | 
132 | ### NL-4. Console State Verification (No State Change)
133 | **Goal**: Verify Unity console integration without file modification
134 | **Actions**:
135 | - Read last 10 Unity console lines (log/info)
136 | - Perform a targeted scan for errors/exceptions (type: errors), up to 3 entries
137 | - Validate no compilation errors from previous operations
138 | - **Expected final state**: State C (unchanged)
139 | - **IMMEDIATELY** write clean XML fragment to `reports/NL-4_results.xml` (no extra text). The `<testcase name>` must start with `NL-4`. Include at most 3 lines total across both reads, or simply state "no errors; console OK" (≤ 400 chars).
140 | 
141 | ## Dynamic Targeting Examples
142 | 
143 | **Instead of hardcoded coordinates:**
144 | ```json
145 | {"startLine": 31, "startCol": 26, "endLine": 31, "endCol": 58}
146 | ```
147 | 
148 | **Use content-aware targeting:**
149 | ```json
150 | # Find current method location
151 | find_in_file(pattern: "public bool HasTarget\\(\\)")
152 | # Then compute edit ranges from found position
153 | ```
154 | 
155 | **Method targeting by signature:**
156 | ```json
157 | {"op": "replace_method", "className": "LongUnityScriptClaudeTest", "methodName": "HasTarget"}
158 | ```
159 | 
160 | **Anchor-based insertions:**
161 | ```json  
162 | {"op": "anchor_insert", "anchor": "private void Update\\(\\)", "position": "before", "text": "// comment"}
163 | ```
164 | 
165 | ---
166 | 
167 | ## State Verification Patterns
168 | 
169 | **After each test:**
170 | 1. Verify expected content exists: `find_in_file` for key markers
171 | 2. Check structural integrity: `validate_script(level:"standard")`  
172 | 3. Update SHA tracking for next test's preconditions
173 | 4. Emit a per‑test fragment to `reports/<TESTID>_results.xml` immediately. If the test failed, still write a single `<testcase>` with a `<failure message="..."/>` and evidence in `system-out`.
174 | 5. Log cumulative changes in test evidence (keep concise per Transcript Minimization Rules; never paste raw tool JSON)
175 | 
176 | **Error Recovery:**
177 | - If test fails, log current state but continue (don't restore)
178 | - Next test adapts to actual current state, not expected state
179 | - Demonstrates resilience of operations on varied file conditions
180 | 
181 | ---
182 | 
183 | ## Benefits of Additive Design
184 | 
185 | 1. **Realistic Workflows**: Tests mirror actual development patterns
186 | 2. **Robust Operations**: Proves edits work on evolving files, not just pristine baselines  
187 | 3. **Composability Validation**: Shows operations coordinate well together
188 | 4. **Simplified Infrastructure**: No restore scripts or snapshots needed
189 | 5. **Better Failure Analysis**: Failures don't cascade - each test adapts to current reality
190 | 6. **State Evolution Testing**: Validates SDK handles cumulative file modifications correctly
191 | 
192 | This additive approach produces a more realistic and maintainable test suite that better represents actual SDK usage patterns.
193 | 
194 | ---
195 | 
196 | BAN ON EXTRA TOOLS AND DIRS
197 | - Do not use any tools outside `AllowedTools`. Do not create directories; assume `reports/` exists.
198 | 
199 | ---
200 | 
201 | 
```
Page 4/18FirstPrevNextLast