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

# Directory Structure

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

# Files

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

```csharp
   1 | using System;
   2 | using System.Collections.Generic;
   3 | using System.Globalization;
   4 | using System.IO;
   5 | using System.Linq;
   6 | using Newtonsoft.Json.Linq;
   7 | using UnityEditor;
   8 | using UnityEngine;
   9 | using MCPForUnity.Editor.Helpers; // For Response class
  10 | using static MCPForUnity.Editor.Tools.ManageGameObject;
  11 | 
  12 | #if UNITY_6000_0_OR_NEWER
  13 | using PhysicsMaterialType = UnityEngine.PhysicsMaterial;
  14 | using PhysicsMaterialCombine = UnityEngine.PhysicsMaterialCombine;  
  15 | #else
  16 | using PhysicsMaterialType = UnityEngine.PhysicMaterial;
  17 | using PhysicsMaterialCombine = UnityEngine.PhysicMaterialCombine;
  18 | #endif
  19 | 
  20 | namespace MCPForUnity.Editor.Tools
  21 | {
  22 |     /// <summary>
  23 |     /// Handles asset management operations within the Unity project.
  24 |     /// </summary>
  25 |     [McpForUnityTool("manage_asset")]
  26 |     public static class ManageAsset
  27 |     {
  28 |         // --- Main Handler ---
  29 | 
  30 |         // Define the list of valid actions
  31 |         private static readonly List<string> ValidActions = new List<string>
  32 |         {
  33 |             "import",
  34 |             "create",
  35 |             "modify",
  36 |             "delete",
  37 |             "duplicate",
  38 |             "move",
  39 |             "rename",
  40 |             "search",
  41 |             "get_info",
  42 |             "create_folder",
  43 |             "get_components",
  44 |         };
  45 | 
  46 |         public static object HandleCommand(JObject @params)
  47 |         {
  48 |             string action = @params["action"]?.ToString().ToLower();
  49 |             if (string.IsNullOrEmpty(action))
  50 |             {
  51 |                 return Response.Error("Action parameter is required.");
  52 |             }
  53 | 
  54 |             // Check if the action is valid before switching
  55 |             if (!ValidActions.Contains(action))
  56 |             {
  57 |                 string validActionsList = string.Join(", ", ValidActions);
  58 |                 return Response.Error(
  59 |                     $"Unknown action: '{action}'. Valid actions are: {validActionsList}"
  60 |                 );
  61 |             }
  62 | 
  63 |             // Common parameters
  64 |             string path = @params["path"]?.ToString();
  65 | 
  66 |             try
  67 |             {
  68 |                 switch (action)
  69 |                 {
  70 |                     case "import":
  71 |                         // Note: Unity typically auto-imports. This might re-import or configure import settings.
  72 |                         return ReimportAsset(path, @params["properties"] as JObject);
  73 |                     case "create":
  74 |                         return CreateAsset(@params);
  75 |                     case "modify":
  76 |                         return ModifyAsset(path, @params["properties"] as JObject);
  77 |                     case "delete":
  78 |                         return DeleteAsset(path);
  79 |                     case "duplicate":
  80 |                         return DuplicateAsset(path, @params["destination"]?.ToString());
  81 |                     case "move": // Often same as rename if within Assets/
  82 |                     case "rename":
  83 |                         return MoveOrRenameAsset(path, @params["destination"]?.ToString());
  84 |                     case "search":
  85 |                         return SearchAssets(@params);
  86 |                     case "get_info":
  87 |                         return GetAssetInfo(
  88 |                             path,
  89 |                             @params["generatePreview"]?.ToObject<bool>() ?? false
  90 |                         );
  91 |                     case "create_folder": // Added specific action for clarity
  92 |                         return CreateFolder(path);
  93 |                     case "get_components":
  94 |                         return GetComponentsFromAsset(path);
  95 | 
  96 |                     default:
  97 |                         // This error message is less likely to be hit now, but kept here as a fallback or for potential future modifications.
  98 |                         string validActionsListDefault = string.Join(", ", ValidActions);
  99 |                         return Response.Error(
 100 |                             $"Unknown action: '{action}'. Valid actions are: {validActionsListDefault}"
 101 |                         );
 102 |                 }
 103 |             }
 104 |             catch (Exception e)
 105 |             {
 106 |                 Debug.LogError($"[ManageAsset] Action '{action}' failed for path '{path}': {e}");
 107 |                 return Response.Error(
 108 |                     $"Internal error processing action '{action}' on '{path}': {e.Message}"
 109 |                 );
 110 |             }
 111 |         }
 112 | 
 113 |         // --- Action Implementations ---
 114 | 
 115 |         private static object ReimportAsset(string path, JObject properties)
 116 |         {
 117 |             if (string.IsNullOrEmpty(path))
 118 |                 return Response.Error("'path' is required for reimport.");
 119 |             string fullPath = AssetPathUtility.SanitizeAssetPath(path);
 120 |             if (!AssetExists(fullPath))
 121 |                 return Response.Error($"Asset not found at path: {fullPath}");
 122 | 
 123 |             try
 124 |             {
 125 |                 // TODO: Apply importer properties before reimporting?
 126 |                 // This is complex as it requires getting the AssetImporter, casting it,
 127 |                 // applying properties via reflection or specific methods, saving, then reimporting.
 128 |                 if (properties != null && properties.HasValues)
 129 |                 {
 130 |                     Debug.LogWarning(
 131 |                         "[ManageAsset.Reimport] Modifying importer properties before reimport is not fully implemented yet."
 132 |                     );
 133 |                     // AssetImporter importer = AssetImporter.GetAtPath(fullPath);
 134 |                     // if (importer != null) { /* Apply properties */ AssetDatabase.WriteImportSettingsIfDirty(fullPath); }
 135 |                 }
 136 | 
 137 |                 AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate);
 138 |                 // AssetDatabase.Refresh(); // Usually ImportAsset handles refresh
 139 |                 return Response.Success($"Asset '{fullPath}' reimported.", GetAssetData(fullPath));
 140 |             }
 141 |             catch (Exception e)
 142 |             {
 143 |                 return Response.Error($"Failed to reimport asset '{fullPath}': {e.Message}");
 144 |             }
 145 |         }
 146 | 
 147 |         private static object CreateAsset(JObject @params)
 148 |         {
 149 |             string path = @params["path"]?.ToString();
 150 |             string assetType = @params["assetType"]?.ToString();
 151 |             JObject properties = @params["properties"] as JObject;
 152 | 
 153 |             if (string.IsNullOrEmpty(path))
 154 |                 return Response.Error("'path' is required for create.");
 155 |             if (string.IsNullOrEmpty(assetType))
 156 |                 return Response.Error("'assetType' is required for create.");
 157 | 
 158 |             string fullPath = AssetPathUtility.SanitizeAssetPath(path);
 159 |             string directory = Path.GetDirectoryName(fullPath);
 160 | 
 161 |             // Ensure directory exists
 162 |             if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), directory)))
 163 |             {
 164 |                 Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), directory));
 165 |                 AssetDatabase.Refresh(); // Make sure Unity knows about the new folder
 166 |             }
 167 | 
 168 |             if (AssetExists(fullPath))
 169 |                 return Response.Error($"Asset already exists at path: {fullPath}");
 170 | 
 171 |             try
 172 |             {
 173 |                 UnityEngine.Object newAsset = null;
 174 |                 string lowerAssetType = assetType.ToLowerInvariant();
 175 | 
 176 |                 // Handle common asset types
 177 |                 if (lowerAssetType == "folder")
 178 |                 {
 179 |                     return CreateFolder(path); // Use dedicated method
 180 |                 }
 181 |                 else if (lowerAssetType == "material")
 182 |                 {
 183 |                     // Prefer provided shader; fall back to common pipelines
 184 |                     var requested = properties?["shader"]?.ToString();
 185 |                     Shader shader =
 186 |                         (!string.IsNullOrEmpty(requested) ? Shader.Find(requested) : null)
 187 |                         ?? Shader.Find("Universal Render Pipeline/Lit")
 188 |                         ?? Shader.Find("HDRP/Lit")
 189 |                         ?? Shader.Find("Standard")
 190 |                         ?? Shader.Find("Unlit/Color");
 191 |                     if (shader == null)
 192 |                         return Response.Error($"Could not find a suitable shader (requested: '{requested ?? "none"}').");
 193 | 
 194 |                     var mat = new Material(shader);
 195 |                     if (properties != null)
 196 |                         ApplyMaterialProperties(mat, properties);
 197 |                     AssetDatabase.CreateAsset(mat, fullPath);
 198 |                     newAsset = mat;
 199 |                 }
 200 |                 else if (lowerAssetType == "physicsmaterial")
 201 |                 {
 202 |                     PhysicsMaterialType pmat = new PhysicsMaterialType();
 203 |                     if (properties != null)
 204 |                         ApplyPhysicsMaterialProperties(pmat, properties);
 205 |                     AssetDatabase.CreateAsset(pmat, fullPath);
 206 |                     newAsset = pmat;
 207 |                 }
 208 |                 else if (lowerAssetType == "scriptableobject")
 209 |                 {
 210 |                     string scriptClassName = properties?["scriptClass"]?.ToString();
 211 |                     if (string.IsNullOrEmpty(scriptClassName))
 212 |                         return Response.Error(
 213 |                             "'scriptClass' property required when creating ScriptableObject asset."
 214 |                         );
 215 | 
 216 |                     Type scriptType = ComponentResolver.TryResolve(scriptClassName, out var resolvedType, out var error) ? resolvedType : null;
 217 |                     if (
 218 |                         scriptType == null
 219 |                         || !typeof(ScriptableObject).IsAssignableFrom(scriptType)
 220 |                     )
 221 |                     {
 222 |                         var reason = scriptType == null
 223 |                             ? (string.IsNullOrEmpty(error) ? "Type not found." : error)
 224 |                             : "Type found but does not inherit from ScriptableObject.";
 225 |                         return Response.Error($"Script class '{scriptClassName}' invalid: {reason}");
 226 |                     }
 227 | 
 228 |                     ScriptableObject so = ScriptableObject.CreateInstance(scriptType);
 229 |                     // TODO: Apply properties from JObject to the ScriptableObject instance?
 230 |                     AssetDatabase.CreateAsset(so, fullPath);
 231 |                     newAsset = so;
 232 |                 }
 233 |                 else if (lowerAssetType == "prefab")
 234 |                 {
 235 |                     // Creating prefabs usually involves saving an existing GameObject hierarchy.
 236 |                     // A common pattern is to create an empty GameObject, configure it, and then save it.
 237 |                     return Response.Error(
 238 |                         "Creating prefabs programmatically usually requires a source GameObject. Use manage_gameobject to create/configure, then save as prefab via a separate mechanism or future enhancement."
 239 |                     );
 240 |                     // Example (conceptual):
 241 |                     // GameObject source = GameObject.Find(properties["sourceGameObject"].ToString());
 242 |                     // if(source != null) PrefabUtility.SaveAsPrefabAsset(source, fullPath);
 243 |                 }
 244 |                 // TODO: Add more asset types (Animation Controller, Scene, etc.)
 245 |                 else
 246 |                 {
 247 |                     // Generic creation attempt (might fail or create empty files)
 248 |                     // For some types, just creating the file might be enough if Unity imports it.
 249 |                     // File.Create(Path.Combine(Directory.GetCurrentDirectory(), fullPath)).Close();
 250 |                     // AssetDatabase.ImportAsset(fullPath); // Let Unity try to import it
 251 |                     // newAsset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(fullPath);
 252 |                     return Response.Error(
 253 |                         $"Creation for asset type '{assetType}' is not explicitly supported yet. Supported: Folder, Material, ScriptableObject."
 254 |                     );
 255 |                 }
 256 | 
 257 |                 if (
 258 |                     newAsset == null
 259 |                     && !Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), fullPath))
 260 |                 ) // Check if it wasn't a folder and asset wasn't created
 261 |                 {
 262 |                     return Response.Error(
 263 |                         $"Failed to create asset '{assetType}' at '{fullPath}'. See logs for details."
 264 |                     );
 265 |                 }
 266 | 
 267 |                 AssetDatabase.SaveAssets();
 268 |                 // AssetDatabase.Refresh(); // CreateAsset often handles refresh
 269 |                 return Response.Success(
 270 |                     $"Asset '{fullPath}' created successfully.",
 271 |                     GetAssetData(fullPath)
 272 |                 );
 273 |             }
 274 |             catch (Exception e)
 275 |             {
 276 |                 return Response.Error($"Failed to create asset at '{fullPath}': {e.Message}");
 277 |             }
 278 |         }
 279 | 
 280 |         private static object CreateFolder(string path)
 281 |         {
 282 |             if (string.IsNullOrEmpty(path))
 283 |                 return Response.Error("'path' is required for create_folder.");
 284 |             string fullPath = AssetPathUtility.SanitizeAssetPath(path);
 285 |             string parentDir = Path.GetDirectoryName(fullPath);
 286 |             string folderName = Path.GetFileName(fullPath);
 287 | 
 288 |             if (AssetExists(fullPath))
 289 |             {
 290 |                 // Check if it's actually a folder already
 291 |                 if (AssetDatabase.IsValidFolder(fullPath))
 292 |                 {
 293 |                     return Response.Success(
 294 |                         $"Folder already exists at path: {fullPath}",
 295 |                         GetAssetData(fullPath)
 296 |                     );
 297 |                 }
 298 |                 else
 299 |                 {
 300 |                     return Response.Error(
 301 |                         $"An asset (not a folder) already exists at path: {fullPath}"
 302 |                     );
 303 |                 }
 304 |             }
 305 | 
 306 |             try
 307 |             {
 308 |                 // Ensure parent exists
 309 |                 if (!string.IsNullOrEmpty(parentDir) && !AssetDatabase.IsValidFolder(parentDir))
 310 |                 {
 311 |                     // Recursively create parent folders if needed (AssetDatabase handles this internally)
 312 |                     // Or we can do it manually: Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), parentDir)); AssetDatabase.Refresh();
 313 |                 }
 314 | 
 315 |                 string guid = AssetDatabase.CreateFolder(parentDir, folderName);
 316 |                 if (string.IsNullOrEmpty(guid))
 317 |                 {
 318 |                     return Response.Error(
 319 |                         $"Failed to create folder '{fullPath}'. Check logs and permissions."
 320 |                     );
 321 |                 }
 322 | 
 323 |                 // AssetDatabase.Refresh(); // CreateFolder usually handles refresh
 324 |                 return Response.Success(
 325 |                     $"Folder '{fullPath}' created successfully.",
 326 |                     GetAssetData(fullPath)
 327 |                 );
 328 |             }
 329 |             catch (Exception e)
 330 |             {
 331 |                 return Response.Error($"Failed to create folder '{fullPath}': {e.Message}");
 332 |             }
 333 |         }
 334 | 
 335 |         private static object ModifyAsset(string path, JObject properties)
 336 |         {
 337 |             if (string.IsNullOrEmpty(path))
 338 |                 return Response.Error("'path' is required for modify.");
 339 |             if (properties == null || !properties.HasValues)
 340 |                 return Response.Error("'properties' are required for modify.");
 341 | 
 342 |             string fullPath = AssetPathUtility.SanitizeAssetPath(path);
 343 |             if (!AssetExists(fullPath))
 344 |                 return Response.Error($"Asset not found at path: {fullPath}");
 345 | 
 346 |             try
 347 |             {
 348 |                 UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(
 349 |                     fullPath
 350 |                 );
 351 |                 if (asset == null)
 352 |                     return Response.Error($"Failed to load asset at path: {fullPath}");
 353 | 
 354 |                 bool modified = false; // Flag to track if any changes were made
 355 | 
 356 |                 // --- NEW: Handle GameObject / Prefab Component Modification ---
 357 |                 if (asset is GameObject gameObject)
 358 |                 {
 359 |                     // Iterate through the properties JSON: keys are component names, values are properties objects for that component
 360 |                     foreach (var prop in properties.Properties())
 361 |                     {
 362 |                         string componentName = prop.Name; // e.g., "Collectible"
 363 |                         // Check if the value associated with the component name is actually an object containing properties
 364 |                         if (
 365 |                             prop.Value is JObject componentProperties
 366 |                             && componentProperties.HasValues
 367 |                         ) // e.g., {"bobSpeed": 2.0}
 368 |                         {
 369 |                             // Resolve component type via ComponentResolver, then fetch by Type
 370 |                             Component targetComponent = null;
 371 |                             bool resolved = ComponentResolver.TryResolve(componentName, out var compType, out var compError);
 372 |                             if (resolved)
 373 |                             {
 374 |                                 targetComponent = gameObject.GetComponent(compType);
 375 |                             }
 376 | 
 377 |                             // Only warn about resolution failure if component also not found
 378 |                             if (targetComponent == null && !resolved)
 379 |                             {
 380 |                                 Debug.LogWarning(
 381 |                                     $"[ManageAsset.ModifyAsset] Failed to resolve component '{componentName}' on '{gameObject.name}': {compError}"
 382 |                                 );
 383 |                             }
 384 | 
 385 |                             if (targetComponent != null)
 386 |                             {
 387 |                                 // Apply the nested properties (e.g., bobSpeed) to the found component instance
 388 |                                 // Use |= to ensure 'modified' becomes true if any component is successfully modified
 389 |                                 modified |= ApplyObjectProperties(
 390 |                                     targetComponent,
 391 |                                     componentProperties
 392 |                                 );
 393 |                             }
 394 |                             else
 395 |                             {
 396 |                                 // Log a warning if a specified component couldn't be found
 397 |                                 Debug.LogWarning(
 398 |                                     $"[ManageAsset.ModifyAsset] Component '{componentName}' not found on GameObject '{gameObject.name}' in asset '{fullPath}'. Skipping modification for this component."
 399 |                                 );
 400 |                             }
 401 |                         }
 402 |                         else
 403 |                         {
 404 |                             // Log a warning if the structure isn't {"ComponentName": {"prop": value}}
 405 |                             // We could potentially try to apply this property directly to the GameObject here if needed,
 406 |                             // but the primary goal is component modification.
 407 |                             Debug.LogWarning(
 408 |                                 $"[ManageAsset.ModifyAsset] Property '{prop.Name}' for GameObject modification should have a JSON object value containing component properties. Value was: {prop.Value.Type}. Skipping."
 409 |                             );
 410 |                         }
 411 |                     }
 412 |                     // Note: 'modified' is now true if ANY component property was successfully changed.
 413 |                 }
 414 |                 // --- End NEW ---
 415 | 
 416 |                 // --- Existing logic for other asset types (now as else-if) ---
 417 |                 // Example: Modifying a Material
 418 |                 else if (asset is Material material)
 419 |                 {
 420 |                     // Apply properties directly to the material. If this modifies, it sets modified=true.
 421 |                     // Use |= in case the asset was already marked modified by previous logic (though unlikely here)
 422 |                     modified |= ApplyMaterialProperties(material, properties);
 423 |                 }
 424 |                 // Example: Modifying a ScriptableObject
 425 |                 else if (asset is ScriptableObject so)
 426 |                 {
 427 |                     // Apply properties directly to the ScriptableObject.
 428 |                     modified |= ApplyObjectProperties(so, properties); // General helper
 429 |                 }
 430 |                 // Example: Modifying TextureImporter settings
 431 |                 else if (asset is Texture)
 432 |                 {
 433 |                     AssetImporter importer = AssetImporter.GetAtPath(fullPath);
 434 |                     if (importer is TextureImporter textureImporter)
 435 |                     {
 436 |                         bool importerModified = ApplyObjectProperties(textureImporter, properties);
 437 |                         if (importerModified)
 438 |                         {
 439 |                             // Importer settings need saving and reimporting
 440 |                             AssetDatabase.WriteImportSettingsIfDirty(fullPath);
 441 |                             AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); // Reimport to apply changes
 442 |                             modified = true; // Mark overall operation as modified
 443 |                         }
 444 |                     }
 445 |                     else
 446 |                     {
 447 |                         Debug.LogWarning($"Could not get TextureImporter for {fullPath}.");
 448 |                     }
 449 |                 }
 450 |                 // TODO: Add modification logic for other common asset types (Models, AudioClips importers, etc.)
 451 |                 else // Fallback for other asset types OR direct properties on non-GameObject assets
 452 |                 {
 453 |                     // This block handles non-GameObject/Material/ScriptableObject/Texture assets.
 454 |                     // Attempts to apply properties directly to the asset itself.
 455 |                     Debug.LogWarning(
 456 |                         $"[ManageAsset.ModifyAsset] Asset type '{asset.GetType().Name}' at '{fullPath}' is not explicitly handled for component modification. Attempting generic property setting on the asset itself."
 457 |                     );
 458 |                     modified |= ApplyObjectProperties(asset, properties);
 459 |                 }
 460 |                 // --- End Existing Logic ---
 461 | 
 462 |                 // Check if any modification happened (either component or direct asset modification)
 463 |                 if (modified)
 464 |                 {
 465 |                     // Mark the asset as dirty (important for prefabs/SOs) so Unity knows to save it.
 466 |                     EditorUtility.SetDirty(asset);
 467 |                     // Save all modified assets to disk.
 468 |                     AssetDatabase.SaveAssets();
 469 |                     // Refresh might be needed in some edge cases, but SaveAssets usually covers it.
 470 |                     // AssetDatabase.Refresh();
 471 |                     return Response.Success(
 472 |                         $"Asset '{fullPath}' modified successfully.",
 473 |                         GetAssetData(fullPath)
 474 |                     );
 475 |                 }
 476 |                 else
 477 |                 {
 478 |                     // If no changes were made (e.g., component not found, property names incorrect, value unchanged), return a success message indicating nothing changed.
 479 |                     return Response.Success(
 480 |                         $"No applicable or modifiable properties found for asset '{fullPath}'. Check component names, property names, and values.",
 481 |                         GetAssetData(fullPath)
 482 |                     );
 483 |                     // Previous message: return Response.Success($"No applicable properties found to modify for asset '{fullPath}'.", GetAssetData(fullPath));
 484 |                 }
 485 |             }
 486 |             catch (Exception e)
 487 |             {
 488 |                 // Log the detailed error internally
 489 |                 Debug.LogError($"[ManageAsset] Action 'modify' failed for path '{path}': {e}");
 490 |                 // Return a user-friendly error message
 491 |                 return Response.Error($"Failed to modify asset '{fullPath}': {e.Message}");
 492 |             }
 493 |         }
 494 | 
 495 |         private static object DeleteAsset(string path)
 496 |         {
 497 |             if (string.IsNullOrEmpty(path))
 498 |                 return Response.Error("'path' is required for delete.");
 499 |             string fullPath = AssetPathUtility.SanitizeAssetPath(path);
 500 |             if (!AssetExists(fullPath))
 501 |                 return Response.Error($"Asset not found at path: {fullPath}");
 502 | 
 503 |             try
 504 |             {
 505 |                 bool success = AssetDatabase.DeleteAsset(fullPath);
 506 |                 if (success)
 507 |                 {
 508 |                     // AssetDatabase.Refresh(); // DeleteAsset usually handles refresh
 509 |                     return Response.Success($"Asset '{fullPath}' deleted successfully.");
 510 |                 }
 511 |                 else
 512 |                 {
 513 |                     // This might happen if the file couldn't be deleted (e.g., locked)
 514 |                     return Response.Error(
 515 |                         $"Failed to delete asset '{fullPath}'. Check logs or if the file is locked."
 516 |                     );
 517 |                 }
 518 |             }
 519 |             catch (Exception e)
 520 |             {
 521 |                 return Response.Error($"Error deleting asset '{fullPath}': {e.Message}");
 522 |             }
 523 |         }
 524 | 
 525 |         private static object DuplicateAsset(string path, string destinationPath)
 526 |         {
 527 |             if (string.IsNullOrEmpty(path))
 528 |                 return Response.Error("'path' is required for duplicate.");
 529 | 
 530 |             string sourcePath = AssetPathUtility.SanitizeAssetPath(path);
 531 |             if (!AssetExists(sourcePath))
 532 |                 return Response.Error($"Source asset not found at path: {sourcePath}");
 533 | 
 534 |             string destPath;
 535 |             if (string.IsNullOrEmpty(destinationPath))
 536 |             {
 537 |                 // Generate a unique path if destination is not provided
 538 |                 destPath = AssetDatabase.GenerateUniqueAssetPath(sourcePath);
 539 |             }
 540 |             else
 541 |             {
 542 |                 destPath = AssetPathUtility.SanitizeAssetPath(destinationPath);
 543 |                 if (AssetExists(destPath))
 544 |                     return Response.Error($"Asset already exists at destination path: {destPath}");
 545 |                 // Ensure destination directory exists
 546 |                 EnsureDirectoryExists(Path.GetDirectoryName(destPath));
 547 |             }
 548 | 
 549 |             try
 550 |             {
 551 |                 bool success = AssetDatabase.CopyAsset(sourcePath, destPath);
 552 |                 if (success)
 553 |                 {
 554 |                     // AssetDatabase.Refresh();
 555 |                     return Response.Success(
 556 |                         $"Asset '{sourcePath}' duplicated to '{destPath}'.",
 557 |                         GetAssetData(destPath)
 558 |                     );
 559 |                 }
 560 |                 else
 561 |                 {
 562 |                     return Response.Error(
 563 |                         $"Failed to duplicate asset from '{sourcePath}' to '{destPath}'."
 564 |                     );
 565 |                 }
 566 |             }
 567 |             catch (Exception e)
 568 |             {
 569 |                 return Response.Error($"Error duplicating asset '{sourcePath}': {e.Message}");
 570 |             }
 571 |         }
 572 | 
 573 |         private static object MoveOrRenameAsset(string path, string destinationPath)
 574 |         {
 575 |             if (string.IsNullOrEmpty(path))
 576 |                 return Response.Error("'path' is required for move/rename.");
 577 |             if (string.IsNullOrEmpty(destinationPath))
 578 |                 return Response.Error("'destination' path is required for move/rename.");
 579 | 
 580 |             string sourcePath = AssetPathUtility.SanitizeAssetPath(path);
 581 |             string destPath = AssetPathUtility.SanitizeAssetPath(destinationPath);
 582 | 
 583 |             if (!AssetExists(sourcePath))
 584 |                 return Response.Error($"Source asset not found at path: {sourcePath}");
 585 |             if (AssetExists(destPath))
 586 |                 return Response.Error(
 587 |                     $"An asset already exists at the destination path: {destPath}"
 588 |                 );
 589 | 
 590 |             // Ensure destination directory exists
 591 |             EnsureDirectoryExists(Path.GetDirectoryName(destPath));
 592 | 
 593 |             try
 594 |             {
 595 |                 // Validate will return an error string if failed, null if successful
 596 |                 string error = AssetDatabase.ValidateMoveAsset(sourcePath, destPath);
 597 |                 if (!string.IsNullOrEmpty(error))
 598 |                 {
 599 |                     return Response.Error(
 600 |                         $"Failed to move/rename asset from '{sourcePath}' to '{destPath}': {error}"
 601 |                     );
 602 |                 }
 603 | 
 604 |                 string guid = AssetDatabase.MoveAsset(sourcePath, destPath);
 605 |                 if (!string.IsNullOrEmpty(guid)) // MoveAsset returns the new GUID on success
 606 |                 {
 607 |                     // AssetDatabase.Refresh(); // MoveAsset usually handles refresh
 608 |                     return Response.Success(
 609 |                         $"Asset moved/renamed from '{sourcePath}' to '{destPath}'.",
 610 |                         GetAssetData(destPath)
 611 |                     );
 612 |                 }
 613 |                 else
 614 |                 {
 615 |                     // This case might not be reachable if ValidateMoveAsset passes, but good to have
 616 |                     return Response.Error(
 617 |                         $"MoveAsset call failed unexpectedly for '{sourcePath}' to '{destPath}'."
 618 |                     );
 619 |                 }
 620 |             }
 621 |             catch (Exception e)
 622 |             {
 623 |                 return Response.Error($"Error moving/renaming asset '{sourcePath}': {e.Message}");
 624 |             }
 625 |         }
 626 | 
 627 |         private static object SearchAssets(JObject @params)
 628 |         {
 629 |             string searchPattern = @params["searchPattern"]?.ToString();
 630 |             string filterType = @params["filterType"]?.ToString();
 631 |             string pathScope = @params["path"]?.ToString(); // Use path as folder scope
 632 |             string filterDateAfterStr = @params["filterDateAfter"]?.ToString();
 633 |             int pageSize = @params["pageSize"]?.ToObject<int?>() ?? 50; // Default page size
 634 |             int pageNumber = @params["pageNumber"]?.ToObject<int?>() ?? 1; // Default page number (1-based)
 635 |             bool generatePreview = @params["generatePreview"]?.ToObject<bool>() ?? false;
 636 | 
 637 |             List<string> searchFilters = new List<string>();
 638 |             if (!string.IsNullOrEmpty(searchPattern))
 639 |                 searchFilters.Add(searchPattern);
 640 |             if (!string.IsNullOrEmpty(filterType))
 641 |                 searchFilters.Add($"t:{filterType}");
 642 | 
 643 |             string[] folderScope = null;
 644 |             if (!string.IsNullOrEmpty(pathScope))
 645 |             {
 646 |                 folderScope = new string[] { AssetPathUtility.SanitizeAssetPath(pathScope) };
 647 |                 if (!AssetDatabase.IsValidFolder(folderScope[0]))
 648 |                 {
 649 |                     // Maybe the user provided a file path instead of a folder?
 650 |                     // We could search in the containing folder, or return an error.
 651 |                     Debug.LogWarning(
 652 |                         $"Search path '{folderScope[0]}' is not a valid folder. Searching entire project."
 653 |                     );
 654 |                     folderScope = null; // Search everywhere if path isn't a folder
 655 |                 }
 656 |             }
 657 | 
 658 |             DateTime? filterDateAfter = null;
 659 |             if (!string.IsNullOrEmpty(filterDateAfterStr))
 660 |             {
 661 |                 if (
 662 |                     DateTime.TryParse(
 663 |                         filterDateAfterStr,
 664 |                         CultureInfo.InvariantCulture,
 665 |                         DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
 666 |                         out DateTime parsedDate
 667 |                     )
 668 |                 )
 669 |                 {
 670 |                     filterDateAfter = parsedDate;
 671 |                 }
 672 |                 else
 673 |                 {
 674 |                     Debug.LogWarning(
 675 |                         $"Could not parse filterDateAfter: '{filterDateAfterStr}'. Expected ISO 8601 format."
 676 |                     );
 677 |                 }
 678 |             }
 679 | 
 680 |             try
 681 |             {
 682 |                 string[] guids = AssetDatabase.FindAssets(
 683 |                     string.Join(" ", searchFilters),
 684 |                     folderScope
 685 |                 );
 686 |                 List<object> results = new List<object>();
 687 |                 int totalFound = 0;
 688 | 
 689 |                 foreach (string guid in guids)
 690 |                 {
 691 |                     string assetPath = AssetDatabase.GUIDToAssetPath(guid);
 692 |                     if (string.IsNullOrEmpty(assetPath))
 693 |                         continue;
 694 | 
 695 |                     // Apply date filter if present
 696 |                     if (filterDateAfter.HasValue)
 697 |                     {
 698 |                         DateTime lastWriteTime = File.GetLastWriteTimeUtc(
 699 |                             Path.Combine(Directory.GetCurrentDirectory(), assetPath)
 700 |                         );
 701 |                         if (lastWriteTime <= filterDateAfter.Value)
 702 |                         {
 703 |                             continue; // Skip assets older than or equal to the filter date
 704 |                         }
 705 |                     }
 706 | 
 707 |                     totalFound++; // Count matching assets before pagination
 708 |                     results.Add(GetAssetData(assetPath, generatePreview));
 709 |                 }
 710 | 
 711 |                 // Apply pagination
 712 |                 int startIndex = (pageNumber - 1) * pageSize;
 713 |                 var pagedResults = results.Skip(startIndex).Take(pageSize).ToList();
 714 | 
 715 |                 return Response.Success(
 716 |                     $"Found {totalFound} asset(s). Returning page {pageNumber} ({pagedResults.Count} assets).",
 717 |                     new
 718 |                     {
 719 |                         totalAssets = totalFound,
 720 |                         pageSize = pageSize,
 721 |                         pageNumber = pageNumber,
 722 |                         assets = pagedResults,
 723 |                     }
 724 |                 );
 725 |             }
 726 |             catch (Exception e)
 727 |             {
 728 |                 return Response.Error($"Error searching assets: {e.Message}");
 729 |             }
 730 |         }
 731 | 
 732 |         private static object GetAssetInfo(string path, bool generatePreview)
 733 |         {
 734 |             if (string.IsNullOrEmpty(path))
 735 |                 return Response.Error("'path' is required for get_info.");
 736 |             string fullPath = AssetPathUtility.SanitizeAssetPath(path);
 737 |             if (!AssetExists(fullPath))
 738 |                 return Response.Error($"Asset not found at path: {fullPath}");
 739 | 
 740 |             try
 741 |             {
 742 |                 return Response.Success(
 743 |                     "Asset info retrieved.",
 744 |                     GetAssetData(fullPath, generatePreview)
 745 |                 );
 746 |             }
 747 |             catch (Exception e)
 748 |             {
 749 |                 return Response.Error($"Error getting info for asset '{fullPath}': {e.Message}");
 750 |             }
 751 |         }
 752 | 
 753 |         /// <summary>
 754 |         /// Retrieves components attached to a GameObject asset (like a Prefab).
 755 |         /// </summary>
 756 |         /// <param name="path">The asset path of the GameObject or Prefab.</param>
 757 |         /// <returns>A response object containing a list of component type names or an error.</returns>
 758 |         private static object GetComponentsFromAsset(string path)
 759 |         {
 760 |             // 1. Validate input path
 761 |             if (string.IsNullOrEmpty(path))
 762 |                 return Response.Error("'path' is required for get_components.");
 763 | 
 764 |             // 2. Sanitize and check existence
 765 |             string fullPath = AssetPathUtility.SanitizeAssetPath(path);
 766 |             if (!AssetExists(fullPath))
 767 |                 return Response.Error($"Asset not found at path: {fullPath}");
 768 | 
 769 |             try
 770 |             {
 771 |                 // 3. Load the asset
 772 |                 UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(
 773 |                     fullPath
 774 |                 );
 775 |                 if (asset == null)
 776 |                     return Response.Error($"Failed to load asset at path: {fullPath}");
 777 | 
 778 |                 // 4. Check if it's a GameObject (Prefabs load as GameObjects)
 779 |                 GameObject gameObject = asset as GameObject;
 780 |                 if (gameObject == null)
 781 |                 {
 782 |                     // Also check if it's *directly* a Component type (less common for primary assets)
 783 |                     Component componentAsset = asset as Component;
 784 |                     if (componentAsset != null)
 785 |                     {
 786 |                         // If the asset itself *is* a component, maybe return just its info?
 787 |                         // This is an edge case. Let's stick to GameObjects for now.
 788 |                         return Response.Error(
 789 |                             $"Asset at '{fullPath}' is a Component ({asset.GetType().FullName}), not a GameObject. Components are typically retrieved *from* a GameObject."
 790 |                         );
 791 |                     }
 792 |                     return Response.Error(
 793 |                         $"Asset at '{fullPath}' is not a GameObject (Type: {asset.GetType().FullName}). Cannot get components from this asset type."
 794 |                     );
 795 |                 }
 796 | 
 797 |                 // 5. Get components
 798 |                 Component[] components = gameObject.GetComponents<Component>();
 799 | 
 800 |                 // 6. Format component data
 801 |                 List<object> componentList = components
 802 |                     .Select(comp => new
 803 |                     {
 804 |                         typeName = comp.GetType().FullName,
 805 |                         instanceID = comp.GetInstanceID(),
 806 |                         // TODO: Add more component-specific details here if needed in the future?
 807 |                         //       Requires reflection or specific handling per component type.
 808 |                     })
 809 |                     .ToList<object>(); // Explicit cast for clarity if needed
 810 | 
 811 |                 // 7. Return success response
 812 |                 return Response.Success(
 813 |                     $"Found {componentList.Count} component(s) on asset '{fullPath}'.",
 814 |                     componentList
 815 |                 );
 816 |             }
 817 |             catch (Exception e)
 818 |             {
 819 |                 Debug.LogError(
 820 |                     $"[ManageAsset.GetComponentsFromAsset] Error getting components for '{fullPath}': {e}"
 821 |                 );
 822 |                 return Response.Error(
 823 |                     $"Error getting components for asset '{fullPath}': {e.Message}"
 824 |                 );
 825 |             }
 826 |         }
 827 | 
 828 |         // --- Internal Helpers ---
 829 | 
 830 |         /// <summary>
 831 |         /// Ensures the asset path starts with "Assets/".
 832 |         /// </summary>
 833 |         /// <summary>
 834 |         /// Checks if an asset exists at the given path (file or folder).
 835 |         /// </summary>
 836 |         private static bool AssetExists(string sanitizedPath)
 837 |         {
 838 |             // AssetDatabase APIs are generally preferred over raw File/Directory checks for assets.
 839 |             // Check if it's a known asset GUID.
 840 |             if (!string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(sanitizedPath)))
 841 |             {
 842 |                 return true;
 843 |             }
 844 |             // AssetPathToGUID might not work for newly created folders not yet refreshed.
 845 |             // Check directory explicitly for folders.
 846 |             if (Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), sanitizedPath)))
 847 |             {
 848 |                 // Check if it's considered a *valid* folder by Unity
 849 |                 return AssetDatabase.IsValidFolder(sanitizedPath);
 850 |             }
 851 |             // Check file existence for non-folder assets.
 852 |             if (File.Exists(Path.Combine(Directory.GetCurrentDirectory(), sanitizedPath)))
 853 |             {
 854 |                 return true; // Assume if file exists, it's an asset or will be imported
 855 |             }
 856 | 
 857 |             return false;
 858 |             // Alternative: return !string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(sanitizedPath));
 859 |         }
 860 | 
 861 |         /// <summary>
 862 |         /// Ensures the directory for a given asset path exists, creating it if necessary.
 863 |         /// </summary>
 864 |         private static void EnsureDirectoryExists(string directoryPath)
 865 |         {
 866 |             if (string.IsNullOrEmpty(directoryPath))
 867 |                 return;
 868 |             string fullDirPath = Path.Combine(Directory.GetCurrentDirectory(), directoryPath);
 869 |             if (!Directory.Exists(fullDirPath))
 870 |             {
 871 |                 Directory.CreateDirectory(fullDirPath);
 872 |                 AssetDatabase.Refresh(); // Let Unity know about the new folder
 873 |             }
 874 |         }
 875 | 
 876 |         /// <summary>
 877 |         /// Applies properties from JObject to a Material.
 878 |         /// </summary>
 879 |         private static bool ApplyMaterialProperties(Material mat, JObject properties)
 880 |         {
 881 |             if (mat == null || properties == null)
 882 |                 return false;
 883 |             bool modified = false;
 884 | 
 885 |             // Example: Set shader
 886 |             if (properties["shader"]?.Type == JTokenType.String)
 887 |             {
 888 |                 Shader newShader = Shader.Find(properties["shader"].ToString());
 889 |                 if (newShader != null && mat.shader != newShader)
 890 |                 {
 891 |                     mat.shader = newShader;
 892 |                     modified = true;
 893 |                 }
 894 |             }
 895 |             // Example: Set color property
 896 |             if (properties["color"] is JObject colorProps)
 897 |             {
 898 |                 string propName = colorProps["name"]?.ToString() ?? "_Color"; // Default main color
 899 |                 if (colorProps["value"] is JArray colArr && colArr.Count >= 3)
 900 |                 {
 901 |                     try
 902 |                     {
 903 |                         Color newColor = new Color(
 904 |                             colArr[0].ToObject<float>(),
 905 |                             colArr[1].ToObject<float>(),
 906 |                             colArr[2].ToObject<float>(),
 907 |                             colArr.Count > 3 ? colArr[3].ToObject<float>() : 1.0f
 908 |                         );
 909 |                         if (mat.HasProperty(propName) && mat.GetColor(propName) != newColor)
 910 |                         {
 911 |                             mat.SetColor(propName, newColor);
 912 |                             modified = true;
 913 |                         }
 914 |                     }
 915 |                     catch (Exception ex)
 916 |                     {
 917 |                         Debug.LogWarning(
 918 |                             $"Error parsing color property '{propName}': {ex.Message}"
 919 |                         );
 920 |                     }
 921 |                 }
 922 |             }
 923 |             else if (properties["color"] is JArray colorArr) //Use color now with examples set in manage_asset.py
 924 |             {
 925 |                 string propName = "_Color";
 926 |                 try
 927 |                 {
 928 |                     if (colorArr.Count >= 3)
 929 |                     {
 930 |                         Color newColor = new Color(
 931 |                             colorArr[0].ToObject<float>(),
 932 |                             colorArr[1].ToObject<float>(),
 933 |                             colorArr[2].ToObject<float>(),
 934 |                             colorArr.Count > 3 ? colorArr[3].ToObject<float>() : 1.0f
 935 |                         );
 936 |                         if (mat.HasProperty(propName) && mat.GetColor(propName) != newColor)
 937 |                         {
 938 |                             mat.SetColor(propName, newColor);
 939 |                             modified = true;
 940 |                         }
 941 |                     }
 942 |                 }
 943 |                 catch (Exception ex)
 944 |                 {
 945 |                     Debug.LogWarning(
 946 |                         $"Error parsing color property '{propName}': {ex.Message}"
 947 |                     );
 948 |                 }
 949 |             }
 950 |             // Example: Set float property
 951 |             if (properties["float"] is JObject floatProps)
 952 |             {
 953 |                 string propName = floatProps["name"]?.ToString();
 954 |                 if (
 955 |                     !string.IsNullOrEmpty(propName) &&
 956 |                     (floatProps["value"]?.Type == JTokenType.Float || floatProps["value"]?.Type == JTokenType.Integer)
 957 |                 )
 958 |                 {
 959 |                     try
 960 |                     {
 961 |                         float newVal = floatProps["value"].ToObject<float>();
 962 |                         if (mat.HasProperty(propName) && mat.GetFloat(propName) != newVal)
 963 |                         {
 964 |                             mat.SetFloat(propName, newVal);
 965 |                             modified = true;
 966 |                         }
 967 |                     }
 968 |                     catch (Exception ex)
 969 |                     {
 970 |                         Debug.LogWarning(
 971 |                             $"Error parsing float property '{propName}': {ex.Message}"
 972 |                         );
 973 |                     }
 974 |                 }
 975 |             }
 976 |             // Example: Set texture property
 977 |             if (properties["texture"] is JObject texProps)
 978 |             {
 979 |                 string propName = texProps["name"]?.ToString() ?? "_MainTex"; // Default main texture
 980 |                 string texPath = texProps["path"]?.ToString();
 981 |                 if (!string.IsNullOrEmpty(texPath))
 982 |                 {
 983 |                     Texture newTex = AssetDatabase.LoadAssetAtPath<Texture>(
 984 |                         AssetPathUtility.SanitizeAssetPath(texPath)
 985 |                     );
 986 |                     if (
 987 |                         newTex != null
 988 |                         && mat.HasProperty(propName)
 989 |                         && mat.GetTexture(propName) != newTex
 990 |                     )
 991 |                     {
 992 |                         mat.SetTexture(propName, newTex);
 993 |                         modified = true;
 994 |                     }
 995 |                     else if (newTex == null)
 996 |                     {
 997 |                         Debug.LogWarning($"Texture not found at path: {texPath}");
 998 |                     }
 999 |                 }
1000 |             }
1001 | 
1002 |             // TODO: Add handlers for other property types (Vectors, Ints, Keywords, RenderQueue, etc.)
1003 |             return modified;
1004 |         }
1005 | 
1006 |         /// <summary>
1007 |         ///  Applies properties from JObject to a PhysicsMaterial.
1008 |         /// </summary>
1009 |         private static bool ApplyPhysicsMaterialProperties(PhysicsMaterialType pmat, JObject properties)
1010 |         {
1011 |             if (pmat == null || properties == null)
1012 |                 return false;
1013 |             bool modified = false;
1014 | 
1015 |             // Example: Set dynamic friction
1016 |             if (properties["dynamicFriction"]?.Type == JTokenType.Float)
1017 |             {
1018 |                 float dynamicFriction = properties["dynamicFriction"].ToObject<float>();
1019 |                 pmat.dynamicFriction = dynamicFriction;
1020 |                 modified = true;
1021 |             }
1022 | 
1023 |             // Example: Set static friction
1024 |             if (properties["staticFriction"]?.Type == JTokenType.Float)
1025 |             {
1026 |                 float staticFriction = properties["staticFriction"].ToObject<float>();
1027 |                 pmat.staticFriction = staticFriction;
1028 |                 modified = true;
1029 |             }
1030 | 
1031 |             // Example: Set bounciness
1032 |             if (properties["bounciness"]?.Type == JTokenType.Float)
1033 |             {
1034 |                 float bounciness = properties["bounciness"].ToObject<float>();
1035 |                 pmat.bounciness = bounciness;
1036 |                 modified = true;
1037 |             }
1038 | 
1039 |             List<String> averageList = new List<String> { "ave", "Ave", "average", "Average" };
1040 |             List<String> multiplyList = new List<String> { "mul", "Mul", "mult", "Mult", "multiply", "Multiply" };
1041 |             List<String> minimumList = new List<String> { "min", "Min", "minimum", "Minimum" };
1042 |             List<String> maximumList = new List<String> { "max", "Max", "maximum", "Maximum" };
1043 | 
1044 |             // Example: Set friction combine
1045 |             if (properties["frictionCombine"]?.Type == JTokenType.String)
1046 |             {
1047 |                 string frictionCombine = properties["frictionCombine"].ToString();
1048 |                 if (averageList.Contains(frictionCombine))
1049 |                     pmat.frictionCombine = PhysicsMaterialCombine.Average;
1050 |                 else if (multiplyList.Contains(frictionCombine))
1051 |                     pmat.frictionCombine = PhysicsMaterialCombine.Multiply;
1052 |                 else if (minimumList.Contains(frictionCombine))
1053 |                     pmat.frictionCombine = PhysicsMaterialCombine.Minimum;
1054 |                 else if (maximumList.Contains(frictionCombine))
1055 |                     pmat.frictionCombine = PhysicsMaterialCombine.Maximum;
1056 |                 modified = true;
1057 |             }
1058 | 
1059 |             // Example: Set bounce combine
1060 |             if (properties["bounceCombine"]?.Type == JTokenType.String)
1061 |             {
1062 |                 string bounceCombine = properties["bounceCombine"].ToString();
1063 |                 if (averageList.Contains(bounceCombine))
1064 |                     pmat.bounceCombine = PhysicsMaterialCombine.Average;
1065 |                 else if (multiplyList.Contains(bounceCombine))
1066 |                     pmat.bounceCombine = PhysicsMaterialCombine.Multiply;
1067 |                 else if (minimumList.Contains(bounceCombine))
1068 |                     pmat.bounceCombine = PhysicsMaterialCombine.Minimum;
1069 |                 else if (maximumList.Contains(bounceCombine))
1070 |                     pmat.bounceCombine = PhysicsMaterialCombine.Maximum;
1071 |                 modified = true;
1072 |             }
1073 | 
1074 |             return modified;
1075 |         }
1076 | 
1077 |         /// <summary>
1078 |         /// Generic helper to set properties on any UnityEngine.Object using reflection.
1079 |         /// </summary>
1080 |         private static bool ApplyObjectProperties(UnityEngine.Object target, JObject properties)
1081 |         {
1082 |             if (target == null || properties == null)
1083 |                 return false;
1084 |             bool modified = false;
1085 |             Type type = target.GetType();
1086 | 
1087 |             foreach (var prop in properties.Properties())
1088 |             {
1089 |                 string propName = prop.Name;
1090 |                 JToken propValue = prop.Value;
1091 |                 if (SetPropertyOrField(target, propName, propValue, type))
1092 |                 {
1093 |                     modified = true;
1094 |                 }
1095 |             }
1096 |             return modified;
1097 |         }
1098 | 
1099 |         /// <summary>
1100 |         /// Helper to set a property or field via reflection, handling basic types and Unity objects.
1101 |         /// </summary>
1102 |         private static bool SetPropertyOrField(
1103 |             object target,
1104 |             string memberName,
1105 |             JToken value,
1106 |             Type type = null
1107 |         )
1108 |         {
1109 |             type = type ?? target.GetType();
1110 |             System.Reflection.BindingFlags flags =
1111 |                 System.Reflection.BindingFlags.Public
1112 |                 | System.Reflection.BindingFlags.Instance
1113 |                 | System.Reflection.BindingFlags.IgnoreCase;
1114 | 
1115 |             try
1116 |             {
1117 |                 System.Reflection.PropertyInfo propInfo = type.GetProperty(memberName, flags);
1118 |                 if (propInfo != null && propInfo.CanWrite)
1119 |                 {
1120 |                     object convertedValue = ConvertJTokenToType(value, propInfo.PropertyType);
1121 |                     if (
1122 |                         convertedValue != null
1123 |                         && !object.Equals(propInfo.GetValue(target), convertedValue)
1124 |                     )
1125 |                     {
1126 |                         propInfo.SetValue(target, convertedValue);
1127 |                         return true;
1128 |                     }
1129 |                 }
1130 |                 else
1131 |                 {
1132 |                     System.Reflection.FieldInfo fieldInfo = type.GetField(memberName, flags);
1133 |                     if (fieldInfo != null)
1134 |                     {
1135 |                         object convertedValue = ConvertJTokenToType(value, fieldInfo.FieldType);
1136 |                         if (
1137 |                             convertedValue != null
1138 |                             && !object.Equals(fieldInfo.GetValue(target), convertedValue)
1139 |                         )
1140 |                         {
1141 |                             fieldInfo.SetValue(target, convertedValue);
1142 |                             return true;
1143 |                         }
1144 |                     }
1145 |                 }
1146 |             }
1147 |             catch (Exception ex)
1148 |             {
1149 |                 Debug.LogWarning(
1150 |                     $"[SetPropertyOrField] Failed to set '{memberName}' on {type.Name}: {ex.Message}"
1151 |                 );
1152 |             }
1153 |             return false;
1154 |         }
1155 | 
1156 |         /// <summary>
1157 |         /// Simple JToken to Type conversion for common Unity types and primitives.
1158 |         /// </summary>
1159 |         private static object ConvertJTokenToType(JToken token, Type targetType)
1160 |         {
1161 |             try
1162 |             {
1163 |                 if (token == null || token.Type == JTokenType.Null)
1164 |                     return null;
1165 | 
1166 |                 if (targetType == typeof(string))
1167 |                     return token.ToObject<string>();
1168 |                 if (targetType == typeof(int))
1169 |                     return token.ToObject<int>();
1170 |                 if (targetType == typeof(float))
1171 |                     return token.ToObject<float>();
1172 |                 if (targetType == typeof(bool))
1173 |                     return token.ToObject<bool>();
1174 |                 if (targetType == typeof(Vector2) && token is JArray arrV2 && arrV2.Count == 2)
1175 |                     return new Vector2(arrV2[0].ToObject<float>(), arrV2[1].ToObject<float>());
1176 |                 if (targetType == typeof(Vector3) && token is JArray arrV3 && arrV3.Count == 3)
1177 |                     return new Vector3(
1178 |                         arrV3[0].ToObject<float>(),
1179 |                         arrV3[1].ToObject<float>(),
1180 |                         arrV3[2].ToObject<float>()
1181 |                     );
1182 |                 if (targetType == typeof(Vector4) && token is JArray arrV4 && arrV4.Count == 4)
1183 |                     return new Vector4(
1184 |                         arrV4[0].ToObject<float>(),
1185 |                         arrV4[1].ToObject<float>(),
1186 |                         arrV4[2].ToObject<float>(),
1187 |                         arrV4[3].ToObject<float>()
1188 |                     );
1189 |                 if (targetType == typeof(Quaternion) && token is JArray arrQ && arrQ.Count == 4)
1190 |                     return new Quaternion(
1191 |                         arrQ[0].ToObject<float>(),
1192 |                         arrQ[1].ToObject<float>(),
1193 |                         arrQ[2].ToObject<float>(),
1194 |                         arrQ[3].ToObject<float>()
1195 |                     );
1196 |                 if (targetType == typeof(Color) && token is JArray arrC && arrC.Count >= 3) // Allow RGB or RGBA
1197 |                     return new Color(
1198 |                         arrC[0].ToObject<float>(),
1199 |                         arrC[1].ToObject<float>(),
1200 |                         arrC[2].ToObject<float>(),
1201 |                         arrC.Count > 3 ? arrC[3].ToObject<float>() : 1.0f
1202 |                     );
1203 |                 if (targetType.IsEnum)
1204 |                     return Enum.Parse(targetType, token.ToString(), true); // Case-insensitive enum parsing
1205 | 
1206 |                 // Handle loading Unity Objects (Materials, Textures, etc.) by path
1207 |                 if (
1208 |                     typeof(UnityEngine.Object).IsAssignableFrom(targetType)
1209 |                     && token.Type == JTokenType.String
1210 |                 )
1211 |                 {
1212 |                     string assetPath = AssetPathUtility.SanitizeAssetPath(token.ToString());
1213 |                     UnityEngine.Object loadedAsset = AssetDatabase.LoadAssetAtPath(
1214 |                         assetPath,
1215 |                         targetType
1216 |                     );
1217 |                     if (loadedAsset == null)
1218 |                     {
1219 |                         Debug.LogWarning(
1220 |                             $"[ConvertJTokenToType] Could not load asset of type {targetType.Name} from path: {assetPath}"
1221 |                         );
1222 |                     }
1223 |                     return loadedAsset;
1224 |                 }
1225 | 
1226 |                 // Fallback: Try direct conversion (might work for other simple value types)
1227 |                 return token.ToObject(targetType);
1228 |             }
1229 |             catch (Exception ex)
1230 |             {
1231 |                 Debug.LogWarning(
1232 |                     $"[ConvertJTokenToType] Could not convert JToken '{token}' (type {token.Type}) to type '{targetType.Name}': {ex.Message}"
1233 |                 );
1234 |                 return null;
1235 |             }
1236 |         }
1237 | 
1238 | 
1239 |         // --- Data Serialization ---
1240 | 
1241 |         /// <summary>
1242 |         /// Creates a serializable representation of an asset.
1243 |         /// </summary>
1244 |         private static object GetAssetData(string path, bool generatePreview = false)
1245 |         {
1246 |             if (string.IsNullOrEmpty(path) || !AssetExists(path))
1247 |                 return null;
1248 | 
1249 |             string guid = AssetDatabase.AssetPathToGUID(path);
1250 |             Type assetType = AssetDatabase.GetMainAssetTypeAtPath(path);
1251 |             UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
1252 |             string previewBase64 = null;
1253 |             int previewWidth = 0;
1254 |             int previewHeight = 0;
1255 | 
1256 |             if (generatePreview && asset != null)
1257 |             {
1258 |                 Texture2D preview = AssetPreview.GetAssetPreview(asset);
1259 | 
1260 |                 if (preview != null)
1261 |                 {
1262 |                     try
1263 |                     {
1264 |                         // Ensure texture is readable for EncodeToPNG
1265 |                         // Creating a temporary readable copy is safer
1266 |                         RenderTexture rt = null;
1267 |                         Texture2D readablePreview = null;
1268 |                         RenderTexture previous = RenderTexture.active;
1269 |                         try
1270 |                         {
1271 |                             rt = RenderTexture.GetTemporary(preview.width, preview.height);
1272 |                             Graphics.Blit(preview, rt);
1273 |                             RenderTexture.active = rt;
1274 |                             readablePreview = new Texture2D(preview.width, preview.height, TextureFormat.RGB24, false);
1275 |                             readablePreview.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
1276 |                             readablePreview.Apply();
1277 | 
1278 |                             var pngData = readablePreview.EncodeToPNG();
1279 |                             if (pngData != null && pngData.Length > 0)
1280 |                             {
1281 |                                 previewBase64 = Convert.ToBase64String(pngData);
1282 |                                 previewWidth = readablePreview.width;
1283 |                                 previewHeight = readablePreview.height;
1284 |                             }
1285 |                         }
1286 |                         finally
1287 |                         {
1288 |                             RenderTexture.active = previous;
1289 |                             if (rt != null) RenderTexture.ReleaseTemporary(rt);
1290 |                             if (readablePreview != null) UnityEngine.Object.DestroyImmediate(readablePreview);
1291 |                         }
1292 |                     }
1293 |                     catch (Exception ex)
1294 |                     {
1295 |                         Debug.LogWarning(
1296 |                             $"Failed to generate readable preview for '{path}': {ex.Message}. Preview might not be readable."
1297 |                         );
1298 |                         // Fallback: Try getting static preview if available?
1299 |                         // Texture2D staticPreview = AssetPreview.GetMiniThumbnail(asset);
1300 |                     }
1301 |                 }
1302 |                 else
1303 |                 {
1304 |                     Debug.LogWarning(
1305 |                         $"Could not get asset preview for {path} (Type: {assetType?.Name}). Is it supported?"
1306 |                     );
1307 |                 }
1308 |             }
1309 | 
1310 |             return new
1311 |             {
1312 |                 path = path,
1313 |                 guid = guid,
1314 |                 assetType = assetType?.FullName ?? "Unknown",
1315 |                 name = Path.GetFileNameWithoutExtension(path),
1316 |                 fileName = Path.GetFileName(path),
1317 |                 isFolder = AssetDatabase.IsValidFolder(path),
1318 |                 instanceID = asset?.GetInstanceID() ?? 0,
1319 |                 lastWriteTimeUtc = File.GetLastWriteTimeUtc(
1320 |                         Path.Combine(Directory.GetCurrentDirectory(), path)
1321 |                     )
1322 |                     .ToString("o"), // ISO 8601
1323 |                 // --- Preview Data ---
1324 |                 previewBase64 = previewBase64, // PNG data as Base64 string
1325 |                 previewWidth = previewWidth,
1326 |                 previewHeight = previewHeight,
1327 |                 // TODO: Add more metadata? Importer settings? Dependencies?
1328 |             };
1329 |         }
1330 |     }
1331 | }
1332 | 
```

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

