From ba4c2a85bf40a16e3f367bbb72595c96ef22893b Mon Sep 17 00:00:00 2001 From: Justin Barnett Date: Mon, 31 Mar 2025 10:49:35 -0400 Subject: [PATCH] fixed prefab creation/update errors --- Editor/Tools/ManageAsset.cs | 171 ++++++++++-- Editor/Tools/ManageGameObject.cs | 415 ++++++++++++++++++++++++------ Editor/Tools/ManageScene.cs | 9 +- Editor/Tools/ManageScript.cs | 7 +- Editor/Tools/ReadConsole.cs | 107 +++++--- Python/server.py | 16 +- Python/tools/execute_menu_item.py | 8 +- Python/tools/manage_asset.py | 32 ++- Python/tools/manage_editor.py | 6 - Python/tools/manage_gameobject.py | 58 +++-- Python/tools/manage_scene.py | 2 - Python/tools/manage_script.py | 1 + Python/tools/read_console.py | 28 +- 13 files changed, 647 insertions(+), 213 deletions(-) diff --git a/Editor/Tools/ManageAsset.cs b/Editor/Tools/ManageAsset.cs index 6e00314..138b798 100644 --- a/Editor/Tools/ManageAsset.cs +++ b/Editor/Tools/ManageAsset.cs @@ -17,6 +17,14 @@ namespace UnityMCP.Editor.Tools { // --- 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(); @@ -25,6 +33,13 @@ namespace UnityMCP.Editor.Tools 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(); @@ -52,9 +67,13 @@ namespace UnityMCP.Editor.Tools 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: - return Response.Error($"Unknown action: '{action}'."); + // 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) @@ -239,27 +258,72 @@ namespace UnityMCP.Editor.Tools UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath(fullPath); if (asset == null) return Response.Error($"Failed to load asset at path: {fullPath}"); - bool modified = false; - // Example: Modifying a Material - if (asset is Material material) + bool modified = false; // Flag to track if any changes were made + + // --- NEW: Handle GameObject / Prefab Component Modification --- + if (asset is GameObject gameObject) { - modified = ApplyMaterialProperties(material, properties); + // 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. } - // Example: Modifying a ScriptableObject (more complex, needs reflection or specific interface) + // --- 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) { - modified = ApplyObjectProperties(so, properties); // General helper + // 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) { - modified = ApplyObjectProperties(textureImporter, properties); - if (modified) { - // Importer settings need saving + 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 { @@ -267,25 +331,37 @@ namespace UnityMCP.Editor.Tools } } // TODO: Add modification logic for other common asset types (Models, AudioClips importers, etc.) - else + else // Fallback for other asset types OR direct properties on non-GameObject assets { - Debug.LogWarning($"Modification for asset type '{asset.GetType().Name}' at '{fullPath}' is not fully implemented. Attempting generic property setting."); - modified = ApplyObjectProperties(asset, properties); + // 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) - { - EditorUtility.SetDirty(asset); // Mark the asset itself as dirty - AssetDatabase.SaveAssets(); // Save changes to disk - // AssetDatabase.Refresh(); // SaveAssets usually handles refresh + { + // 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 { - return Response.Success($"No applicable properties found to modify for asset '{fullPath}'.", GetAssetData(fullPath)); + // 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) { - return Response.Error($"Failed to modify asset '{fullPath}': {e.Message}"); + // 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}"); } } @@ -486,7 +562,62 @@ namespace UnityMCP.Editor.Tools return Response.Error($"Error getting info for asset '{fullPath}': {e.Message}"); } } - + + /// + /// Retrieves components attached to a GameObject asset (like a Prefab). + /// + /// The asset path of the GameObject or Prefab. + /// A response object containing a list of component type names or an error. + private static object GetComponentsFromAsset(string path) + { + // 1. Validate input path + if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for get_components."); + + // 2. Sanitize and check existence + string fullPath = SanitizeAssetPath(path); + if (!AssetExists(fullPath)) return Response.Error($"Asset not found at path: {fullPath}"); + + try + { + // 3. Load the asset + UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath(fullPath); + if (asset == null) return Response.Error($"Failed to load asset at path: {fullPath}"); + + // 4. Check if it's a GameObject (Prefabs load as GameObjects) + GameObject gameObject = asset as GameObject; + if (gameObject == null) + { + // Also check if it's *directly* a Component type (less common for primary assets) + Component componentAsset = asset as Component; + if (componentAsset != null) { + // If the asset itself *is* a component, maybe return just its info? + // This is an edge case. Let's stick to GameObjects for now. + return Response.Error($"Asset at '{fullPath}' is a Component ({asset.GetType().FullName}), not a GameObject. Components are typically retrieved *from* a GameObject."); + } + return Response.Error($"Asset at '{fullPath}' is not a GameObject (Type: {asset.GetType().FullName}). Cannot get components from this asset type."); + } + + // 5. Get components + Component[] components = gameObject.GetComponents(); + + // 6. Format component data + List componentList = components.Select(comp => new { + typeName = comp.GetType().FullName, + instanceID = comp.GetInstanceID(), + // TODO: Add more component-specific details here if needed in the future? + // Requires reflection or specific handling per component type. + }).ToList(); // Explicit cast for clarity if needed + + // 7. Return success response + return Response.Success($"Found {componentList.Count} component(s) on asset '{fullPath}'.", componentList); + } + catch (Exception e) + { + Debug.LogError($"[ManageAsset.GetComponentsFromAsset] Error getting components for '{fullPath}': {e}"); + return Response.Error($"Error getting components for asset '{fullPath}': {e.Message}"); + } + } + // --- Internal Helpers --- /// diff --git a/Editor/Tools/ManageGameObject.cs b/Editor/Tools/ManageGameObject.cs index 1af13eb..d85c09b 100644 --- a/Editor/Tools/ManageGameObject.cs +++ b/Editor/Tools/ManageGameObject.cs @@ -32,6 +32,54 @@ namespace UnityMCP.Editor.Tools string searchMethod = @params["searchMethod"]?.ToString().ToLower(); string name = @params["name"]?.ToString(); + // --- Prefab Redirection Check --- + string targetPath = targetToken?.Type == JTokenType.String ? targetToken.ToString() : null; + if (!string.IsNullOrEmpty(targetPath) && targetPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) + { + // Allow 'create' (instantiate), 'find' (?), 'get_components' (?) + if (action == "modify" || action == "set_component_property") + { + Debug.Log($"[ManageGameObject->ManageAsset] Redirecting action '{action}' for prefab '{targetPath}' to ManageAsset."); + // Prepare params for ManageAsset.ModifyAsset + JObject assetParams = new JObject(); + assetParams["action"] = "modify"; // ManageAsset uses "modify" + assetParams["path"] = targetPath; + + // Extract properties. + // For 'set_component_property', combine componentName and componentProperties. + // For 'modify', directly use componentProperties. + JObject properties = null; + if (action == "set_component_property") + { + string compName = @params["componentName"]?.ToString(); + JObject compProps = @params["componentProperties"]?[compName] as JObject; // Handle potential nesting + if (string.IsNullOrEmpty(compName)) return Response.Error("Missing 'componentName' for 'set_component_property' on prefab."); + if (compProps == null) return Response.Error($"Missing or invalid 'componentProperties' for component '{compName}' for 'set_component_property' on prefab."); + + properties = new JObject(); + properties[compName] = compProps; + } + else // action == "modify" + { + properties = @params["componentProperties"] as JObject; + if (properties == null) return Response.Error("Missing 'componentProperties' for 'modify' action on prefab."); + } + + assetParams["properties"] = properties; + + // Call ManageAsset handler + return ManageAsset.HandleCommand(assetParams); + } + else if (action == "delete" || action == "add_component" || action == "remove_component" || action == "get_components") // Added get_components here too + { + // Explicitly block other modifications on the prefab asset itself via manage_gameobject + return Response.Error($"Action '{action}' on a prefab asset ('{targetPath}') should be performed using the 'manage_asset' command."); + } + // Allow 'create' (instantiation) and 'find' to proceed, although finding a prefab asset by path might be less common via manage_gameobject. + // No specific handling needed here, the code below will run. + } + // --- End Prefab Redirection Check --- + try { switch (action) @@ -80,48 +128,141 @@ namespace UnityMCP.Editor.Tools bool saveAsPrefab = @params["saveAsPrefab"]?.ToObject() ?? false; string prefabPath = @params["prefabPath"]?.ToString(); string tag = @params["tag"]?.ToString(); // Get tag for creation + string primitiveType = @params["primitiveType"]?.ToString(); // Keep primitiveType check + GameObject newGo = null; // Initialize as null - if (saveAsPrefab && string.IsNullOrEmpty(prefabPath)) + // --- Try Instantiating Prefab First --- + string originalPrefabPath = prefabPath; // Keep original for messages + if (!string.IsNullOrEmpty(prefabPath)) { - return Response.Error("'prefabPath' is required when 'saveAsPrefab' is true."); - } - if (saveAsPrefab && !prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) - { - return Response.Error($"'prefabPath' must end with '.prefab'. Provided: '{prefabPath}'"); - } - - string primitiveType = @params["primitiveType"]?.ToString(); - GameObject newGo; - - // Create primitive or empty GameObject - if (!string.IsNullOrEmpty(primitiveType)) - { - try + // If no extension, search for the prefab by name + if (!prefabPath.Contains("/") && !prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) { - PrimitiveType type = (PrimitiveType)Enum.Parse(typeof(PrimitiveType), primitiveType, true); - newGo = GameObject.CreatePrimitive(type); - newGo.name = name; // Set name after creation + string prefabNameOnly = prefabPath; + Debug.Log($"[ManageGameObject.Create] Searching for prefab named: '{prefabNameOnly}'"); + string[] guids = AssetDatabase.FindAssets($"t:Prefab {prefabNameOnly}"); + if (guids.Length == 0) + { + return Response.Error($"Prefab named '{prefabNameOnly}' not found anywhere in the project."); + } + else if (guids.Length > 1) + { + string foundPaths = string.Join(", ", guids.Select(g => AssetDatabase.GUIDToAssetPath(g))); + return Response.Error($"Multiple prefabs found matching name '{prefabNameOnly}': {foundPaths}. Please provide a more specific path."); + } + else // Exactly one found + { + prefabPath = AssetDatabase.GUIDToAssetPath(guids[0]); // Update prefabPath with the full path + Debug.Log($"[ManageGameObject.Create] Found unique prefab at path: '{prefabPath}'"); + } } - catch (ArgumentException) + else if (!prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) { - return Response.Error($"Invalid primitive type: '{primitiveType}'. Valid types: {string.Join(", ", Enum.GetNames(typeof(PrimitiveType)))}"); + // If it looks like a path but doesn't end with .prefab, assume user forgot it and append it. + // We could also error here, but appending might be more user-friendly. + Debug.LogWarning($"[ManageGameObject.Create] Provided prefabPath '{prefabPath}' does not end with .prefab. Assuming it's missing and appending."); + prefabPath += ".prefab"; + // Note: This path might still not exist, AssetDatabase.LoadAssetAtPath will handle that. + } + + // Removed the early return error for missing .prefab ending. + // The logic above now handles finding or assuming the .prefab extension. + + GameObject prefabAsset = AssetDatabase.LoadAssetAtPath(prefabPath); + if (prefabAsset != null) + { + try + { + // Instantiate the prefab, initially place it at the root + // Parent will be set later if specified + newGo = PrefabUtility.InstantiatePrefab(prefabAsset) as GameObject; + + if (newGo == null) + { + // This might happen if the asset exists but isn't a valid GameObject prefab somehow + Debug.LogError($"[ManageGameObject.Create] Failed to instantiate prefab at '{prefabPath}', asset might be corrupted or not a GameObject."); + return Response.Error($"Failed to instantiate prefab at '{prefabPath}'."); + } + + // Name the instance based on the 'name' parameter, not the prefab's default name + if (!string.IsNullOrEmpty(name)) + { + newGo.name = name; + } + + // Register Undo for prefab instantiation + Undo.RegisterCreatedObjectUndo(newGo, $"Instantiate Prefab '{prefabAsset.name}' as '{newGo.name}'"); + Debug.Log($"[ManageGameObject.Create] Instantiated prefab '{prefabAsset.name}' from path '{prefabPath}' as '{newGo.name}'."); + + } + catch (Exception e) + { + return Response.Error($"Error instantiating prefab '{prefabPath}': {e.Message}"); + } + } + else + { + // Only return error if prefabPath was specified but not found. + // If prefabPath was empty/null, we proceed to create primitive/empty. + Debug.LogWarning($"[ManageGameObject.Create] Prefab asset not found at path: '{prefabPath}'. Will proceed to create new object if specified."); + // Do not return error here, allow fallback to primitive/empty creation } - catch (Exception e) - { - return Response.Error($"Failed to create primitive '{primitiveType}': {e.Message}"); - } } - else + + // --- Fallback: Create Primitive or Empty GameObject --- + bool createdNewObject = false; // Flag to track if we created (not instantiated) + if (newGo == null) // Only proceed if prefab instantiation didn't happen { - newGo = new GameObject(name); + if (!string.IsNullOrEmpty(primitiveType)) + { + try + { + PrimitiveType type = (PrimitiveType)Enum.Parse(typeof(PrimitiveType), primitiveType, true); + newGo = GameObject.CreatePrimitive(type); + // Set name *after* creation for primitives + if (!string.IsNullOrEmpty(name)) newGo.name = name; + else return Response.Error("'name' parameter is required when creating a primitive."); // Name is essential + createdNewObject = true; + } + catch (ArgumentException) + { + return Response.Error($"Invalid primitive type: '{primitiveType}'. Valid types: {string.Join(", ", Enum.GetNames(typeof(PrimitiveType)))}"); + } + catch (Exception e) + { + return Response.Error($"Failed to create primitive '{primitiveType}': {e.Message}"); + } + } + else // Create empty GameObject + { + if (string.IsNullOrEmpty(name)) + { + return Response.Error("'name' parameter is required for 'create' action when not instantiating a prefab or creating a primitive."); + } + newGo = new GameObject(name); + createdNewObject = true; + } + + // Record creation for Undo *only* if we created a new object + if (createdNewObject) + { + Undo.RegisterCreatedObjectUndo(newGo, $"Create GameObject '{newGo.name}'"); + } } - // Record creation for Undo (initial object) - // Note: Prefab saving might have its own Undo implications or require different handling. - // PrefabUtility operations often handle their own Undo steps. - Undo.RegisterCreatedObjectUndo(newGo, $"Create GameObject '{name}'"); + // --- Common Setup (Parent, Transform, Tag, Components) - Applied AFTER object exists --- + if (newGo == null) + { + // Should theoretically not happen if logic above is correct, but safety check. + return Response.Error("Failed to create or instantiate the GameObject."); + } + + // Record potential changes to the existing prefab instance or the new GO + // Record transform separately in case parent changes affect it + Undo.RecordObject(newGo.transform, "Set GameObject Transform"); + Undo.RecordObject(newGo, "Set GameObject Properties"); - // Set Parent (before potentially making it a prefab root) + // Set Parent JToken parentToken = @params["parent"]; if (parentToken != null) { @@ -202,52 +343,85 @@ namespace UnityMCP.Editor.Tools } } - // Save as Prefab if requested - GameObject prefabInstance = newGo; // Keep track of the instance potentially linked to the prefab - if (saveAsPrefab) + // Save as Prefab ONLY if we *created* a new object AND saveAsPrefab is true + GameObject finalInstance = newGo; // Use this for selection and return data + if (createdNewObject && saveAsPrefab) { + string finalPrefabPath = prefabPath; // Use a separate variable for saving path + // This check should now happen *before* attempting to save + if (string.IsNullOrEmpty(finalPrefabPath)) + { + // Clean up the created object before returning error + UnityEngine.Object.DestroyImmediate(newGo); + return Response.Error("'prefabPath' is required when 'saveAsPrefab' is true and creating a new object."); + } + // Ensure the *saving* path ends with .prefab + if (!finalPrefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) + { + Debug.Log($"[ManageGameObject.Create] Appending .prefab extension to save path: '{finalPrefabPath}' -> '{finalPrefabPath}.prefab'"); + finalPrefabPath += ".prefab"; + } + + // Removed the error check here as we now ensure the extension exists + // if (!prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) + // { + // UnityEngine.Object.DestroyImmediate(newGo); + // return Response.Error($"'prefabPath' must end with '.prefab'. Provided: '{prefabPath}'"); + // } + try { - // Ensure directory exists - string directoryPath = System.IO.Path.GetDirectoryName(prefabPath); - if (!System.IO.Directory.Exists(directoryPath)) + // Ensure directory exists using the final saving path + string directoryPath = System.IO.Path.GetDirectoryName(finalPrefabPath); + if (!string.IsNullOrEmpty(directoryPath) && !System.IO.Directory.Exists(directoryPath)) { System.IO.Directory.CreateDirectory(directoryPath); AssetDatabase.Refresh(); // Refresh asset database to recognize the new folder Debug.Log($"[ManageGameObject.Create] Created directory for prefab: {directoryPath}"); } - // Save the GameObject as a prefab asset and connect the instance - // Use SaveAsPrefabAssetAndConnect to keep the instance in the scene linked - prefabInstance = PrefabUtility.SaveAsPrefabAssetAndConnect(newGo, prefabPath, InteractionMode.UserAction); + // Use SaveAsPrefabAssetAndConnect with the final saving path + finalInstance = PrefabUtility.SaveAsPrefabAssetAndConnect(newGo, finalPrefabPath, InteractionMode.UserAction); - if (prefabInstance == null) + if (finalInstance == null) { // Destroy the original if saving failed somehow (shouldn't usually happen if path is valid) UnityEngine.Object.DestroyImmediate(newGo); - return Response.Error($"Failed to save GameObject '{name}' as prefab at '{prefabPath}'. Check path and permissions."); + return Response.Error($"Failed to save GameObject '{name}' as prefab at '{finalPrefabPath}'. Check path and permissions."); } - Debug.Log($"[ManageGameObject.Create] GameObject '{name}' saved as prefab to '{prefabPath}' and instance connected."); + Debug.Log($"[ManageGameObject.Create] GameObject '{name}' saved as prefab to '{finalPrefabPath}' and instance connected."); // Mark the new prefab asset as dirty? Not usually necessary, SaveAsPrefabAsset handles it. - // EditorUtility.SetDirty(prefabInstance); // Instance is handled by SaveAsPrefabAssetAndConnect + // EditorUtility.SetDirty(finalInstance); // Instance is handled by SaveAsPrefabAssetAndConnect } catch (Exception e) { // Clean up the instance if prefab saving fails UnityEngine.Object.DestroyImmediate(newGo); // Destroy the original attempt - return Response.Error($"Error saving prefab '{prefabPath}': {e.Message}"); + return Response.Error($"Error saving prefab '{finalPrefabPath}': {e.Message}"); } } - // Select the instance in the scene (which might now be a prefab instance) - Selection.activeGameObject = prefabInstance; + // Select the instance in the scene (either prefab instance or newly created/saved one) + Selection.activeGameObject = finalInstance; - string successMessage = saveAsPrefab - ? $"GameObject '{name}' created and saved as prefab to '{prefabPath}'." - : $"GameObject '{name}' created successfully in scene."; + // Determine appropriate success message using the potentially updated or original path + string messagePrefabPath = finalInstance == null ? originalPrefabPath : AssetDatabase.GetAssetPath(PrefabUtility.GetCorrespondingObjectFromSource(finalInstance) ?? (UnityEngine.Object)finalInstance); + string successMessage; + if (!createdNewObject && !string.IsNullOrEmpty(messagePrefabPath)) // Instantiated existing prefab + { + successMessage = $"Prefab '{messagePrefabPath}' instantiated successfully as '{finalInstance.name}'."; + } + else if (createdNewObject && saveAsPrefab && !string.IsNullOrEmpty(messagePrefabPath)) // Created new and saved as prefab + { + successMessage = $"GameObject '{finalInstance.name}' created and saved as prefab to '{messagePrefabPath}'."; + } + else // Created new primitive or empty GO, didn't save as prefab + { + successMessage = $"GameObject '{finalInstance.name}' created successfully in scene."; + } // Return data for the instance in the scene - return Response.Success(successMessage, GetGameObjectData(prefabInstance)); + return Response.Success(successMessage, GetGameObjectData(finalInstance)); } private static object ModifyGameObject(JObject @params, JToken targetToken, string searchMethod) @@ -967,10 +1141,13 @@ namespace UnityMCP.Editor.Tools { try { + // Basic types first if (targetType == typeof(string)) return token.ToObject(); if (targetType == typeof(int)) return token.ToObject(); if (targetType == typeof(float)) return token.ToObject(); if (targetType == typeof(bool)) return token.ToObject(); + + // Vector/Quaternion/Color types if (targetType == typeof(Vector2) && token is JArray arrV2 && arrV2.Count == 2) return new Vector2(arrV2[0].ToObject(), arrV2[1].ToObject()); if (targetType == typeof(Vector3) && token is JArray arrV3 && arrV3.Count == 3) @@ -981,47 +1158,121 @@ namespace UnityMCP.Editor.Tools return new Quaternion(arrQ[0].ToObject(), arrQ[1].ToObject(), arrQ[2].ToObject(), arrQ[3].ToObject()); if (targetType == typeof(Color) && token is JArray arrC && arrC.Count >= 3) // Allow RGB or RGBA return new Color(arrC[0].ToObject(), arrC[1].ToObject(), arrC[2].ToObject(), arrC.Count > 3 ? arrC[3].ToObject() : 1.0f); - if (targetType.IsEnum) + + // Enum types + if (targetType.IsEnum) return Enum.Parse(targetType, token.ToString(), true); // Case-insensitive enum parsing - // Handle assigning Unity Objects (like Prefabs, Materials, Textures) using their asset path + // Handle assigning Unity Objects (Assets, Scene Objects, Components) if (typeof(UnityEngine.Object).IsAssignableFrom(targetType)) { - // Check if the input token is a string, which we'll assume is the asset path - if (token.Type == JTokenType.String) - { - string assetPath = token.ToString(); - if (!string.IsNullOrEmpty(assetPath)) - { - // Attempt to load the asset from the provided path using the target type - UnityEngine.Object loadedAsset = AssetDatabase.LoadAssetAtPath(assetPath, targetType); - if (loadedAsset != null) + // CASE 1: Reference is a JSON Object specifying a scene object/component find criteria + if (token is JObject refObject) + { + JToken findToken = refObject["find"]; + string findMethod = refObject["method"]?.ToString() ?? "by_id_or_name_or_path"; // Default search + string componentTypeName = refObject["component"]?.ToString(); + + if (findToken == null) + { + Debug.LogWarning($"[ConvertJTokenToType] Reference object missing 'find' property: {token}"); + return null; + } + + // Find the target GameObject + // Pass 'searchInactive: true' for internal lookups to be more robust + JObject findParams = new JObject(); + findParams["searchInactive"] = true; + GameObject foundGo = FindObjectInternal(findToken, findMethod, findParams); + + if (foundGo == null) + { + Debug.LogWarning($"[ConvertJTokenToType] Could not find GameObject specified by reference object: {token}"); + return null; + } + + // If a component type is specified, try to get it + if (!string.IsNullOrEmpty(componentTypeName)) + { + Type compType = FindType(componentTypeName); + if (compType == null) { - return loadedAsset; // Return the loaded asset if successful - } - else - { - // Log a warning if the asset could not be found at the path - Debug.LogWarning($"[ConvertJTokenToType] Could not load asset of type '{targetType.Name}' from path: '{assetPath}'. Make sure the path is correct and the asset exists."); + Debug.LogWarning($"[ConvertJTokenToType] Could not find component type '{componentTypeName}' specified in reference object: {token}"); return null; - } - } - else - { - // Handle cases where an empty string might be intended to clear the reference - return null; // Assign null if the path is empty - } - } - else + } + + // Ensure the targetType is assignable from the found component type + if (!targetType.IsAssignableFrom(compType)) + { + Debug.LogWarning($"[ConvertJTokenToType] Found component '{componentTypeName}' but it is not assignable to the target property type '{targetType.Name}'. Reference: {token}"); + return null; + } + + Component foundComp = foundGo.GetComponent(compType); + if (foundComp == null) + { + Debug.LogWarning($"[ConvertJTokenToType] Found GameObject '{foundGo.name}' but could not find component '{componentTypeName}' on it. Reference: {token}"); + return null; + } + return foundComp; // Return the found component + } + else + { + // Otherwise, return the GameObject itself, ensuring it's assignable + if (!targetType.IsAssignableFrom(typeof(GameObject))) + { + Debug.LogWarning($"[ConvertJTokenToType] Found GameObject '{foundGo.name}' but it is not assignable to the target property type '{targetType.Name}' (component name was not specified). Reference: {token}"); + return null; + } + return foundGo; // Return the found GameObject + } + } + // CASE 2: Reference is a string, assume it's an asset path + else if (token.Type == JTokenType.String) + { + string assetPath = token.ToString(); + if (!string.IsNullOrEmpty(assetPath)) + { + // Attempt to load the asset from the provided path using the target type + UnityEngine.Object loadedAsset = AssetDatabase.LoadAssetAtPath(assetPath, targetType); + if (loadedAsset != null) + { + return loadedAsset; // Return the loaded asset if successful + } + else + { + // Log a warning if the asset could not be found at the path + Debug.LogWarning($"[ConvertJTokenToType] Could not load asset of type '{targetType.Name}' from path: '{assetPath}'. Make sure the path is correct and the asset exists."); + return null; + } + } + else + { + // Handle cases where an empty string might be intended to clear the reference + return null; // Assign null if the path is empty + } + } + // CASE 3: Reference is null or empty JToken, assign null + else if (token.Type == JTokenType.Null || string.IsNullOrEmpty(token.ToString())) { - // Log a warning if the input token is not a string (path) for a Unity Object assignment - Debug.LogWarning($"[ConvertJTokenToType] Expected a string asset path to assign Unity Object of type '{targetType.Name}', but received token type '{token.Type}'. Value: {token}"); - return null; + return null; } + // CASE 4: Invalid format for Unity Object reference + else + { + Debug.LogWarning($"[ConvertJTokenToType] Expected a string asset path or a reference object to assign Unity Object of type '{targetType.Name}', but received token type '{token.Type}'. Value: {token}"); + return null; + } } - // Fallback: Try direct conversion (might work for simple value types) - return token.ToObject(targetType); + // Fallback: Try direct conversion (might work for other simple value types) + // Be cautious here, this might throw errors for complex types not handled above + try { + return token.ToObject(targetType); + } catch (Exception directConversionEx) { + Debug.LogWarning($"[ConvertJTokenToType] Direct conversion failed for JToken '{token}' to type '{targetType.Name}': {directConversionEx.Message}. Specific handling might be needed."); + return null; + } } catch (Exception ex) { diff --git a/Editor/Tools/ManageScene.cs b/Editor/Tools/ManageScene.cs index aae70ce..7ef8d02 100644 --- a/Editor/Tools/ManageScene.cs +++ b/Editor/Tools/ManageScene.cs @@ -317,7 +317,6 @@ namespace UnityMCP.Editor.Tools childrenData.Add(GetGameObjectDataRecursive(child.gameObject)); } - // Basic info var gameObjectData = new Dictionary { { "name", go.name }, @@ -328,14 +327,12 @@ namespace UnityMCP.Editor.Tools { "isStatic", go.isStatic }, { "instanceID", go.GetInstanceID() }, // Useful unique identifier { "transform", new { - position = go.transform.localPosition, - rotation = go.transform.localRotation.eulerAngles, // Euler for simplicity - scale = go.transform.localScale + position = new { x = go.transform.localPosition.x, y = go.transform.localPosition.y, z = go.transform.localPosition.z }, + rotation = new { x = go.transform.localRotation.eulerAngles.x, y = go.transform.localRotation.eulerAngles.y, z = go.transform.localRotation.eulerAngles.z }, // Euler for simplicity + scale = new { x = go.transform.localScale.x, y = go.transform.localScale.y, z = go.transform.localScale.z } } }, { "children", childrenData } - // Add components if needed - potentially large data - // { "components", go.GetComponents().Select(c => c.GetType().FullName).ToList() } }; return gameObjectData; diff --git a/Editor/Tools/ManageScript.cs b/Editor/Tools/ManageScript.cs index 6c049d3..ef73210 100644 --- a/Editor/Tools/ManageScript.cs +++ b/Editor/Tools/ManageScript.cs @@ -43,7 +43,8 @@ namespace UnityMCP.Editor.Tools } // Ensure path is relative to Assets/, removing any leading "Assets/" - string relativeDir = path ?? string.Empty; + // Set default directory to "Scripts" if path is not provided + string relativeDir = path ?? "Scripts"; // Default to "Scripts" if path is null if (!string.IsNullOrEmpty(relativeDir)) { relativeDir = relativeDir.Replace('\\', '/').Trim('/'); @@ -52,6 +53,10 @@ namespace UnityMCP.Editor.Tools relativeDir = relativeDir.Substring("Assets/".Length).TrimStart('/'); } } + // Handle empty string case explicitly after processing + if (string.IsNullOrEmpty(relativeDir)) { + relativeDir = "Scripts"; // Ensure default if path was provided as "" or only "/" or "Assets/" + } // Construct paths string scriptFileName = $"{name}.cs"; diff --git a/Editor/Tools/ReadConsole.cs b/Editor/Tools/ReadConsole.cs index cc1353a..f58a097 100644 --- a/Editor/Tools/ReadConsole.cs +++ b/Editor/Tools/ReadConsole.cs @@ -18,9 +18,9 @@ namespace UnityMCP.Editor.Tools public static class ReadConsole { // Reflection members for accessing internal LogEntry data - private static MethodInfo _getEntriesMethod; + // private static MethodInfo _getEntriesMethod; // Removed as it's unused and fails reflection private static MethodInfo _startGettingEntriesMethod; - private static MethodInfo _stopGettingEntriesMethod; + private static MethodInfo _endGettingEntriesMethod; // Renamed from _stopGettingEntriesMethod, trying End... private static MethodInfo _clearMethod; private static MethodInfo _getCountMethod; private static MethodInfo _getEntryMethod; @@ -38,33 +38,49 @@ namespace UnityMCP.Editor.Tools Type logEntriesType = typeof(EditorApplication).Assembly.GetType("UnityEditor.LogEntries"); if (logEntriesType == null) throw new Exception("Could not find internal type UnityEditor.LogEntries"); - _getEntriesMethod = logEntriesType.GetMethod("GetEntries", BindingFlags.Static | BindingFlags.Public); - _startGettingEntriesMethod = logEntriesType.GetMethod("StartGettingEntries", BindingFlags.Static | BindingFlags.Public); - _stopGettingEntriesMethod = logEntriesType.GetMethod("StopGettingEntries", BindingFlags.Static | BindingFlags.Public); - _clearMethod = logEntriesType.GetMethod("Clear", BindingFlags.Static | BindingFlags.Public); - _getCountMethod = logEntriesType.GetMethod("GetCount", BindingFlags.Static | BindingFlags.Public); - _getEntryMethod = logEntriesType.GetMethod("GetEntryInternal", BindingFlags.Static | BindingFlags.Public); + // Include NonPublic binding flags as internal APIs might change accessibility + BindingFlags staticFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; + BindingFlags instanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + _startGettingEntriesMethod = logEntriesType.GetMethod("StartGettingEntries", staticFlags); + if (_startGettingEntriesMethod == null) throw new Exception("Failed to reflect LogEntries.StartGettingEntries"); + + // Try reflecting EndGettingEntries based on warning message + _endGettingEntriesMethod = logEntriesType.GetMethod("EndGettingEntries", staticFlags); + if (_endGettingEntriesMethod == null) throw new Exception("Failed to reflect LogEntries.EndGettingEntries"); + + _clearMethod = logEntriesType.GetMethod("Clear", staticFlags); + if (_clearMethod == null) throw new Exception("Failed to reflect LogEntries.Clear"); + + _getCountMethod = logEntriesType.GetMethod("GetCount", staticFlags); + if (_getCountMethod == null) throw new Exception("Failed to reflect LogEntries.GetCount"); + + _getEntryMethod = logEntriesType.GetMethod("GetEntryInternal", staticFlags); + if (_getEntryMethod == null) throw new Exception("Failed to reflect LogEntries.GetEntryInternal"); Type logEntryType = typeof(EditorApplication).Assembly.GetType("UnityEditor.LogEntry"); if (logEntryType == null) throw new Exception("Could not find internal type UnityEditor.LogEntry"); - _modeField = logEntryType.GetField("mode", BindingFlags.Instance | BindingFlags.Public); - _messageField = logEntryType.GetField("message", BindingFlags.Instance | BindingFlags.Public); - _fileField = logEntryType.GetField("file", BindingFlags.Instance | BindingFlags.Public); - _lineField = logEntryType.GetField("line", BindingFlags.Instance | BindingFlags.Public); - _instanceIdField = logEntryType.GetField("instanceID", BindingFlags.Instance | BindingFlags.Public); + _modeField = logEntryType.GetField("mode", instanceFlags); + if (_modeField == null) throw new Exception("Failed to reflect LogEntry.mode"); - // Basic check if reflection worked - if (_getEntriesMethod == null || _clearMethod == null || _modeField == null || _messageField == null) - { - throw new Exception("Failed to get required reflection members for LogEntries/LogEntry."); - } + _messageField = logEntryType.GetField("message", instanceFlags); + if (_messageField == null) throw new Exception("Failed to reflect LogEntry.message"); + + _fileField = logEntryType.GetField("file", instanceFlags); + if (_fileField == null) throw new Exception("Failed to reflect LogEntry.file"); + + _lineField = logEntryType.GetField("line", instanceFlags); + if (_lineField == null) throw new Exception("Failed to reflect LogEntry.line"); + + _instanceIdField = logEntryType.GetField("instanceID", instanceFlags); + if (_instanceIdField == null) throw new Exception("Failed to reflect LogEntry.instanceID"); } catch (Exception e) { - Debug.LogError($"[ReadConsole] Static Initialization Failed: Could not setup reflection for LogEntries. Console reading/clearing will likely fail. Error: {e}"); + Debug.LogError($"[ReadConsole] Static Initialization Failed: Could not setup reflection for LogEntries/LogEntry. Console reading/clearing will likely fail. Specific Error: {e.Message}"); // Set members to null to prevent NullReferenceExceptions later, HandleCommand should check this. - _getEntriesMethod = _startGettingEntriesMethod = _stopGettingEntriesMethod = _clearMethod = _getCountMethod = _getEntryMethod = null; + _startGettingEntriesMethod = _endGettingEntriesMethod = _clearMethod = _getCountMethod = _getEntryMethod = null; _modeField = _messageField = _fileField = _lineField = _instanceIdField = null; } } @@ -73,9 +89,13 @@ namespace UnityMCP.Editor.Tools public static object HandleCommand(JObject @params) { - // Check if reflection setup failed in static constructor - if (_clearMethod == null || _getEntriesMethod == null || _startGettingEntriesMethod == null || _stopGettingEntriesMethod == null || _getCountMethod == null || _getEntryMethod == null || _modeField == null || _messageField == null) + // Check if ALL required reflection members were successfully initialized. + if (_startGettingEntriesMethod == null || _endGettingEntriesMethod == null || + _clearMethod == null || _getCountMethod == null || _getEntryMethod == null || + _modeField == null || _messageField == null || _fileField == null || _lineField == null || _instanceIdField == null) { + // Log the error here as well for easier debugging in Unity Console + Debug.LogError("[ReadConsole] HandleCommand called but reflection members are not initialized. Static constructor might have failed silently or there's an issue."); return Response.Error("ReadConsole handler failed to initialize due to reflection errors. Cannot access console logs."); } @@ -162,6 +182,7 @@ namespace UnityMCP.Editor.Tools int mode = (int)_modeField.GetValue(logEntryInstance); string message = (string)_messageField.GetValue(logEntryInstance); string file = (string)_fileField.GetValue(logEntryInstance); + int line = (int)_lineField.GetValue(logEntryInstance); // int instanceId = (int)_instanceIdField.GetValue(logEntryInstance); @@ -220,15 +241,15 @@ namespace UnityMCP.Editor.Tools } catch (Exception e) { Debug.LogError($"[ReadConsole] Error while retrieving log entries: {e}"); - // Ensure StopGettingEntries is called even if there's an error during iteration - try { _stopGettingEntriesMethod.Invoke(null, null); } catch { /* Ignore nested exception */ } + // Ensure EndGettingEntries is called even if there's an error during iteration + try { _endGettingEntriesMethod.Invoke(null, null); } catch { /* Ignore nested exception */ } return Response.Error($"Error retrieving log entries: {e.Message}"); } finally { - // Ensure we always call StopGettingEntries - try { _stopGettingEntriesMethod.Invoke(null, null); } catch (Exception e) { - Debug.LogError($"[ReadConsole] Failed to call StopGettingEntries: {e}"); + // Ensure we always call EndGettingEntries + try { _endGettingEntriesMethod.Invoke(null, null); } catch (Exception e) { + Debug.LogError($"[ReadConsole] Failed to call EndGettingEntries: {e}"); // Don't return error here as we might have valid data, but log it. } } @@ -257,20 +278,30 @@ namespace UnityMCP.Editor.Tools private static LogType GetLogTypeFromMode(int mode) { - // Check for specific error/exception/assert types first - // Combine general and scripting-specific bits for broader matching. - if ((mode & (ModeBitError | ModeBitScriptingError | ModeBitException | ModeBitScriptingException)) != 0) { - return LogType.Error; + // First, determine the type based on the original logic (most severe first) + LogType initialType; + if ((mode & (ModeBitError | ModeBitScriptingError | ModeBitException | ModeBitScriptingException)) != 0) { + initialType = LogType.Error; } - if ((mode & (ModeBitAssert | ModeBitScriptingAssertion)) != 0) { - return LogType.Assert; + else if ((mode & (ModeBitAssert | ModeBitScriptingAssertion)) != 0) { + initialType = LogType.Assert; } - if ((mode & (ModeBitWarning | ModeBitScriptingWarning)) != 0) { - return LogType.Warning; + else if ((mode & (ModeBitWarning | ModeBitScriptingWarning)) != 0) { + initialType = LogType.Warning; + } + else { + initialType = LogType.Log; + } + + // Apply the observed "one level lower" correction + switch (initialType) + { + case LogType.Error: return LogType.Warning; // Error becomes Warning + case LogType.Warning: return LogType.Log; // Warning becomes Log + case LogType.Assert: return LogType.Assert; // Assert remains Assert (no lower level defined) + case LogType.Log: return LogType.Log; // Log remains Log + default: return LogType.Log; // Default fallback } - // If none of the above, assume it's a standard log message. - // This covers ModeBitLog and ModeBitScriptingLog. - return LogType.Log; } /// diff --git a/Python/server.py b/Python/server.py index ceca968..fc11d99 100644 --- a/Python/server.py +++ b/Python/server.py @@ -55,14 +55,16 @@ def asset_creation_strategy() -> str: """Guide for discovering and using Unity MCP tools effectively.""" return ( "Available Unity MCP Server Tools:\\n\\n" - "For detailed usage, please refer to the specific tool's documentation.\\n\\n" - "- `manage_editor`: Controls editor state (play/pause/stop) and queries info (state, selection).\\n" - "- `execute_menu_item`: Executes Unity Editor menu items by path (e.g., 'File/Save Project').\\n" + "- `manage_editor`: Controls editor state and queries info.\\n" + "- `execute_menu_item`: Executes Unity Editor menu items by path.\\n" "- `read_console`: Reads or clears Unity console messages, with filtering options.\\n" - "- `manage_scene`: Manages scenes (load, save, create, get hierarchy).\\n" - "- `manage_gameobject`: Manages GameObjects in the scene (CRUD, find, components, assign properties).\\n" - "- `manage_script`: Manages C# script files (CRUD).\\n" - "- `manage_asset`: Manages project assets (import, create, modify, delete, search).\\n\\n" + "- `manage_scene`: Manages scenes.\\n" + "- `manage_gameobject`: Manages GameObjects in the scene.\\n" + "- `manage_script`: Manages C# script files.\\n" + "- `manage_asset`: Manages prefabs and assets.\\n\\n" + "Tips:\\n" + "- Create prefabs for reusable GameObjects.\\n" + "- Always include a camera and main light in your scenes.\\n" ) # Run the server diff --git a/Python/tools/execute_menu_item.py b/Python/tools/execute_menu_item.py index 5efab12..daa45b1 100644 --- a/Python/tools/execute_menu_item.py +++ b/Python/tools/execute_menu_item.py @@ -11,10 +11,8 @@ def register_execute_menu_item_tools(mcp: FastMCP): async def execute_menu_item( ctx: Context, menu_path: str, - action: Optional[str] = 'execute', # Allows extending later (e.g., 'validate', 'get_available') - parameters: Optional[Dict[str, Any]] = None, # For menu items that might accept parameters (less common) - # alias: Optional[str] = None, # Potential future addition for common commands - # context: Optional[Dict[str, Any]] = None # Potential future addition for context-specific menus + action: Optional[str] = 'execute', + parameters: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """Executes a Unity Editor menu item via its path (e.g., "File/Save Project"). @@ -35,8 +33,6 @@ def register_execute_menu_item_tools(mcp: FastMCP): "action": action, "menuPath": menu_path, "parameters": parameters if parameters else {}, - # "alias": alias, - # "context": context } # Remove None values diff --git a/Python/tools/manage_asset.py b/Python/tools/manage_asset.py index 56be50e..492a460 100644 --- a/Python/tools/manage_asset.py +++ b/Python/tools/manage_asset.py @@ -1,8 +1,11 @@ """ Defines the manage_asset tool for interacting with Unity assets. """ +import asyncio # Added: Import asyncio for running sync code in async from typing import Optional, Dict, Any, List from mcp.server.fastmcp import FastMCP, Context +# from ..unity_connection import get_unity_connection # Original line that caused error +from unity_connection import get_unity_connection # Use absolute import relative to Python dir def register_manage_asset_tools(mcp: FastMCP): """Registers the manage_asset tool with the MCP server.""" @@ -14,12 +17,11 @@ def register_manage_asset_tools(mcp: FastMCP): path: str, asset_type: Optional[str] = None, properties: Optional[Dict[str, Any]] = None, - destination: Optional[str] = None, # Used for move/duplicate + destination: Optional[str] = None, generate_preview: Optional[bool] = False, - # Search specific parameters - search_pattern: Optional[str] = None, # Replaces path for search action? Or use path as pattern? - filter_type: Optional[str] = None, # Redundant with asset_type? - filter_date_after: Optional[str] = None, # ISO 8601 format + search_pattern: Optional[str] = None, + filter_type: Optional[str] = None, + filter_date_after: Optional[str] = None, page_size: Optional[int] = None, page_number: Optional[int] = None ) -> Dict[str, Any]: @@ -27,7 +29,7 @@ def register_manage_asset_tools(mcp: FastMCP): Args: ctx: The MCP context. - action: Operation to perform (e.g., 'import', 'create', 'search'). + action: Operation to perform (e.g., 'import', 'create', 'modify', 'delete', 'duplicate', 'move', 'rename', 'search', 'get_info', 'create_folder', 'get_components'). path: Asset path (e.g., "Materials/MyMaterial.mat") or search scope. asset_type: Asset type (e.g., 'Material', 'Folder') - required for 'create'. properties: Dictionary of properties for 'create'/'modify'. @@ -61,6 +63,18 @@ def register_manage_asset_tools(mcp: FastMCP): # Remove None values to avoid sending unnecessary nulls params_dict = {k: v for k, v in params_dict.items() if v is not None} - # Forward the command to the Unity editor handler using the send_command method - # The C# side expects a command type and parameters. - return await ctx.send_command("manage_asset", params_dict) \ No newline at end of file + # Get the current asyncio event loop + loop = asyncio.get_running_loop() + # Get the Unity connection instance + connection = get_unity_connection() + + # Run the synchronous send_command in the default executor (thread pool) + # This prevents blocking the main async event loop. + result = await loop.run_in_executor( + None, # Use default executor + connection.send_command, # The function to call + "manage_asset", # First argument for send_command + params_dict # Second argument for send_command + ) + # Return the result obtained from Unity + return result \ No newline at end of file diff --git a/Python/tools/manage_editor.py b/Python/tools/manage_editor.py index 2ff8de0..4ba65c1 100644 --- a/Python/tools/manage_editor.py +++ b/Python/tools/manage_editor.py @@ -11,15 +11,9 @@ def register_manage_editor_tools(mcp: FastMCP): action: str, wait_for_completion: Optional[bool] = None, # --- Parameters for specific actions --- - # For 'set_active_tool' tool_name: Optional[str] = None, - # For 'add_tag', 'remove_tag' tag_name: Optional[str] = None, - # For 'add_layer', 'remove_layer' layer_name: Optional[str] = None, - # Example: width: Optional[int] = None, height: Optional[int] = None - # Example: window_name: Optional[str] = None - # context: Optional[Dict[str, Any]] = None # Additional context ) -> Dict[str, Any]: """Controls and queries the Unity editor's state and settings. diff --git a/Python/tools/manage_gameobject.py b/Python/tools/manage_gameobject.py index 732781b..931f6d6 100644 --- a/Python/tools/manage_gameobject.py +++ b/Python/tools/manage_gameobject.py @@ -9,50 +9,60 @@ def register_manage_gameobject_tools(mcp: FastMCP): def manage_gameobject( ctx: Context, action: str, - target: Optional[Union[str, int]] = None, # Name, path, or instance ID - search_method: Optional[str] = None, # by_name, by_tag, by_layer, by_component, by_id + target: Optional[Union[str, int]] = None, + search_method: Optional[str] = None, # --- Parameters for 'create' --- - name: Optional[str] = None, # Required for 'create' - tag: Optional[str] = None, # Tag to assign during creation - parent: Optional[Union[str, int]] = None, # Name or ID of parent - position: Optional[List[float]] = None, # [x, y, z] - rotation: Optional[List[float]] = None, # [x, y, z] Euler angles - scale: Optional[List[float]] = None, # [x, y, z] - components_to_add: Optional[List[Union[str, Dict[str, Any]]]] = None, # List of component names or dicts with properties - primitive_type: Optional[str] = None, # Optional: create primitive (Cube, Sphere, etc.) instead of empty - save_as_prefab: Optional[bool] = False, # If True, save the created object as a prefab - prefab_path: Optional[str] = None, # Full path to save prefab (e.g., "Assets/Prefabs/MyObject.prefab"). Overrides prefab_folder. - prefab_folder: Optional[str] = "Assets/Prefabs", # Default folder if prefab_path not set (e.g., "Assets/Prefabs") + name: Optional[str] = None, + tag: Optional[str] = None, + parent: Optional[Union[str, int]] = None, + position: Optional[List[float]] = None, + rotation: Optional[List[float]] = None, + scale: Optional[List[float]] = None, + components_to_add: Optional[List[Union[str, Dict[str, Any]]]] = None, + primitive_type: Optional[str] = None, + save_as_prefab: Optional[bool] = False, + prefab_path: Optional[str] = None, + prefab_folder: Optional[str] = "Assets/Prefabs", # --- Parameters for 'modify' --- new_name: Optional[str] = None, new_parent: Optional[Union[str, int]] = None, set_active: Optional[bool] = None, new_tag: Optional[str] = None, - new_layer: Optional[Union[str, int]] = None, # Layer name or number + new_layer: Optional[Union[str, int]] = None, components_to_remove: Optional[List[str]] = None, - component_properties: Optional[Dict[str, Dict[str, Any]]] = None, # { "ComponentName": { "propName": value } } + component_properties: Optional[Dict[str, Dict[str, Any]]] = None, # --- Parameters for 'find' --- - search_term: Optional[str] = None, # Used with search_method (e.g., name, tag value, component type) - find_all: Optional[bool] = False, # Find all matches or just the first? - search_in_children: Optional[bool] = False, # Limit search scope - search_inactive: Optional[bool] = False, # Include inactive GameObjects? + search_term: Optional[str] = None, + find_all: Optional[bool] = False, + search_in_children: Optional[bool] = False, + search_inactive: Optional[bool] = False, # -- Component Management Arguments -- - component_name: Optional[str] = None, # Target component for component actions + component_name: Optional[str] = None, ) -> Dict[str, Any]: """Manages GameObjects: create, modify, delete, find, and component operations. Args: - action: Operation (e.g., 'create', 'modify', 'find', 'add_component'). + action: Operation (e.g., 'create', 'modify', 'find', 'add_component', 'remove_component', 'set_component_property'). target: GameObject identifier (name, path, ID) for modify/delete/component actions. - search_method: How to find objects ('by_name', 'by_id', 'by_path', etc.). Used with 'find'. - Action-specific arguments (e.g., name, parent, position for 'create'; - component_name, component_properties for component actions; + search_method: How to find objects ('by_name', 'by_id', 'by_path', etc.). Used with 'find' and some 'target' lookups. + component_properties: Dict mapping Component names to their properties to set. + Example: {"Rigidbody": {"mass": 10.0, "useGravity": True}}, + To set references: + - Use asset path string for Prefabs/Materials, e.g., {"MeshRenderer": {"material": "Assets/Materials/MyMat.mat"}} + - Use a dict for scene objects/components, e.g.: + {"MyScript": {"otherObject": {"find": "Player", "method": "by_name"}}} (assigns GameObject) + {"MyScript": {"playerHealth": {"find": "Player", "component": "HealthComponent"}}} (assigns Component) + Action-specific arguments (e.g., name, parent, position for 'create'; + component_name for component actions; search_term, find_all for 'find'). Returns: Dictionary with operation results ('success', 'message', 'data'). """ try: + # --- Early check for attempting to modify a prefab asset --- + # ---------------------------------------------------------- + # Prepare parameters, removing None values params = { "action": action, diff --git a/Python/tools/manage_scene.py b/Python/tools/manage_scene.py index af923c4..79e02d2 100644 --- a/Python/tools/manage_scene.py +++ b/Python/tools/manage_scene.py @@ -12,8 +12,6 @@ def register_manage_scene_tools(mcp: FastMCP): name: Optional[str] = None, path: Optional[str] = None, build_index: Optional[int] = None, - # Add other potential parameters like load_additive, etc. if needed - # context: Optional[Dict[str, Any]] = None # Future: Contextual info (e.g., current project settings) ) -> Dict[str, Any]: """Manages Unity scenes (load, save, create, get hierarchy, etc.). diff --git a/Python/tools/manage_script.py b/Python/tools/manage_script.py index 1a04f37..c6f2744 100644 --- a/Python/tools/manage_script.py +++ b/Python/tools/manage_script.py @@ -17,6 +17,7 @@ def register_manage_script_tools(mcp: FastMCP): namespace: Optional[str] = None ) -> Dict[str, Any]: """Manages C# scripts in Unity (create, read, update, delete). + Make reference variables public for easier access in the Unity Editor. Args: action: Operation ('create', 'read', 'update', 'delete'). diff --git a/Python/tools/read_console.py b/Python/tools/read_console.py index 4409f31..0de9bac 100644 --- a/Python/tools/read_console.py +++ b/Python/tools/read_console.py @@ -3,21 +3,21 @@ Defines the read_console tool for accessing Unity Editor console messages. """ from typing import Optional, List, Dict, Any from mcp.server.fastmcp import FastMCP, Context +from unity_connection import get_unity_connection def register_read_console_tools(mcp: FastMCP): """Registers the read_console tool with the MCP server.""" @mcp.tool() - async def read_console( + def read_console( ctx: Context, - action: Optional[str] = 'get', # Default action is to get messages - types: Optional[List[str]] = ['error', 'warning', 'log'], # Default types to retrieve - count: Optional[int] = None, # Max number of messages to return (null for all matching) - filter_text: Optional[str] = None, # Text to filter messages by - since_timestamp: Optional[str] = None, # ISO 8601 timestamp to get messages since - format: Optional[str] = 'detailed', # 'plain', 'detailed', 'json' - include_stacktrace: Optional[bool] = True, # Whether to include stack traces in detailed/json formats - # context: Optional[Dict[str, Any]] = None # Future context + action: Optional[str] = 'get', + types: Optional[List[str]] = ['error', 'warning', 'log'], + count: Optional[int] = None, + filter_text: Optional[str] = None, + since_timestamp: Optional[str] = None, + format: Optional[str] = 'detailed', + include_stacktrace: Optional[bool] = True, ) -> Dict[str, Any]: """Gets messages from or clears the Unity Editor console. @@ -34,6 +34,9 @@ def register_read_console_tools(mcp: FastMCP): Dictionary with results. For 'get', includes 'data' (messages). """ + # Get the connection instance + bridge = get_unity_connection() + # Normalize action action = action.lower() if action else 'get' @@ -55,6 +58,7 @@ def register_read_console_tools(mcp: FastMCP): if 'count' not in params_dict: params_dict['count'] = None - # Forward the command to the Unity editor handler - # The C# handler name might need adjustment (e.g., CommandRegistry) - return await ctx.bridge.unity_editor.HandleReadConsole(params_dict) \ No newline at end of file + # Forward the command using the bridge's send_command method + # The command type is the name of the tool itself in this case + # No await needed as send_command is synchronous + return bridge.send_command("read_console", params_dict) \ No newline at end of file