diff --git a/.gitignore b/.gitignore
index ddad8cbd..75612dc8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,8 +4,7 @@
CLAUDE.md
# Code-copy related files
-.codeignore
-*codeclip*
+.clipignore
# Python-generated files
__pycache__/
@@ -14,6 +13,8 @@ build/
dist/
wheels/
*.egg-info
+UnityMcpServer/**/*.meta
+UnityMcpServer.meta
# Virtual environments
.venv
@@ -21,6 +22,10 @@ wheels/
# Unity Editor
*.unitypackage
*.asset
+UnityMcpBridge.meta
+LICENSE.meta
+README.md.meta
+CONTRIBUTING.md.meta
# IDE
.idea/
diff --git a/Editor/Models/MCPConfigServers.cs b/Editor/Models/MCPConfigServers.cs
deleted file mode 100644
index de9e8757..00000000
--- a/Editor/Models/MCPConfigServers.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System;
-using Newtonsoft.Json;
-
-namespace UnityMCP.Editor.Models
-{
- [Serializable]
- public class MCPConfigServers
- {
- [JsonProperty("unityMCP")]
- public MCPConfigServer unityMCP;
- }
-}
diff --git a/Editor/Models/McpStatus.cs b/Editor/Models/McpStatus.cs
deleted file mode 100644
index 36308e5e..00000000
--- a/Editor/Models/McpStatus.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace UnityMCP.Editor.Models
-{
- // Enum representing the various status states for MCP clients
- public enum McpStatus
- {
- NotConfigured, // Not set up yet
- Configured, // Successfully configured
- Running, // Service is running
- Connected, // Successfully connected
- IncorrectPath, // Configuration has incorrect paths
- CommunicationError, // Connected but communication issues
- NoResponse, // Connected but not responding
- MissingConfig, // Config file exists but missing required elements
- UnsupportedOS, // OS is not supported
- Error // General error state
- }
-}
\ No newline at end of file
diff --git a/Editor/Tools/ManageAsset.cs b/Editor/Tools/ManageAsset.cs
deleted file mode 100644
index 138b798c..00000000
--- a/Editor/Tools/ManageAsset.cs
+++ /dev/null
@@ -1,959 +0,0 @@
-using UnityEngine;
-using UnityEditor;
-using Newtonsoft.Json.Linq;
-using System;
-using System.IO;
-using System.Linq;
-using System.Collections.Generic;
-using UnityMCP.Editor.Helpers; // For Response class
-using System.Globalization;
-
-namespace UnityMCP.Editor.Tools
-{
- ///
- /// Handles asset management operations within the Unity project.
- ///
- public static class ManageAsset
- {
- // --- Main Handler ---
-
- // Define the list of valid actions
- private static readonly List ValidActions = new List
- {
- "import", "create", "modify", "delete", "duplicate",
- "move", "rename", "search", "get_info", "create_folder",
- "get_components"
- };
-
- public static object HandleCommand(JObject @params)
- {
- string action = @params["action"]?.ToString().ToLower();
- if (string.IsNullOrEmpty(action))
- {
- return Response.Error("Action parameter is required.");
- }
-
- // Check if the action is valid before switching
- if (!ValidActions.Contains(action))
- {
- string validActionsList = string.Join(", ", ValidActions);
- return Response.Error($"Unknown action: '{action}'. Valid actions are: {validActionsList}");
- }
-
- // Common parameters
- string path = @params["path"]?.ToString();
-
- try
- {
- switch (action)
- {
- case "import":
- // Note: Unity typically auto-imports. This might re-import or configure import settings.
- return ReimportAsset(path, @params["properties"] as JObject);
- case "create":
- return CreateAsset(@params);
- case "modify":
- return ModifyAsset(path, @params["properties"] as JObject);
- case "delete":
- return DeleteAsset(path);
- case "duplicate":
- return DuplicateAsset(path, @params["destination"]?.ToString());
- case "move": // Often same as rename if within Assets/
- case "rename":
- return MoveOrRenameAsset(path, @params["destination"]?.ToString());
- case "search":
- return SearchAssets(@params);
- case "get_info":
- return GetAssetInfo(path, @params["generatePreview"]?.ToObject() ?? false);
- case "create_folder": // Added specific action for clarity
- return CreateFolder(path);
- case "get_components":
- return GetComponentsFromAsset(path);
-
- default:
- // This error message is less likely to be hit now, but kept here as a fallback or for potential future modifications.
- string validActionsListDefault = string.Join(", ", ValidActions);
- return Response.Error($"Unknown action: '{action}'. Valid actions are: {validActionsListDefault}");
- }
- }
- catch (Exception e)
- {
- Debug.LogError($"[ManageAsset] Action '{action}' failed for path '{path}': {e}");
- return Response.Error($"Internal error processing action '{action}' on '{path}': {e.Message}");
- }
- }
-
- // --- Action Implementations ---
-
- private static object ReimportAsset(string path, JObject properties)
- {
- if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for reimport.");
- string fullPath = SanitizeAssetPath(path);
- if (!AssetExists(fullPath)) return Response.Error($"Asset not found at path: {fullPath}");
-
- try
- {
- // TODO: Apply importer properties before reimporting?
- // This is complex as it requires getting the AssetImporter, casting it,
- // applying properties via reflection or specific methods, saving, then reimporting.
- if (properties != null && properties.HasValues)
- {
- Debug.LogWarning("[ManageAsset.Reimport] Modifying importer properties before reimport is not fully implemented yet.");
- // AssetImporter importer = AssetImporter.GetAtPath(fullPath);
- // if (importer != null) { /* Apply properties */ AssetDatabase.WriteImportSettingsIfDirty(fullPath); }
- }
-
- AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate);
- // AssetDatabase.Refresh(); // Usually ImportAsset handles refresh
- return Response.Success($"Asset '{fullPath}' reimported.", GetAssetData(fullPath));
- }
- catch (Exception e)
- {
- return Response.Error($"Failed to reimport asset '{fullPath}': {e.Message}");
- }
- }
-
- private static object CreateAsset(JObject @params)
- {
- string path = @params["path"]?.ToString();
- string assetType = @params["assetType"]?.ToString();
- JObject properties = @params["properties"] as JObject;
-
- if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for create.");
- if (string.IsNullOrEmpty(assetType)) return Response.Error("'assetType' is required for create.");
-
- string fullPath = SanitizeAssetPath(path);
- string directory = Path.GetDirectoryName(fullPath);
-
- // Ensure directory exists
- if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), directory)))
- {
- Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), directory));
- AssetDatabase.Refresh(); // Make sure Unity knows about the new folder
- }
-
- if (AssetExists(fullPath)) return Response.Error($"Asset already exists at path: {fullPath}");
-
- try
- {
- UnityEngine.Object newAsset = null;
- string lowerAssetType = assetType.ToLowerInvariant();
-
- // Handle common asset types
- if (lowerAssetType == "folder")
- {
- return CreateFolder(path); // Use dedicated method
- }
- else if (lowerAssetType == "material")
- {
- Material mat = new Material(Shader.Find("Standard")); // Default shader
- // TODO: Apply properties from JObject (e.g., shader name, color, texture assignments)
- if(properties != null) ApplyMaterialProperties(mat, properties);
- AssetDatabase.CreateAsset(mat, fullPath);
- newAsset = mat;
- }
- else if (lowerAssetType == "scriptableobject")
- {
- string scriptClassName = properties?["scriptClass"]?.ToString();
- if (string.IsNullOrEmpty(scriptClassName)) return Response.Error("'scriptClass' property required when creating ScriptableObject asset.");
-
- Type scriptType = FindType(scriptClassName);
- if (scriptType == null || !typeof(ScriptableObject).IsAssignableFrom(scriptType))
- {
- return Response.Error($"Script class '{scriptClassName}' not found or does not inherit from ScriptableObject.");
- }
-
- ScriptableObject so = ScriptableObject.CreateInstance(scriptType);
- // TODO: Apply properties from JObject to the ScriptableObject instance?
- AssetDatabase.CreateAsset(so, fullPath);
- newAsset = so;
- }
- else if (lowerAssetType == "prefab")
- {
- // Creating prefabs usually involves saving an existing GameObject hierarchy.
- // A common pattern is to create an empty GameObject, configure it, and then save it.
- return Response.Error("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.");
- // Example (conceptual):
- // GameObject source = GameObject.Find(properties["sourceGameObject"].ToString());
- // if(source != null) PrefabUtility.SaveAsPrefabAsset(source, fullPath);
- }
- // TODO: Add more asset types (Animation Controller, Scene, etc.)
- else
- {
- // Generic creation attempt (might fail or create empty files)
- // For some types, just creating the file might be enough if Unity imports it.
- // File.Create(Path.Combine(Directory.GetCurrentDirectory(), fullPath)).Close();
- // AssetDatabase.ImportAsset(fullPath); // Let Unity try to import it
- // newAsset = AssetDatabase.LoadAssetAtPath(fullPath);
- return Response.Error($"Creation for asset type '{assetType}' is not explicitly supported yet. Supported: Folder, Material, ScriptableObject.");
- }
-
- if (newAsset == null && !Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), fullPath))) // Check if it wasn't a folder and asset wasn't created
- {
- return Response.Error($"Failed to create asset '{assetType}' at '{fullPath}'. See logs for details.");
- }
-
- AssetDatabase.SaveAssets();
- // AssetDatabase.Refresh(); // CreateAsset often handles refresh
- return Response.Success($"Asset '{fullPath}' created successfully.", GetAssetData(fullPath));
- }
- catch (Exception e)
- {
- return Response.Error($"Failed to create asset at '{fullPath}': {e.Message}");
- }
- }
-
- private static object CreateFolder(string path)
- {
- if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for create_folder.");
- string fullPath = SanitizeAssetPath(path);
- string parentDir = Path.GetDirectoryName(fullPath);
- string folderName = Path.GetFileName(fullPath);
-
- if (AssetExists(fullPath))
- {
- // Check if it's actually a folder already
- if (AssetDatabase.IsValidFolder(fullPath))
- {
- return Response.Success($"Folder already exists at path: {fullPath}", GetAssetData(fullPath));
- }
- else
- {
- return Response.Error($"An asset (not a folder) already exists at path: {fullPath}");
- }
- }
-
- try
- {
- // Ensure parent exists
- if (!string.IsNullOrEmpty(parentDir) && !AssetDatabase.IsValidFolder(parentDir)) {
- // Recursively create parent folders if needed (AssetDatabase handles this internally)
- // Or we can do it manually: Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), parentDir)); AssetDatabase.Refresh();
- }
-
- string guid = AssetDatabase.CreateFolder(parentDir, folderName);
- if (string.IsNullOrEmpty(guid)) {
- return Response.Error($"Failed to create folder '{fullPath}'. Check logs and permissions.");
- }
-
- // AssetDatabase.Refresh(); // CreateFolder usually handles refresh
- return Response.Success($"Folder '{fullPath}' created successfully.", GetAssetData(fullPath));
- }
- catch (Exception e)
- {
- return Response.Error($"Failed to create folder '{fullPath}': {e.Message}");
- }
- }
-
- private static object ModifyAsset(string path, JObject properties)
- {
- if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for modify.");
- if (properties == null || !properties.HasValues) return Response.Error("'properties' are required for modify.");
-
- string fullPath = SanitizeAssetPath(path);
- if (!AssetExists(fullPath)) return Response.Error($"Asset not found at path: {fullPath}");
-
- try
- {
- UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath(fullPath);
- if (asset == null) return Response.Error($"Failed to load asset at path: {fullPath}");
-
- bool modified = false; // Flag to track if any changes were made
-
- // --- NEW: Handle GameObject / Prefab Component Modification ---
- if (asset is GameObject gameObject)
- {
- // Iterate through the properties JSON: keys are component names, values are properties objects for that component
- foreach (var prop in properties.Properties())
- {
- string componentName = prop.Name; // e.g., "Collectible"
- // Check if the value associated with the component name is actually an object containing properties
- if (prop.Value is JObject componentProperties && componentProperties.HasValues) // e.g., {"bobSpeed": 2.0}
- {
- // Find the component on the GameObject using the name from the JSON key
- // Using GetComponent(string) is convenient but might require exact type name or be ambiguous.
- // Consider using FindType helper if needed for more complex scenarios.
- Component targetComponent = gameObject.GetComponent(componentName);
-
- if (targetComponent != null)
- {
- // Apply the nested properties (e.g., bobSpeed) to the found component instance
- // Use |= to ensure 'modified' becomes true if any component is successfully modified
- modified |= ApplyObjectProperties(targetComponent, componentProperties);
- }
- else
- {
- // Log a warning if a specified component couldn't be found
- Debug.LogWarning($"[ManageAsset.ModifyAsset] Component '{componentName}' not found on GameObject '{gameObject.name}' in asset '{fullPath}'. Skipping modification for this component.");
- }
- }
- else
- {
- // Log a warning if the structure isn't {"ComponentName": {"prop": value}}
- // We could potentially try to apply this property directly to the GameObject here if needed,
- // but the primary goal is component modification.
- Debug.LogWarning($"[ManageAsset.ModifyAsset] Property '{prop.Name}' for GameObject modification should have a JSON object value containing component properties. Value was: {prop.Value.Type}. Skipping.");
- }
- }
- // Note: 'modified' is now true if ANY component property was successfully changed.
- }
- // --- End NEW ---
-
- // --- Existing logic for other asset types (now as else-if) ---
- // Example: Modifying a Material
- else if (asset is Material material)
- {
- // Apply properties directly to the material. If this modifies, it sets modified=true.
- // Use |= in case the asset was already marked modified by previous logic (though unlikely here)
- modified |= ApplyMaterialProperties(material, properties);
- }
- // Example: Modifying a ScriptableObject
- else if (asset is ScriptableObject so)
- {
- // Apply properties directly to the ScriptableObject.
- modified |= ApplyObjectProperties(so, properties); // General helper
- }
- // Example: Modifying TextureImporter settings
- else if (asset is Texture) {
- AssetImporter importer = AssetImporter.GetAtPath(fullPath);
- if (importer is TextureImporter textureImporter)
- {
- bool importerModified = ApplyObjectProperties(textureImporter, properties);
- if (importerModified) {
- // Importer settings need saving and reimporting
- AssetDatabase.WriteImportSettingsIfDirty(fullPath);
- AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); // Reimport to apply changes
- modified = true; // Mark overall operation as modified
- }
- }
- else {
- Debug.LogWarning($"Could not get TextureImporter for {fullPath}.");
- }
- }
- // TODO: Add modification logic for other common asset types (Models, AudioClips importers, etc.)
- else // Fallback for other asset types OR direct properties on non-GameObject assets
- {
- // This block handles non-GameObject/Material/ScriptableObject/Texture assets.
- // Attempts to apply properties directly to the asset itself.
- Debug.LogWarning($"[ManageAsset.ModifyAsset] Asset type '{asset.GetType().Name}' at '{fullPath}' is not explicitly handled for component modification. Attempting generic property setting on the asset itself.");
- modified |= ApplyObjectProperties(asset, properties);
- }
- // --- End Existing Logic ---
-
- // Check if any modification happened (either component or direct asset modification)
- if (modified)
- {
- // Mark the asset as dirty (important for prefabs/SOs) so Unity knows to save it.
- EditorUtility.SetDirty(asset);
- // Save all modified assets to disk.
- AssetDatabase.SaveAssets();
- // Refresh might be needed in some edge cases, but SaveAssets usually covers it.
- // AssetDatabase.Refresh();
- return Response.Success($"Asset '{fullPath}' modified successfully.", GetAssetData(fullPath));
- } else {
- // If no changes were made (e.g., component not found, property names incorrect, value unchanged), return a success message indicating nothing changed.
- return Response.Success($"No applicable or modifiable properties found for asset '{fullPath}'. Check component names, property names, and values.", GetAssetData(fullPath));
- // Previous message: return Response.Success($"No applicable properties found to modify for asset '{fullPath}'.", GetAssetData(fullPath));
- }
- }
- catch (Exception e)
- {
- // Log the detailed error internally
- Debug.LogError($"[ManageAsset] Action 'modify' failed for path '{path}': {e}");
- // Return a user-friendly error message
- return Response.Error($"Failed to modify asset '{fullPath}': {e.Message}");
- }
- }
-
- private static object DeleteAsset(string path)
- {
- if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for delete.");
- string fullPath = SanitizeAssetPath(path);
- if (!AssetExists(fullPath)) return Response.Error($"Asset not found at path: {fullPath}");
-
- try
- {
- bool success = AssetDatabase.DeleteAsset(fullPath);
- if (success)
- {
- // AssetDatabase.Refresh(); // DeleteAsset usually handles refresh
- return Response.Success($"Asset '{fullPath}' deleted successfully.");
- }
- else
- {
- // This might happen if the file couldn't be deleted (e.g., locked)
- return Response.Error($"Failed to delete asset '{fullPath}'. Check logs or if the file is locked.");
- }
- }
- catch (Exception e)
- {
- return Response.Error($"Error deleting asset '{fullPath}': {e.Message}");
- }
- }
-
- private static object DuplicateAsset(string path, string destinationPath)
- {
- if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for duplicate.");
-
- string sourcePath = SanitizeAssetPath(path);
- if (!AssetExists(sourcePath)) return Response.Error($"Source asset not found at path: {sourcePath}");
-
- string destPath;
- if (string.IsNullOrEmpty(destinationPath))
- {
- // Generate a unique path if destination is not provided
- destPath = AssetDatabase.GenerateUniqueAssetPath(sourcePath);
- }
- else
- {
- destPath = SanitizeAssetPath(destinationPath);
- if (AssetExists(destPath)) return Response.Error($"Asset already exists at destination path: {destPath}");
- // Ensure destination directory exists
- EnsureDirectoryExists(Path.GetDirectoryName(destPath));
- }
-
- try
- {
- bool success = AssetDatabase.CopyAsset(sourcePath, destPath);
- if (success)
- {
- // AssetDatabase.Refresh();
- return Response.Success($"Asset '{sourcePath}' duplicated to '{destPath}'.", GetAssetData(destPath));
- }
- else
- {
- return Response.Error($"Failed to duplicate asset from '{sourcePath}' to '{destPath}'.");
- }
- }
- catch (Exception e)
- {
- return Response.Error($"Error duplicating asset '{sourcePath}': {e.Message}");
- }
- }
-
- private static object MoveOrRenameAsset(string path, string destinationPath)
- {
- if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for move/rename.");
- if (string.IsNullOrEmpty(destinationPath)) return Response.Error("'destination' path is required for move/rename.");
-
- string sourcePath = SanitizeAssetPath(path);
- string destPath = SanitizeAssetPath(destinationPath);
-
- if (!AssetExists(sourcePath)) return Response.Error($"Source asset not found at path: {sourcePath}");
- if (AssetExists(destPath)) return Response.Error($"An asset already exists at the destination path: {destPath}");
-
- // Ensure destination directory exists
- EnsureDirectoryExists(Path.GetDirectoryName(destPath));
-
- try
- {
- // Validate will return an error string if failed, null if successful
- string error = AssetDatabase.ValidateMoveAsset(sourcePath, destPath);
- if (!string.IsNullOrEmpty(error))
- {
- return Response.Error($"Failed to move/rename asset from '{sourcePath}' to '{destPath}': {error}");
- }
-
- string guid = AssetDatabase.MoveAsset(sourcePath, destPath);
- if (!string.IsNullOrEmpty(guid)) // MoveAsset returns the new GUID on success
- {
- // AssetDatabase.Refresh(); // MoveAsset usually handles refresh
- return Response.Success($"Asset moved/renamed from '{sourcePath}' to '{destPath}'.", GetAssetData(destPath));
- }
- else
- {
- // This case might not be reachable if ValidateMoveAsset passes, but good to have
- return Response.Error($"MoveAsset call failed unexpectedly for '{sourcePath}' to '{destPath}'.");
- }
- }
- catch (Exception e)
- {
- return Response.Error($"Error moving/renaming asset '{sourcePath}': {e.Message}");
- }
- }
-
- private static object SearchAssets(JObject @params)
- {
- string searchPattern = @params["searchPattern"]?.ToString();
- string filterType = @params["filterType"]?.ToString();
- string pathScope = @params["path"]?.ToString(); // Use path as folder scope
- string filterDateAfterStr = @params["filterDateAfter"]?.ToString();
- int pageSize = @params["pageSize"]?.ToObject() ?? 50; // Default page size
- int pageNumber = @params["pageNumber"]?.ToObject() ?? 1; // Default page number (1-based)
- bool generatePreview = @params["generatePreview"]?.ToObject() ?? false;
-
- List searchFilters = new List();
- if (!string.IsNullOrEmpty(searchPattern)) searchFilters.Add(searchPattern);
- if (!string.IsNullOrEmpty(filterType)) searchFilters.Add($"t:{filterType}");
-
- string[] folderScope = null;
- if (!string.IsNullOrEmpty(pathScope))
- {
- folderScope = new string[] { SanitizeAssetPath(pathScope) };
- if (!AssetDatabase.IsValidFolder(folderScope[0])) {
- // Maybe the user provided a file path instead of a folder?
- // We could search in the containing folder, or return an error.
- Debug.LogWarning($"Search path '{folderScope[0]}' is not a valid folder. Searching entire project.");
- folderScope = null; // Search everywhere if path isn't a folder
- }
- }
-
- DateTime? filterDateAfter = null;
- if (!string.IsNullOrEmpty(filterDateAfterStr)) {
- if (DateTime.TryParse(filterDateAfterStr, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime parsedDate)) {
- filterDateAfter = parsedDate;
- } else {
- Debug.LogWarning($"Could not parse filterDateAfter: '{filterDateAfterStr}'. Expected ISO 8601 format.");
- }
- }
-
- try
- {
- string[] guids = AssetDatabase.FindAssets(string.Join(" ", searchFilters), folderScope);
- List