```csharp
   1 | using System;
   2 | using System.Collections.Generic;
   3 | using System.Globalization;
   4 | using System.IO;
   5 | using System.Linq;
   6 | using Newtonsoft.Json.Linq;
   7 | using UnityEditor;
   8 | using UnityEngine;
   9 | using MCPForUnity.Editor.Helpers; // For Response class
  10 | using static MCPForUnity.Editor.Tools.ManageGameObject;
  11 | 
  12 | #if UNITY_6000_0_OR_NEWER
  13 | using PhysicsMaterialType = UnityEngine.PhysicsMaterial;
  14 | using PhysicsMaterialCombine = UnityEngine.PhysicsMaterialCombine;  
  15 | #else
  16 | using PhysicsMaterialType = UnityEngine.PhysicMaterial;
  17 | using PhysicsMaterialCombine = UnityEngine.PhysicMaterialCombine;
  18 | #endif
  19 | 
  20 | namespace MCPForUnity.Editor.Tools
  21 | {
  22 |     /// <summary>
  23 |     /// Handles asset management operations within the Unity project.
  24 |     /// </summary>
  25 |     [McpForUnityTool("manage_asset")]
  26 |     public static class ManageAsset
  27 |     {
  28 |         // --- Main Handler ---
  29 | 
  30 |         // Define the list of valid actions
  31 |         private static readonly List<string> ValidActions = new List<string>
  32 |         {
  33 |             "import",
  34 |             "create",
  35 |             "modify",
  36 |             "delete",
  37 |             "duplicate",
  38 |             "move",
  39 |             "rename",
  40 |             "search",
  41 |             "get_info",
  42 |             "create_folder",
  43 |             "get_components",
  44 |         };
  45 | 
  46 |         public static object HandleCommand(JObject @params)
  47 |         {
  48 |             string action = @params["action"]?.ToString().ToLower();
  49 |             if (string.IsNullOrEmpty(action))
  50 |             {
  51 |                 return Response.Error("Action parameter is required.");
  52 |             }
  53 | 
  54 |             // Check if the action is valid before switching
  55 |             if (!ValidActions.Contains(action))
  56 |             {
  57 |                 string validActionsList = string.Join(", ", ValidActions);
  58 |                 return Response.Error(
  59 |                     $"Unknown action: '{action}'. Valid actions are: {validActionsList}"
  60 |                 );
  61 |             }
  62 | 
  63 |             // Common parameters
  64 |             string path = @params["path"]?.ToString();
  65 | 
  66 |             try
  67 |             {
  68 |                 switch (action)
  69 |                 {
  70 |                     case "import":
  71 |                         // Note: Unity typically auto-imports. This might re-import or configure import settings.
  72 |                         return ReimportAsset(path, @params["properties"] as JObject);
  73 |                     case "create":
  74 |                         return CreateAsset(@params);
  75 |                     case "modify":
  76 |                         return ModifyAsset(path, @params["properties"] as JObject);
  77 |                     case "delete":
  78 |                         return DeleteAsset(path);
  79 |                     case "duplicate":
  80 |                         return DuplicateAsset(path, @params["destination"]?.ToString());
  81 |                     case "move": // Often same as rename if within Assets/
  82 |                     case "rename":
  83 |                         return MoveOrRenameAsset(path, @params["destination"]?.ToString());
  84 |                     case "search":
  85 |                         return SearchAssets(@params);
  86 |                     case "get_info":
  87 |                         return GetAssetInfo(
  88 |                             path,
  89 |                             @params["generatePreview"]?.ToObject<bool>() ?? false
  90 |                         );
  91 |                     case "create_folder": // Added specific action for clarity
  92 |                         return CreateFolder(path);
  93 |                     case "get_components":
  94 |                         return GetComponentsFromAsset(path);
  95 | 
  96 |                     default:
  97 |                         // This error message is less likely to be hit now, but kept here as a fallback or for potential future modifications.
  98 |                         string validActionsListDefault = string.Join(", ", ValidActions);
  99 |                         return Response.Error(
 100 |                             $"Unknown action: '{action}'. Valid actions are: {validActionsListDefault}"
 101 |                         );
 102 |                 }
 103 |             }
 104 |             catch (Exception e)
 105 |             {
 106 |                 Debug.LogError($"[ManageAsset] Action '{action}' failed for path '{path}': {e}");
 107 |                 return Response.Error(
 108 |                     $"Internal error processing action '{action}' on '{path}': {e.Message}"
 109 |                 );
 110 |             }
 111 |         }
 112 | 
 113 |         // --- Action Implementations ---
 114 | 
 115 |         private static object ReimportAsset(string path, JObject properties)
 116 |         {
 117 |             if (string.IsNullOrEmpty(path))
 118 |                 return Response.Error("'path' is required for reimport.");
 119 |             string fullPath = AssetPathUtility.SanitizeAssetPath(path);
 120 |             if (!AssetExists(fullPath))
 121 |                 return Response.Error($"Asset not found at path: {fullPath}");
 122 | 
 123 |             try
 124 |             {
 125 |                 // TODO: Apply importer properties before reimporting?
 126 |                 // This is complex as it requires getting the AssetImporter, casting it,
 127 |                 // applying properties via reflection or specific methods, saving, then reimporting.
 128 |                 if (properties != null && properties.HasValues)
 129 |                 {
 130 |                     Debug.LogWarning(
 131 |                         "[ManageAsset.Reimport] Modifying importer properties before reimport is not fully implemented yet."
 132 |                     );
 133 |                     // AssetImporter importer = AssetImporter.GetAtPath(fullPath);
 134 |                     // if (importer != null) { /* Apply properties */ AssetDatabase.WriteImportSettingsIfDirty(fullPath); }
 135 |                 }
 136 | 
 137 |                 AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate);
 138 |                 // AssetDatabase.Refresh(); // Usually ImportAsset handles refresh
 139 |                 return Response.Success($"Asset '{fullPath}' reimported.", GetAssetData(fullPath));
 140 |             }
 141 |             catch (Exception e)
 142 |             {
 143 |                 return Response.Error($"Failed to reimport asset '{fullPath}': {e.Message}");
 144 |             }
 145 |         }
 146 | 
 147 |         private static object CreateAsset(JObject @params)
 148 |         {
 149 |             string path = @params["path"]?.ToString();
 150 |             string assetType = @params["assetType"]?.ToString();
 151 |             JObject properties = @params["properties"] as JObject;
 152 | 
 153 |             if (string.IsNullOrEmpty(path))
 154 |                 return Response.Error("'path' is required for create.");
 155 |             if (string.IsNullOrEmpty(assetType))
 156 |                 return Response.Error("'assetType' is required for create.");
 157 | 
 158 |             string fullPath = AssetPathUtility.SanitizeAssetPath(path);
 159 |             string directory = Path.GetDirectoryName(fullPath);
 160 | 
 161 |             // Ensure directory exists
 162 |             if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), directory)))
 163 |             {
 164 |                 Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), directory));
 165 |                 AssetDatabase.Refresh(); // Make sure Unity knows about the new folder
 166 |             }
 167 | 
 168 |             if (AssetExists(fullPath))
 169 |                 return Response.Error($"Asset already exists at path: {fullPath}");
 170 | 
 171 |             try
 172 |             {
 173 |                 UnityEngine.Object newAsset = null;
 174 |                 string lowerAssetType = assetType.ToLowerInvariant();
 175 | 
 176 |                 // Handle common asset types
 177 |                 if (lowerAssetType == "folder")
 178 |                 {
 179 |                     return CreateFolder(path); // Use dedicated method
 180 |                 }
 181 |                 else if (lowerAssetType == "material")
 182 |                 {
 183 |                     // Prefer provided shader; fall back to common pipelines
 184 |                     var requested = properties?["shader"]?.ToString();
 185 |                     Shader shader =
 186 |                         (!string.IsNullOrEmpty(requested) ? Shader.Find(requested) : null)
 187 |                         ?? Shader.Find("Universal Render Pipeline/Lit")
 188 |                         ?? Shader.Find("HDRP/Lit")
 189 |                         ?? Shader.Find("Standard")
 190 |                         ?? Shader.Find("Unlit/Color");
 191 |                     if (shader == null)
 192 |                         return Response.Error($"Could not find a suitable shader (requested: '{requested ?? "none"}').");
 193 | 
 194 |                     var mat = new Material(shader);
 195 |                     if (properties != null)
 196 |                         ApplyMaterialProperties(mat, properties);
 197 |                     AssetDatabase.CreateAsset(mat, fullPath);
 198 |                     newAsset = mat;
 199 |                 }
 200 |                 else if (lowerAssetType == "physicsmaterial")
 201 |                 {
 202 |                     PhysicsMaterialType pmat = new PhysicsMaterialType();
 203 |                     if (properties != null)
 204 |                         ApplyPhysicsMaterialProperties(pmat, properties);
 205 |                     AssetDatabase.CreateAsset(pmat, fullPath);
 206 |                     newAsset = pmat;
 207 |                 }
 208 |                 else if (lowerAssetType == "scriptableobject")
 209 |                 {
 210 |                     string scriptClassName = properties?["scriptClass"]?.ToString();
 211 |                     if (string.IsNullOrEmpty(scriptClassName))
 212 |                         return Response.Error(
 213 |                             "'scriptClass' property required when creating ScriptableObject asset."
 214 |                         );
 215 | 
 216 |                     Type scriptType = ComponentResolver.TryResolve(scriptClassName, out var resolvedType, out var error) ? resolvedType : null;
 217 |                     if (
 218 |                         scriptType == null
 219 |                         || !typeof(ScriptableObject).IsAssignableFrom(scriptType)
 220 |                     )
 221 |                     {
 222 |                         var reason = scriptType == null
 223 |                             ? (string.IsNullOrEmpty(error) ? "Type not found." : error)
 224 |                             : "Type found but does not inherit from ScriptableObject.";
 225 |                         return Response.Error($"Script class '{scriptClassName}' invalid: {reason}");
 226 |                     }
 227 | 
 228 |                     ScriptableObject so = ScriptableObject.CreateInstance(scriptType);
 229 |                     // TODO: Apply properties from JObject to the ScriptableObject instance?
 230 |                     AssetDatabase.CreateAsset(so, fullPath);
 231 |                     newAsset = so;
 232 |                 }
 233 |                 else if (lowerAssetType == "prefab")
 234 |                 {
 235 |                     // Creating prefabs usually involves saving an existing GameObject hierarchy.
 236 |                     // A common pattern is to create an empty GameObject, configure it, and then save it.
 237 |                     return Response.Error(
 238 |                         "Creating prefabs programmatically usually requires a source GameObject. Use manage_gameobject to create/configure, then save as prefab via a separate mechanism or future enhancement."
 239 |                     );
 240 |                     // Example (conceptual):
 241 |                     // GameObject source = GameObject.Find(properties["sourceGameObject"].ToString());
 242 |                     // if(source != null) PrefabUtility.SaveAsPrefabAsset(source, fullPath);
 243 |                 }
 244 |                 // TODO: Add more asset types (Animation Controller, Scene, etc.)
 245 |                 else
 246 |                 {
 247 |                     // Generic creation attempt (might fail or create empty files)
 248 |                     // For some types, just creating the file might be enough if Unity imports it.
 249 |                     // File.Create(Path.Combine(Directory.GetCurrentDirectory(), fullPath)).Close();
 250 |                     // AssetDatabase.ImportAsset(fullPath); // Let Unity try to import it
 251 |                     // newAsset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(fullPath);
 252 |                     return Response.Error(
 253 |                         $"Creation for asset type '{assetType}' is not explicitly supported yet. Supported: Folder, Material, ScriptableObject."
 254 |                     );
 255 |                 }
 256 | 
 257 |                 if (
 258 |                     newAsset == null
 259 |                     && !Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), fullPath))
 260 |                 ) // Check if it wasn't a folder and asset wasn't created
 261 |                 {
 262 |                     return Response.Error(
 263 |                         $"Failed to create asset '{assetType}' at '{fullPath}'. See logs for details."
 264 |                     );
 265 |                 }
 266 | 
 267 |                 AssetDatabase.SaveAssets();
 268 |                 // AssetDatabase.Refresh(); // CreateAsset often handles refresh
 269 |                 return Response.Success(
 270 |                     $"Asset '{fullPath}' created successfully.",
 271 |                     GetAssetData(fullPath)
 272 |                 );
 273 |             }
 274 |             catch (Exception e)
 275 |             {
 276 |                 return Response.Error($"Failed to create asset at '{fullPath}': {e.Message}");
 277 |             }
 278 |         }
 279 | 
 280 |         private static object CreateFolder(string path)
 281 |         {
 282 |             if (string.IsNullOrEmpty(path))
 283 |                 return Response.Error("'path' is required for create_folder.");
 284 |             string fullPath = AssetPathUtility.SanitizeAssetPath(path);
 285 |             string parentDir = Path.GetDirectoryName(fullPath);
 286 |             string folderName = Path.GetFileName(fullPath);
 287 | 
 288 |             if (AssetExists(fullPath))
 289 |             {
 290 |                 // Check if it's actually a folder already
 291 |                 if (AssetDatabase.IsValidFolder(fullPath))
 292 |                 {
 293 |                     return Response.Success(
 294 |                         $"Folder already exists at path: {fullPath}",
 295 |                         GetAssetData(fullPath)
 296 |                     );
 297 |                 }
 298 |                 else
 299 |                 {
 300 |                     return Response.Error(
 301 |                         $"An asset (not a folder) already exists at path: {fullPath}"
 302 |                     );
 303 |                 }
 304 |             }
 305 | 
 306 |             try
 307 |             {
 308 |                 // Ensure parent exists
 309 |                 if (!string.IsNullOrEmpty(parentDir) && !AssetDatabase.IsValidFolder(parentDir))
 310 |                 {
 311 |                     // Recursively create parent folders if needed (AssetDatabase handles this internally)
 312 |                     // Or we can do it manually: Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), parentDir)); AssetDatabase.Refresh();
 313 |                 }
 314 | 
 315 |                 string guid = AssetDatabase.CreateFolder(parentDir, folderName);
 316 |                 if (string.IsNullOrEmpty(guid))
 317 |                 {
 318 |                     return Response.Error(
 319 |                         $"Failed to create folder '{fullPath}'. Check logs and permissions."
 320 |                     );
 321 |                 }
 322 | 
 323 |                 // AssetDatabase.Refresh(); // CreateFolder usually handles refresh
 324 |                 return Response.Success(
 325 |                     $"Folder '{fullPath}' created successfully.",
 326 |                     GetAssetData(fullPath)
 327 |                 );
 328 |             }
 329 |             catch (Exception e)
 330 |             {
 331 |                 return Response.Error($"Failed to create folder '{fullPath}': {e.Message}");
 332 |             }
 333 |         }
 334 | 
 335 |         private static object ModifyAsset(string path, JObject properties)
 336 |         {
 337 |             if (string.IsNullOrEmpty(path))
 338 |                 return Response.Error("'path' is required for modify.");
 339 |             if (properties == null || !properties.HasValues)
 340 |                 return Response.Error("'properties' are required for modify.");
 341 | 
 342 |             string fullPath = AssetPathUtility.SanitizeAssetPath(path);
 343 |             if (!AssetExists(fullPath))
 344 |                 return Response.Error($"Asset not found at path: {fullPath}");
 345 | 
 346 |             try
 347 |             {
 348 |                 UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(
 349 |                     fullPath
 350 |                 );
 351 |                 if (asset == null)
 352 |                     return Response.Error($"Failed to load asset at path: {fullPath}");
 353 | 
 354 |                 bool modified = false; // Flag to track if any changes were made
 355 | 
 356 |                 // --- NEW: Handle GameObject / Prefab Component Modification ---
 357 |                 if (asset is GameObject gameObject)
 358 |                 {
 359 |                     // Iterate through the properties JSON: keys are component names, values are properties objects for that component
 360 |                     foreach (var prop in properties.Properties())
 361 |                     {
 362 |                         string componentName = prop.Name; // e.g., "Collectible"
 363 |                         // Check if the value associated with the component name is actually an object containing properties
 364 |                         if (
 365 |                             prop.Value is JObject componentProperties
 366 |                             && componentProperties.HasValues
 367 |                         ) // e.g., {"bobSpeed": 2.0}
 368 |                         {
 369 |                             // Resolve component type via ComponentResolver, then fetch by Type
 370 |                             Component targetComponent = null;
 371 |                             bool resolved = ComponentResolver.TryResolve(componentName, out var compType, out var compError);
 372 |                             if (resolved)
 373 |                             {
 374 |                                 targetComponent = gameObject.GetComponent(compType);
 375 |                             }
 376 | 
 377 |                             // Only warn about resolution failure if component also not found
 378 |                             if (targetComponent == null && !resolved)
 379 |                             {
 380 |                                 Debug.LogWarning(
 381 |                                     $"[ManageAsset.ModifyAsset] Failed to resolve component '{componentName}' on '{gameObject.name}': {compError}"
 382 |                                 );
 383 |                             }
 384 | 
 385 |                             if (targetComponent != null)
 386 |                             {
 387 |                                 // Apply the nested properties (e.g., bobSpeed) to the found component instance
 388 |                                 // Use |= to ensure 'modified' becomes true if any component is successfully modified
 389 |                                 modified |= ApplyObjectProperties(
 390 |                                     targetComponent,
 391 |                                     componentProperties
 392 |                                 );
 393 |                             }
 394 |                             else
 395 |                             {
 396 |                                 // Log a warning if a specified component couldn't be found
 397 |                                 Debug.LogWarning(
 398 |                                     $"[ManageAsset.ModifyAsset] Component '{componentName}' not found on GameObject '{gameObject.name}' in asset '{fullPath}'. Skipping modification for this component."
 399 |                                 );
 400 |                             }
 401 |                         }
 402 |                         else
 403 |                         {
 404 |                             // Log a warning if the structure isn't {"ComponentName": {"prop": value}}
 405 |                             // We could potentially try to apply this property directly to the GameObject here if needed,
 406 |                             // but the primary goal is component modification.
 407 |                             Debug.LogWarning(
 408 |                                 $"[ManageAsset.ModifyAsset] Property '{prop.Name}' for GameObject modification should have a JSON object value containing component properties. Value was: {prop.Value.Type}. Skipping."
 409 |                             );
 410 |                         }
 411 |                     }
 412 |                     // Note: 'modified' is now true if ANY component property was successfully changed.
 413 |                 }
 414 |                 // --- End NEW ---
 415 | 
 416 |                 // --- Existing logic for other asset types (now as else-if) ---
 417 |                 // Example: Modifying a Material
 418 |                 else if (asset is Material material)
 419 |                 {
 420 |                     // Apply properties directly to the material. If this modifies, it sets modified=true.
 421 |                     // Use |= in case the asset was already marked modified by previous logic (though unlikely here)
 422 |                     modified |= ApplyMaterialProperties(material, properties);
 423 |                 }
 424 |                 // Example: Modifying a ScriptableObject
 425 |                 else if (asset is ScriptableObject so)
 426 |                 {
 427 |                     // Apply properties directly to the ScriptableObject.
 428 |                     modified |= ApplyObjectProperties(so, properties); // General helper
 429 |                 }
 430 |                 // Example: Modifying TextureImporter settings
 431 |                 else if (asset is Texture)
 432 |                 {
 433 |                     AssetImporter importer = AssetImporter.GetAtPath(fullPath);
 434 |                     if (importer is TextureImporter textureImporter)
 435 |                     {
 436 |                         bool importerModified = ApplyObjectProperties(textureImporter, properties);
 437 |                         if (importerModified)
 438 |                         {
 439 |                             // Importer settings need saving and reimporting
 440 |                             AssetDatabase.WriteImportSettingsIfDirty(fullPath);
 441 |                             AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); // Reimport to apply changes
 442 |                             modified = true; // Mark overall operation as modified
 443 |                         }
 444 |                     }
 445 |                     else
 446 |                     {
 447 |                         Debug.LogWarning($"Could not get TextureImporter for {fullPath}.");
 448 |                     }
 449 |                 }
 450 |                 // TODO: Add modification logic for other common asset types (Models, AudioClips importers, etc.)
 451 |                 else // Fallback for other asset types OR direct properties on non-GameObject assets
 452 |                 {
 453 |                     // This block handles non-GameObject/Material/ScriptableObject/Texture assets.
 454 |                     // Attempts to apply properties directly to the asset itself.
 455 |                     Debug.LogWarning(
 456 |                         $"[ManageAsset.ModifyAsset] Asset type '{asset.GetType().Name}' at '{fullPath}' is not explicitly handled for component modification. Attempting generic property setting on the asset itself."
 457 |                     );
 458 |                     modified |= ApplyObjectProperties(asset, properties);
 459 |                 }
 460 |                 // --- End Existing Logic ---
 461 | 
 462 |                 // Check if any modification happened (either component or direct asset modification)
 463 |                 if (modified)
 464 |                 {
 465 |                     // Mark the asset as dirty (important for prefabs/SOs) so Unity knows to save it.
 466 |                     EditorUtility.SetDirty(asset);
 467 |                     // Save all modified assets to disk.
 468 |                     AssetDatabase.SaveAssets();
 469 |                     // Refresh might be needed in some edge cases, but SaveAssets usually covers it.
 470 |                     // AssetDatabase.Refresh();
 471 |                     return Response.Success(
 472 |                         $"Asset '{fullPath}' modified successfully.",
 473 |                         GetAssetData(fullPath)
 474 |                     );
 475 |                 }
 476 |                 else
 477 |                 {
 478 |                     // If no changes were made (e.g., component not found, property names incorrect, value unchanged), return a success message indicating nothing changed.
 479 |                     return Response.Success(
 480 |                         $"No applicable or modifiable properties found for asset '{fullPath}'. Check component names, property names, and values.",
 481 |                         GetAssetData(fullPath)
 482 |                     );
 483 |                     // Previous message: return Response.Success($"No applicable properties found to modify for asset '{fullPath}'.", GetAssetData(fullPath));
 484 |                 }
 485 |             }
 486 |             catch (Exception e)
 487 |             {
 488 |                 // Log the detailed error internally
 489 |                 Debug.LogError($"[ManageAsset] Action 'modify' failed for path '{path}': {e}");
 490 |                 // Return a user-friendly error message
 491 |                 return Response.Error($"Failed to modify asset '{fullPath}': {e.Message}");
 492 |             }
 493 |         }
 494 | 
 495 |         private static object DeleteAsset(string path)
 496 |         {
 497 |             if (string.IsNullOrEmpty(path))
 498 |                 return Response.Error("'path' is required for delete.");
 499 |             string fullPath = AssetPathUtility.SanitizeAssetPath(path);
 500 |             if (!AssetExists(fullPath))
 501 |                 return Response.Error($"Asset not found at path: {fullPath}");
 502 | 
 503 |             try
 504 |             {
 505 |                 bool success = AssetDatabase.DeleteAsset(fullPath);
 506 |                 if (success)
 507 |                 {
 508 |                     // AssetDatabase.Refresh(); // DeleteAsset usually handles refresh
 509 |                     return Response.Success($"Asset '{fullPath}' deleted successfully.");
 510 |                 }
 511 |                 else
 512 |                 {
 513 |                     // This might happen if the file couldn't be deleted (e.g., locked)
 514 |                     return Response.Error(
 515 |                         $"Failed to delete asset '{fullPath}'. Check logs or if the file is locked."
 516 |                     );
 517 |                 }
 518 |             }
 519 |             catch (Exception e)
 520 |             {
 521 |                 return Response.Error($"Error deleting asset '{fullPath}': {e.Message}");
 522 |             }
 523 |         }
 524 | 
 525 |         private static object DuplicateAsset(string path, string destinationPath)
 526 |         {
 527 |             if (string.IsNullOrEmpty(path))
 528 |                 return Response.Error("'path' is required for duplicate.");
 529 | 
 530 |             string sourcePath = AssetPathUtility.SanitizeAssetPath(path);
 531 |             if (!AssetExists(sourcePath))
 532 |                 return Response.Error($"Source asset not found at path: {sourcePath}");
 533 | 
 534 |             string destPath;
 535 |             if (string.IsNullOrEmpty(destinationPath))
 536 |             {
 537 |                 // Generate a unique path if destination is not provided
 538 |                 destPath = AssetDatabase.GenerateUniqueAssetPath(sourcePath);
 539 |             }
 540 |             else
 541 |             {
 542 |                 destPath = AssetPathUtility.SanitizeAssetPath(destinationPath);
 543 |                 if (AssetExists(destPath))
 544 |                     return Response.Error($"Asset already exists at destination path: {destPath}");
 545 |                 // Ensure destination directory exists
 546 |                 EnsureDirectoryExists(Path.GetDirectoryName(destPath));
 547 |             }
 548 | 
 549 |             try
 550 |             {
 551 |                 bool success = AssetDatabase.CopyAsset(sourcePath, destPath);
 552 |                 if (success)
 553 |                 {
 554 |                     // AssetDatabase.Refresh();
 555 |                     return Response.Success(
 556 |                         $"Asset '{sourcePath}' duplicated to '{destPath}'.",
 557 |                         GetAssetData(destPath)
 558 |                     );
 559 |                 }
 560 |                 else
 561 |                 {
 562 |                     return Response.Error(
 563 |                         $"Failed to duplicate asset from '{sourcePath}' to '{destPath}'."
 564 |                     );
 565 |                 }
 566 |             }
 567 |             catch (Exception e)
 568 |             {
 569 |                 return Response.Error($"Error duplicating asset '{sourcePath}': {e.Message}");
 570 |             }
 571 |         }
 572 | 
 573 |         private static object MoveOrRenameAsset(string path, string destinationPath)
 574 |         {
 575 |             if (string.IsNullOrEmpty(path))
 576 |                 return Response.Error("'path' is required for move/rename.");
 577 |             if (string.IsNullOrEmpty(destinationPath))
 578 |                 return Response.Error("'destination' path is required for move/rename.");
 579 | 
 580 |             string sourcePath = AssetPathUtility.SanitizeAssetPath(path);
 581 |             string destPath = AssetPathUtility.SanitizeAssetPath(destinationPath);
 582 | 
 583 |             if (!AssetExists(sourcePath))
 584 |                 return Response.Error($"Source asset not found at path: {sourcePath}");
 585 |             if (AssetExists(destPath))
 586 |                 return Response.Error(
 587 |                     $"An asset already exists at the destination path: {destPath}"
 588 |                 );
 589 | 
 590 |             // Ensure destination directory exists
 591 |             EnsureDirectoryExists(Path.GetDirectoryName(destPath));
 592 | 
 593 |             try
 594 |             {
 595 |                 // Validate will return an error string if failed, null if successful
 596 |                 string error = AssetDatabase.ValidateMoveAsset(sourcePath, destPath);
 597 |                 if (!string.IsNullOrEmpty(error))
 598 |                 {
 599 |                     return Response.Error(
 600 |                         $"Failed to move/rename asset from '{sourcePath}' to '{destPath}': {error}"
 601 |                     );
 602 |                 }
 603 | 
 604 |                 string guid = AssetDatabase.MoveAsset(sourcePath, destPath);
 605 |                 if (!string.IsNullOrEmpty(guid)) // MoveAsset returns the new GUID on success
 606 |                 {
 607 |                     // AssetDatabase.Refresh(); // MoveAsset usually handles refresh
 608 |                     return Response.Success(
 609 |                         $"Asset moved/renamed from '{sourcePath}' to '{destPath}'.",
 610 |                         GetAssetData(destPath)
 611 |                     );
 612 |                 }
 613 |                 else
 614 |                 {
 615 |                     // This case might not be reachable if ValidateMoveAsset passes, but good to have
 616 |                     return Response.Error(
 617 |                         $"MoveAsset call failed unexpectedly for '{sourcePath}' to '{destPath}'."
 618 |                     );
 619 |                 }
 620 |             }
 621 |             catch (Exception e)
 622 |             {
 623 |                 return Response.Error($"Error moving/renaming asset '{sourcePath}': {e.Message}");
 624 |             }
 625 |         }
 626 | 
 627 |         private static object SearchAssets(JObject @params)
 628 |         {
 629 |             string searchPattern = @params["searchPattern"]?.ToString();
 630 |             string filterType = @params["filterType"]?.ToString();
 631 |             string pathScope = @params["path"]?.ToString(); // Use path as folder scope
 632 |             string filterDateAfterStr = @params["filterDateAfter"]?.ToString();
 633 |             int pageSize = @params["pageSize"]?.ToObject<int?>() ?? 50; // Default page size
 634 |             int pageNumber = @params["pageNumber"]?.ToObject<int?>() ?? 1; // Default page number (1-based)
 635 |             bool generatePreview = @params["generatePreview"]?.ToObject<bool>() ?? false;
 636 | 
 637 |             List<string> searchFilters = new List<string>();
 638 |             if (!string.IsNullOrEmpty(searchPattern))
 639 |                 searchFilters.Add(searchPattern);
 640 |             if (!string.IsNullOrEmpty(filterType))
 641 |                 searchFilters.Add($"t:{filterType}");
 642 | 
 643 |             string[] folderScope = null;
 644 |             if (!string.IsNullOrEmpty(pathScope))
 645 |             {
 646 |                 folderScope = new string[] { AssetPathUtility.SanitizeAssetPath(pathScope) };
 647 |                 if (!AssetDatabase.IsValidFolder(folderScope[0]))
 648 |                 {
 649 |                     // Maybe the user provided a file path instead of a folder?
 650 |                     // We could search in the containing folder, or return an error.
 651 |                     Debug.LogWarning(
 652 |                         $"Search path '{folderScope[0]}' is not a valid folder. Searching entire project."
 653 |                     );
 654 |                     folderScope = null; // Search everywhere if path isn't a folder
 655 |                 }
 656 |             }
 657 | 
 658 |             DateTime? filterDateAfter = null;
 659 |             if (!string.IsNullOrEmpty(filterDateAfterStr))
 660 |             {
 661 |                 if (
 662 |                     DateTime.TryParse(
 663 |                         filterDateAfterStr,
 664 |                         CultureInfo.InvariantCulture,
 665 |                         DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
 666 |                         out DateTime parsedDate
 667 |                     )
 668 |                 )
 669 |                 {
 670 |                     filterDateAfter = parsedDate;
 671 |                 }
 672 |                 else
 673 |                 {
 674 |                     Debug.LogWarning(
 675 |                         $"Could not parse filterDateAfter: '{filterDateAfterStr}'. Expected ISO 8601 format."
 676 |                     );
 677 |                 }
 678 |             }
 679 | 
 680 |             try
 681 |             {
 682 |                 string[] guids = AssetDatabase.FindAssets(
 683 |                     string.Join(" ", searchFilters),
 684 |                     folderScope
 685 |                 );
 686 |                 List<object> results = new List<object>();
 687 |                 int totalFound = 0;
 688 | 
 689 |                 foreach (string guid in guids)
 690 |                 {
 691 |                     string assetPath = AssetDatabase.GUIDToAssetPath(guid);
 692 |                     if (string.IsNullOrEmpty(assetPath))
 693 |                         continue;
 694 | 
 695 |                     // Apply date filter if present
 696 |                     if (filterDateAfter.HasValue)
 697 |                     {
 698 |                         DateTime lastWriteTime = File.GetLastWriteTimeUtc(
 699 |                             Path.Combine(Directory.GetCurrentDirectory(), assetPath)
 700 |                         );
 701 |                         if (lastWriteTime <= filterDateAfter.Value)
 702 |                         {
 703 |                             continue; // Skip assets older than or equal to the filter date
 704 |                         }
 705 |                     }
 706 | 
 707 |                     totalFound++; // Count matching assets before pagination
 708 |                     results.Add(GetAssetData(assetPath, generatePreview));
 709 |                 }
 710 | 
 711 |                 // Apply pagination
 712 |                 int startIndex = (pageNumber - 1) * pageSize;
 713 |                 var pagedResults = results.Skip(startIndex).Take(pageSize).ToList();
 714 | 
 715 |                 return Response.Success(
 716 |                     $"Found {totalFound} asset(s). Returning page {pageNumber} ({pagedResults.Count} assets).",
 717 |                     new
 718 |                     {
 719 |                         totalAssets = totalFound,
 720 |                         pageSize = pageSize,
 721 |                         pageNumber = pageNumber,
 722 |                         assets = pagedResults,
 723 |                     }
 724 |                 );
 725 |             }
 726 |             catch (Exception e)
 727 |             {
 728 |                 return Response.Error($"Error searching assets: {e.Message}");
 729 |             }
 730 |         }
 731 | 
 732 |         private static object GetAssetInfo(string path, bool generatePreview)
 733 |         {
 734 |             if (string.IsNullOrEmpty(path))
 735 |                 return Response.Error("'path' is required for get_info.");
 736 |             string fullPath = AssetPathUtility.SanitizeAssetPath(path);
 737 |             if (!AssetExists(fullPath))
 738 |                 return Response.Error($"Asset not found at path: {fullPath}");
 739 | 
 740 |             try
 741 |             {
 742 |                 return Response.Success(
 743 |                     "Asset info retrieved.",
 744 |                     GetAssetData(fullPath, generatePreview)
 745 |                 );
 746 |             }
 747 |             catch (Exception e)
 748 |             {
 749 |                 return Response.Error($"Error getting info for asset '{fullPath}': {e.Message}");
 750 |             }
 751 |         }
 752 | 
 753 |         /// <summary>
 754 |         /// Retrieves components attached to a GameObject asset (like a Prefab).
 755 |         /// </summary>
 756 |         /// <param name="path">The asset path of the GameObject or Prefab.</param>
 757 |         /// <returns>A response object containing a list of component type names or an error.</returns>
 758 |         private static object GetComponentsFromAsset(string path)
 759 |         {
 760 |             // 1. Validate input path
 761 |             if (string.IsNullOrEmpty(path))
 762 |                 return Response.Error("'path' is required for get_components.");
 763 | 
 764 |             // 2. Sanitize and check existence
 765 |             string fullPath = AssetPathUtility.SanitizeAssetPath(path);
 766 |             if (!AssetExists(fullPath))
 767 |                 return Response.Error($"Asset not found at path: {fullPath}");
 768 | 
 769 |             try
 770 |             {
 771 |                 // 3. Load the asset
 772 |                 UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(
 773 |                     fullPath
 774 |                 );
 775 |                 if (asset == null)
 776 |                     return Response.Error($"Failed to load asset at path: {fullPath}");
 777 | 
 778 |                 // 4. Check if it's a GameObject (Prefabs load as GameObjects)
 779 |                 GameObject gameObject = asset as GameObject;
 780 |                 if (gameObject == null)
 781 |                 {
 782 |                     // Also check if it's *directly* a Component type (less common for primary assets)
 783 |                     Component componentAsset = asset as Component;
 784 |                     if (componentAsset != null)
 785 |                     {
 786 |                         // If the asset itself *is* a component, maybe return just its info?
 787 |                         // This is an edge case. Let's stick to GameObjects for now.
 788 |                         return Response.Error(
 789 |                             $"Asset at '{fullPath}' is a Component ({asset.GetType().FullName}), not a GameObject. Components are typically retrieved *from* a GameObject."
 790 |                         );
 791 |                     }
 792 |                     return Response.Error(
 793 |                         $"Asset at '{fullPath}' is not a GameObject (Type: {asset.GetType().FullName}). Cannot get components from this asset type."
 794 |                     );
 795 |                 }
 796 | 
 797 |                 // 5. Get components
 798 |                 Component[] components = gameObject.GetComponents<Component>();
 799 | 
 800 |                 // 6. Format component data
 801 |                 List<object> componentList = components
 802 |                     .Select(comp => new
 803 |                     {
 804 |                         typeName = comp.GetType().FullName,
 805 |                         instanceID = comp.GetInstanceID(),
 806 |                         // TODO: Add more component-specific details here if needed in the future?
 807 |                         //       Requires reflection or specific handling per component type.
 808 |                     })
 809 |                     .ToList<object>(); // Explicit cast for clarity if needed
 810 | 
 811 |                 // 7. Return success response
 812 |                 return Response.Success(
 813 |                     $"Found {componentList.Count} component(s) on asset '{fullPath}'.",
 814 |                     componentList
 815 |                 );
 816 |             }
 817 |             catch (Exception e)
 818 |             {
 819 |                 Debug.LogError(
 820 |                     $"[ManageAsset.GetComponentsFromAsset] Error getting components for '{fullPath}': {e}"
 821 |                 );
 822 |                 return Response.Error(
 823 |                     $"Error getting components for asset '{fullPath}': {e.Message}"
 824 |                 );
 825 |             }
 826 |         }
 827 | 
 828 |         // --- Internal Helpers ---
 829 | 
 830 |         /// <summary>
 831 |         /// Ensures the asset path starts with "Assets/".
 832 |         /// </summary>
 833 |         /// <summary>
 834 |         /// Checks if an asset exists at the given path (file or folder).
 835 |         /// </summary>
 836 |         private static bool AssetExists(string sanitizedPath)
 837 |         {
 838 |             // AssetDatabase APIs are generally preferred over raw File/Directory checks for assets.
 839 |             // Check if it's a known asset GUID.
 840 |             if (!string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(sanitizedPath)))
 841 |             {
 842 |                 return true;
 843 |             }
 844 |             // AssetPathToGUID might not work for newly created folders not yet refreshed.
 845 |             // Check directory explicitly for folders.
 846 |             if (Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), sanitizedPath)))
 847 |             {
 848 |                 // Check if it's considered a *valid* folder by Unity
 849 |                 return AssetDatabase.IsValidFolder(sanitizedPath);
 850 |             }
 851 |             // Check file existence for non-folder assets.
 852 |             if (File.Exists(Path.Combine(Directory.GetCurrentDirectory(), sanitizedPath)))
 853 |             {
 854 |                 return true; // Assume if file exists, it's an asset or will be imported
 855 |             }
 856 | 
 857 |             return false;
 858 |             // Alternative: return !string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(sanitizedPath));
 859 |         }
 860 | 
 861 |         /// <summary>
 862 |         /// Ensures the directory for a given asset path exists, creating it if necessary.
 863 |         /// </summary>
 864 |         private static void EnsureDirectoryExists(string directoryPath)
 865 |         {
 866 |             if (string.IsNullOrEmpty(directoryPath))
 867 |                 return;
 868 |             string fullDirPath = Path.Combine(Directory.GetCurrentDirectory(), directoryPath);
 869 |             if (!Directory.Exists(fullDirPath))
 870 |             {
 871 |                 Directory.CreateDirectory(fullDirPath);
 872 |                 AssetDatabase.Refresh(); // Let Unity know about the new folder
 873 |             }
 874 |         }
 875 | 
 876 |         /// <summary>
 877 |         /// Applies properties from JObject to a Material.
 878 |         /// </summary>
 879 |         private static bool ApplyMaterialProperties(Material mat, JObject properties)
 880 |         {
 881 |             if (mat == null || properties == null)
 882 |                 return false;
 883 |             bool modified = false;
 884 | 
 885 |             // Example: Set shader
 886 |             if (properties["shader"]?.Type == JTokenType.String)
 887 |             {
 888 |                 Shader newShader = Shader.Find(properties["shader"].ToString());
 889 |                 if (newShader != null && mat.shader != newShader)
 890 |                 {
 891 |                     mat.shader = newShader;
 892 |                     modified = true;
 893 |                 }
 894 |             }
 895 |             // Example: Set color property
 896 |             if (properties["color"] is JObject colorProps)
 897 |             {
 898 |                 string propName = colorProps["name"]?.ToString() ?? "_Color"; // Default main color
 899 |                 if (colorProps["value"] is JArray colArr && colArr.Count >= 3)
 900 |                 {
 901 |                     try
 902 |                     {
 903 |                         Color newColor = new Color(
 904 |                             colArr[0].ToObject<float>(),
 905 |                             colArr[1].ToObject<float>(),
 906 |                             colArr[2].ToObject<float>(),
 907 |                             colArr.Count > 3 ? colArr[3].ToObject<float>() : 1.0f
 908 |                         );
 909 |                         if (mat.HasProperty(propName) && mat.GetColor(propName) != newColor)
 910 |                         {
 911 |                             mat.SetColor(propName, newColor);
 912 |                             modified = true;
 913 |                         }
 914 |                     }
 915 |                     catch (Exception ex)
 916 |                     {
 917 |                         Debug.LogWarning(
 918 |                             $"Error parsing color property '{propName}': {ex.Message}"
 919 |                         );
 920 |                     }
 921 |                 }
 922 |             }
 923 |             else if (properties["color"] is JArray colorArr) //Use color now with examples set in manage_asset.py
 924 |             {
 925 |                 string propName = "_Color";
 926 |                 try
 927 |                 {
 928 |                     if (colorArr.Count >= 3)
 929 |                     {
 930 |                         Color newColor = new Color(
 931 |                             colorArr[0].ToObject<float>(),
 932 |                             colorArr[1].ToObject<float>(),
 933 |                             colorArr[2].ToObject<float>(),
 934 |                             colorArr.Count > 3 ? colorArr[3].ToObject<float>() : 1.0f
 935 |                         );
 936 |                         if (mat.HasProperty(propName) && mat.GetColor(propName) != newColor)
 937 |                         {
 938 |                             mat.SetColor(propName, newColor);
 939 |                             modified = true;
 940 |                         }
 941 |                     }
 942 |                 }
 943 |                 catch (Exception ex)
 944 |                 {
 945 |                     Debug.LogWarning(
 946 |                         $"Error parsing color property '{propName}': {ex.Message}"
 947 |                     );
 948 |                 }
 949 |             }
 950 |             // Example: Set float property
 951 |             if (properties["float"] is JObject floatProps)
 952 |             {
 953 |                 string propName = floatProps["name"]?.ToString();
 954 |                 if (
 955 |                     !string.IsNullOrEmpty(propName) &&
 956 |                     (floatProps["value"]?.Type == JTokenType.Float || floatProps["value"]?.Type == JTokenType.Integer)
 957 |                 )
 958 |                 {
 959 |                     try
 960 |                     {
 961 |                         float newVal = floatProps["value"].ToObject<float>();
 962 |                         if (mat.HasProperty(propName) && mat.GetFloat(propName) != newVal)
 963 |                         {
 964 |                             mat.SetFloat(propName, newVal);
 965 |                             modified = true;
 966 |                         }
 967 |                     }
 968 |                     catch (Exception ex)
 969 |                     {
 970 |                         Debug.LogWarning(
 971 |                             $"Error parsing float property '{propName}': {ex.Message}"
 972 |                         );
 973 |                     }
 974 |                 }
 975 |             }
 976 |             // Example: Set texture property
 977 |             if (properties["texture"] is JObject texProps)
 978 |             {
 979 |                 string propName = texProps["name"]?.ToString() ?? "_MainTex"; // Default main texture
 980 |                 string texPath = texProps["path"]?.ToString();
 981 |                 if (!string.IsNullOrEmpty(texPath))
 982 |                 {
 983 |                     Texture newTex = AssetDatabase.LoadAssetAtPath<Texture>(
 984 |                         AssetPathUtility.SanitizeAssetPath(texPath)
 985 |                     );
 986 |                     if (
 987 |                         newTex != null
 988 |                         && mat.HasProperty(propName)
 989 |                         && mat.GetTexture(propName) != newTex
 990 |                     )
 991 |                     {
 992 |                         mat.SetTexture(propName, newTex);
 993 |                         modified = true;
 994 |                     }
 995 |                     else if (newTex == null)
 996 |                     {
 997 |                         Debug.LogWarning($"Texture not found at path: {texPath}");
 998 |                     }
 999 |                 }
1000 |             }
1001 | 
1002 |             // TODO: Add handlers for other property types (Vectors, Ints, Keywords, RenderQueue, etc.)
1003 |             return modified;
1004 |         }
1005 | 
1006 |         /// <summary>
1007 |         ///  Applies properties from JObject to a PhysicsMaterial.
1008 |         /// </summary>
1009 |         private static bool ApplyPhysicsMaterialProperties(PhysicsMaterialType pmat, JObject properties)
1010 |         {
1011 |             if (pmat == null || properties == null)
1012 |                 return false;
1013 |             bool modified = false;
1014 | 
1015 |             // Example: Set dynamic friction
1016 |             if (properties["dynamicFriction"]?.Type == JTokenType.Float)
1017 |             {
1018 |                 float dynamicFriction = properties["dynamicFriction"].ToObject<float>();
1019 |                 pmat.dynamicFriction = dynamicFriction;
1020 |                 modified = true;
1021 |             }
1022 | 
1023 |             // Example: Set static friction
1024 |             if (properties["staticFriction"]?.Type == JTokenType.Float)
1025 |             {
1026 |                 float staticFriction = properties["staticFriction"].ToObject<float>();
1027 |                 pmat.staticFriction = staticFriction;
1028 |                 modified = true;
1029 |             }
1030 | 
1031 |             // Example: Set bounciness
1032 |             if (properties["bounciness"]?.Type == JTokenType.Float)
1033 |             {
1034 |                 float bounciness = properties["bounciness"].ToObject<float>();
1035 |                 pmat.bounciness = bounciness;
1036 |                 modified = true;
1037 |             }
1038 | 
1039 |             List<String> averageList = new List<String> { "ave", "Ave", "average", "Average" };
1040 |             List<String> multiplyList = new List<String> { "mul", "Mul", "mult", "Mult", "multiply", "Multiply" };
1041 |             List<String> minimumList = new List<String> { "min", "Min", "minimum", "Minimum" };
1042 |             List<String> maximumList = new List<String> { "max", "Max", "maximum", "Maximum" };
1043 | 
1044 |             // Example: Set friction combine
1045 |             if (properties["frictionCombine"]?.Type == JTokenType.String)
1046 |             {
1047 |                 string frictionCombine = properties["frictionCombine"].ToString();
1048 |                 if (averageList.Contains(frictionCombine))
1049 |                     pmat.frictionCombine = PhysicsMaterialCombine.Average;
1050 |                 else if (multiplyList.Contains(frictionCombine))
1051 |                     pmat.frictionCombine = PhysicsMaterialCombine.Multiply;
1052 |                 else if (minimumList.Contains(frictionCombine))
1053 |                     pmat.frictionCombine = PhysicsMaterialCombine.Minimum;
1054 |                 else if (maximumList.Contains(frictionCombine))
1055 |                     pmat.frictionCombine = PhysicsMaterialCombine.Maximum;
1056 |                 modified = true;
1057 |             }
1058 | 
1059 |             // Example: Set bounce combine
1060 |             if (properties["bounceCombine"]?.Type == JTokenType.String)
1061 |             {
1062 |                 string bounceCombine = properties["bounceCombine"].ToString();
1063 |                 if (averageList.Contains(bounceCombine))
1064 |                     pmat.bounceCombine = PhysicsMaterialCombine.Average;
1065 |                 else if (multiplyList.Contains(bounceCombine))
1066 |                     pmat.bounceCombine = PhysicsMaterialCombine.Multiply;
1067 |                 else if (minimumList.Contains(bounceCombine))
1068 |                     pmat.bounceCombine = PhysicsMaterialCombine.Minimum;
1069 |                 else if (maximumList.Contains(bounceCombine))
1070 |                     pmat.bounceCombine = PhysicsMaterialCombine.Maximum;
1071 |                 modified = true;
1072 |             }
1073 | 
1074 |             return modified;
1075 |         }
1076 | 
1077 |         /// <summary>
1078 |         /// Generic helper to set properties on any UnityEngine.Object using reflection.
1079 |         /// </summary>
1080 |         private static bool ApplyObjectProperties(UnityEngine.Object target, JObject properties)
1081 |         {
1082 |             if (target == null || properties == null)
1083 |                 return false;
1084 |             bool modified = false;
1085 |             Type type = target.GetType();
1086 | 
1087 |             foreach (var prop in properties.Properties())
1088 |             {
1089 |                 string propName = prop.Name;
1090 |                 JToken propValue = prop.Value;
1091 |                 if (SetPropertyOrField(target, propName, propValue, type))
1092 |                 {
1093 |                     modified = true;
1094 |                 }
1095 |             }
1096 |             return modified;
1097 |         }
1098 | 
1099 |         /// <summary>
1100 |         /// Helper to set a property or field via reflection, handling basic types and Unity objects.
1101 |         /// </summary>
1102 |         private static bool SetPropertyOrField(
1103 |             object target,
1104 |             string memberName,
1105 |             JToken value,
1106 |             Type type = null
1107 |         )
1108 |         {
1109 |             type = type ?? target.GetType();
1110 |             System.Reflection.BindingFlags flags =
1111 |                 System.Reflection.BindingFlags.Public
1112 |                 | System.Reflection.BindingFlags.Instance
1113 |                 | System.Reflection.BindingFlags.IgnoreCase;
1114 | 
1115 |             try
1116 |             {
1117 |                 System.Reflection.PropertyInfo propInfo = type.GetProperty(memberName, flags);
1118 |                 if (propInfo != null && propInfo.CanWrite)
1119 |                 {
1120 |                     object convertedValue = ConvertJTokenToType(value, propInfo.PropertyType);
1121 |                     if (
1122 |                         convertedValue != null
1123 |                         && !object.Equals(propInfo.GetValue(target), convertedValue)
1124 |                     )
1125 |                     {
1126 |                         propInfo.SetValue(target, convertedValue);
1127 |                         return true;
1128 |                     }
1129 |                 }
1130 |                 else
1131 |                 {
1132 |                     System.Reflection.FieldInfo fieldInfo = type.GetField(memberName, flags);
1133 |                     if (fieldInfo != null)
1134 |                     {
1135 |                         object convertedValue = ConvertJTokenToType(value, fieldInfo.FieldType);
1136 |                         if (
1137 |                             convertedValue != null
1138 |                             && !object.Equals(fieldInfo.GetValue(target), convertedValue)
1139 |                         )
1140 |                         {
1141 |                             fieldInfo.SetValue(target, convertedValue);
1142 |                             return true;
1143 |                         }
1144 |                     }
1145 |                 }
1146 |             }
1147 |             catch (Exception ex)
1148 |             {
1149 |                 Debug.LogWarning(
1150 |                     $"[SetPropertyOrField] Failed to set '{memberName}' on {type.Name}: {ex.Message}"
1151 |                 );
1152 |             }
1153 |             return false;
1154 |         }
1155 | 
1156 |         /// <summary>
1157 |         /// Simple JToken to Type conversion for common Unity types and primitives.
1158 |         /// </summary>
1159 |         private static object ConvertJTokenToType(JToken token, Type targetType)
1160 |         {
1161 |             try
1162 |             {
1163 |                 if (token == null || token.Type == JTokenType.Null)
1164 |                     return null;
1165 | 
1166 |                 if (targetType == typeof(string))
1167 |                     return token.ToObject<string>();
1168 |                 if (targetType == typeof(int))
1169 |                     return token.ToObject<int>();
1170 |                 if (targetType == typeof(float))
1171 |                     return token.ToObject<float>();
1172 |                 if (targetType == typeof(bool))
1173 |                     return token.ToObject<bool>();
1174 |                 if (targetType == typeof(Vector2) && token is JArray arrV2 && arrV2.Count == 2)
1175 |                     return new Vector2(arrV2[0].ToObject<float>(), arrV2[1].ToObject<float>());
1176 |                 if (targetType == typeof(Vector3) && token is JArray arrV3 && arrV3.Count == 3)
1177 |                     return new Vector3(
1178 |                         arrV3[0].ToObject<float>(),
1179 |                         arrV3[1].ToObject<float>(),
1180 |                         arrV3[2].ToObject<float>()
1181 |                     );
1182 |                 if (targetType == typeof(Vector4) && token is JArray arrV4 && arrV4.Count == 4)
1183 |                     return new Vector4(
1184 |                         arrV4[0].ToObject<float>(),
1185 |                         arrV4[1].ToObject<float>(),
1186 |                         arrV4[2].ToObject<float>(),
1187 |                         arrV4[3].ToObject<float>()
1188 |                     );
1189 |                 if (targetType == typeof(Quaternion) && token is JArray arrQ && arrQ.Count == 4)
1190 |                     return new Quaternion(
1191 |                         arrQ[0].ToObject<float>(),
1192 |                         arrQ[1].ToObject<float>(),
1193 |                         arrQ[2].ToObject<float>(),
1194 |                         arrQ[3].ToObject<float>()
1195 |                     );
1196 |                 if (targetType == typeof(Color) && token is JArray arrC && arrC.Count >= 3) // Allow RGB or RGBA
1197 |                     return new Color(
1198 |                         arrC[0].ToObject<float>(),
1199 |                         arrC[1].ToObject<float>(),
1200 |                         arrC[2].ToObject<float>(),
1201 |                         arrC.Count > 3 ? arrC[3].ToObject<float>() : 1.0f
1202 |                     );
1203 |                 if (targetType.IsEnum)
1204 |                     return Enum.Parse(targetType, token.ToString(), true); // Case-insensitive enum parsing
1205 | 
1206 |                 // Handle loading Unity Objects (Materials, Textures, etc.) by path
1207 |                 if (
1208 |                     typeof(UnityEngine.Object).IsAssignableFrom(targetType)
1209 |                     && token.Type == JTokenType.String
1210 |                 )
1211 |                 {
1212 |                     string assetPath = AssetPathUtility.SanitizeAssetPath(token.ToString());
1213 |                     UnityEngine.Object loadedAsset = AssetDatabase.LoadAssetAtPath(
1214 |                         assetPath,
1215 |                         targetType
1216 |                     );
1217 |                     if (loadedAsset == null)
1218 |                     {
1219 |                         Debug.LogWarning(
1220 |                             $"[ConvertJTokenToType] Could not load asset of type {targetType.Name} from path: {assetPath}"
1221 |                         );
1222 |                     }
1223 |                     return loadedAsset;
1224 |                 }
1225 | 
1226 |                 // Fallback: Try direct conversion (might work for other simple value types)
1227 |                 return token.ToObject(targetType);
1228 |             }
1229 |             catch (Exception ex)
1230 |             {
1231 |                 Debug.LogWarning(
1232 |                     $"[ConvertJTokenToType] Could not convert JToken '{token}' (type {token.Type}) to type '{targetType.Name}': {ex.Message}"
1233 |                 );
1234 |                 return null;
1235 |             }
1236 |         }
1237 | 
1238 | 
1239 |         // --- Data Serialization ---
1240 | 
1241 |         /// <summary>
1242 |         /// Creates a serializable representation of an asset.
1243 |         /// </summary>
1244 |         private static object GetAssetData(string path, bool generatePreview = false)
1245 |         {
1246 |             if (string.IsNullOrEmpty(path) || !AssetExists(path))
1247 |                 return null;
1248 | 
1249 |             string guid = AssetDatabase.AssetPathToGUID(path);
1250 |             Type assetType = AssetDatabase.GetMainAssetTypeAtPath(path);
1251 |             UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
1252 |             string previewBase64 = null;
1253 |             int previewWidth = 0;
1254 |             int previewHeight = 0;
1255 | 
1256 |             if (generatePreview && asset != null)
1257 |             {
1258 |                 Texture2D preview = AssetPreview.GetAssetPreview(asset);
1259 | 
1260 |                 if (preview != null)
1261 |                 {
1262 |                     try
1263 |                     {
1264 |                         // Ensure texture is readable for EncodeToPNG
1265 |                         // Creating a temporary readable copy is safer
1266 |                         RenderTexture rt = null;
1267 |                         Texture2D readablePreview = null;
1268 |                         RenderTexture previous = RenderTexture.active;
1269 |                         try
1270 |                         {
1271 |                             rt = RenderTexture.GetTemporary(preview.width, preview.height);
1272 |                             Graphics.Blit(preview, rt);
1273 |                             RenderTexture.active = rt;
1274 |                             readablePreview = new Texture2D(preview.width, preview.height, TextureFormat.RGB24, false);
1275 |                             readablePreview.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
1276 |                             readablePreview.Apply();
1277 | 
1278 |                             var pngData = readablePreview.EncodeToPNG();
1279 |                             if (pngData != null && pngData.Length > 0)
1280 |                             {
1281 |                                 previewBase64 = Convert.ToBase64String(pngData);
1282 |                                 previewWidth = readablePreview.width;
1283 |                                 previewHeight = readablePreview.height;
1284 |                             }
1285 |                         }
1286 |                         finally
1287 |                         {
1288 |                             RenderTexture.active = previous;
1289 |                             if (rt != null) RenderTexture.ReleaseTemporary(rt);
1290 |                             if (readablePreview != null) UnityEngine.Object.DestroyImmediate(readablePreview);
1291 |                         }
1292 |                     }
1293 |                     catch (Exception ex)
1294 |                     {
1295 |                         Debug.LogWarning(
1296 |                             $"Failed to generate readable preview for '{path}': {ex.Message}. Preview might not be readable."
1297 |                         );
1298 |                         // Fallback: Try getting static preview if available?
1299 |                         // Texture2D staticPreview = AssetPreview.GetMiniThumbnail(asset);
1300 |                     }
1301 |                 }
1302 |                 else
1303 |                 {
1304 |                     Debug.LogWarning(
1305 |                         $"Could not get asset preview for {path} (Type: {assetType?.Name}). Is it supported?"
1306 |                     );
1307 |                 }
1308 |             }
1309 | 
1310 |             return new
1311 |             {
1312 |                 path = path,
1313 |                 guid = guid,
1314 |                 assetType = assetType?.FullName ?? "Unknown",
1315 |                 name = Path.GetFileNameWithoutExtension(path),
1316 |                 fileName = Path.GetFileName(path),
1317 |                 isFolder = AssetDatabase.IsValidFolder(path),
1318 |                 instanceID = asset?.GetInstanceID() ?? 0,
1319 |                 lastWriteTimeUtc = File.GetLastWriteTimeUtc(
1320 |                         Path.Combine(Directory.GetCurrentDirectory(), path)
1321 |                     )
1322 |                     .ToString("o"), // ISO 8601
1323 |                 // --- Preview Data ---
1324 |                 previewBase64 = previewBase64, // PNG data as Base64 string
1325 |                 previewWidth = previewWidth,
1326 |                 previewHeight = previewHeight,
1327 |                 // TODO: Add more metadata? Importer settings? Dependencies?
1328 |             };
1329 |         }
1330 |     }
1331 | }
1332 | 
```
Page 12/18FirstPrevNextLast