fixed prefab creation/update errors

main
Justin Barnett 2025-03-31 10:49:35 -04:00
parent 4f5a6a0014
commit ba4c2a85bf
13 changed files with 647 additions and 213 deletions

View File

@ -17,6 +17,14 @@ namespace UnityMCP.Editor.Tools
{ {
// --- Main Handler --- // --- Main Handler ---
// Define the list of valid actions
private static readonly List<string> ValidActions = new List<string>
{
"import", "create", "modify", "delete", "duplicate",
"move", "rename", "search", "get_info", "create_folder",
"get_components"
};
public static object HandleCommand(JObject @params) public static object HandleCommand(JObject @params)
{ {
string action = @params["action"]?.ToString().ToLower(); string action = @params["action"]?.ToString().ToLower();
@ -25,6 +33,13 @@ namespace UnityMCP.Editor.Tools
return Response.Error("Action parameter is required."); 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 // Common parameters
string path = @params["path"]?.ToString(); string path = @params["path"]?.ToString();
@ -52,9 +67,13 @@ namespace UnityMCP.Editor.Tools
return GetAssetInfo(path, @params["generatePreview"]?.ToObject<bool>() ?? false); return GetAssetInfo(path, @params["generatePreview"]?.ToObject<bool>() ?? false);
case "create_folder": // Added specific action for clarity case "create_folder": // Added specific action for clarity
return CreateFolder(path); return CreateFolder(path);
case "get_components":
return GetComponentsFromAsset(path);
default: 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) catch (Exception e)
@ -239,27 +258,72 @@ namespace UnityMCP.Editor.Tools
UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(fullPath); UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(fullPath);
if (asset == null) return Response.Error($"Failed to load asset at path: {fullPath}"); if (asset == null) return Response.Error($"Failed to load asset at path: {fullPath}");
bool modified = false; bool modified = false; // Flag to track if any changes were made
// Example: Modifying a Material
if (asset is Material material) // --- 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);
} }
// Example: Modifying a ScriptableObject (more complex, needs reflection or specific interface) 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) 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 // Example: Modifying TextureImporter settings
else if (asset is Texture) { else if (asset is Texture) {
AssetImporter importer = AssetImporter.GetAtPath(fullPath); AssetImporter importer = AssetImporter.GetAtPath(fullPath);
if (importer is TextureImporter textureImporter) if (importer is TextureImporter textureImporter)
{ {
modified = ApplyObjectProperties(textureImporter, properties); bool importerModified = ApplyObjectProperties(textureImporter, properties);
if (modified) { if (importerModified) {
// Importer settings need saving // Importer settings need saving and reimporting
AssetDatabase.WriteImportSettingsIfDirty(fullPath); AssetDatabase.WriteImportSettingsIfDirty(fullPath);
AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); // Reimport to apply changes AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); // Reimport to apply changes
modified = true; // Mark overall operation as modified
} }
} }
else { else {
@ -267,24 +331,36 @@ namespace UnityMCP.Editor.Tools
} }
} }
// TODO: Add modification logic for other common asset types (Models, AudioClips importers, etc.) // 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."); // This block handles non-GameObject/Material/ScriptableObject/Texture assets.
modified = ApplyObjectProperties(asset, properties); // 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) if (modified)
{ {
EditorUtility.SetDirty(asset); // Mark the asset itself as dirty // Mark the asset as dirty (important for prefabs/SOs) so Unity knows to save it.
AssetDatabase.SaveAssets(); // Save changes to disk EditorUtility.SetDirty(asset);
// AssetDatabase.Refresh(); // SaveAssets usually handles refresh // 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)); return Response.Success($"Asset '{fullPath}' modified successfully.", GetAssetData(fullPath));
} else { } 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) 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}"); return Response.Error($"Failed to modify asset '{fullPath}': {e.Message}");
} }
} }
@ -487,6 +563,61 @@ namespace UnityMCP.Editor.Tools
} }
} }
/// <summary>
/// Retrieves components attached to a GameObject asset (like a Prefab).
/// </summary>
/// <param name="path">The asset path of the GameObject or Prefab.</param>
/// <returns>A response object containing a list of component type names or an error.</returns>
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<UnityEngine.Object>(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<Component>();
// 6. Format component data
List<object> 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<object>(); // 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 --- // --- Internal Helpers ---
/// <summary> /// <summary>

View File

@ -32,6 +32,54 @@ namespace UnityMCP.Editor.Tools
string searchMethod = @params["searchMethod"]?.ToString().ToLower(); string searchMethod = @params["searchMethod"]?.ToString().ToLower();
string name = @params["name"]?.ToString(); 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 try
{ {
switch (action) switch (action)
@ -80,27 +128,101 @@ namespace UnityMCP.Editor.Tools
bool saveAsPrefab = @params["saveAsPrefab"]?.ToObject<bool>() ?? false; bool saveAsPrefab = @params["saveAsPrefab"]?.ToObject<bool>() ?? false;
string prefabPath = @params["prefabPath"]?.ToString(); string prefabPath = @params["prefabPath"]?.ToString();
string tag = @params["tag"]?.ToString(); // Get tag for creation 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 no extension, search for the prefab by name
if (!prefabPath.Contains("/") && !prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
{
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.");
} }
if (saveAsPrefab && !prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) else if (guids.Length > 1)
{ {
return Response.Error($"'prefabPath' must end with '.prefab'. Provided: '{prefabPath}'"); 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}'");
}
}
else if (!prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
{
// 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.
} }
string primitiveType = @params["primitiveType"]?.ToString(); // Removed the early return error for missing .prefab ending.
GameObject newGo; // The logic above now handles finding or assuming the .prefab extension.
// Create primitive or empty GameObject GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(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
}
}
// --- 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
{
if (!string.IsNullOrEmpty(primitiveType)) if (!string.IsNullOrEmpty(primitiveType))
{ {
try try
{ {
PrimitiveType type = (PrimitiveType)Enum.Parse(typeof(PrimitiveType), primitiveType, true); PrimitiveType type = (PrimitiveType)Enum.Parse(typeof(PrimitiveType), primitiveType, true);
newGo = GameObject.CreatePrimitive(type); newGo = GameObject.CreatePrimitive(type);
newGo.name = name; // Set name after creation // 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) catch (ArgumentException)
{ {
@ -111,17 +233,36 @@ namespace UnityMCP.Editor.Tools
return Response.Error($"Failed to create primitive '{primitiveType}': {e.Message}"); return Response.Error($"Failed to create primitive '{primitiveType}': {e.Message}");
} }
} }
else 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); newGo = new GameObject(name);
createdNewObject = true;
} }
// Record creation for Undo (initial object) // Record creation for Undo *only* if we created a new object
// Note: Prefab saving might have its own Undo implications or require different handling. if (createdNewObject)
// PrefabUtility operations often handle their own Undo steps. {
Undo.RegisterCreatedObjectUndo(newGo, $"Create GameObject '{name}'"); Undo.RegisterCreatedObjectUndo(newGo, $"Create GameObject '{newGo.name}'");
}
}
// Set Parent (before potentially making it a prefab root) // --- 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
JToken parentToken = @params["parent"]; JToken parentToken = @params["parent"];
if (parentToken != null) if (parentToken != null)
{ {
@ -202,52 +343,85 @@ namespace UnityMCP.Editor.Tools
} }
} }
// Save as Prefab if requested // Save as Prefab ONLY if we *created* a new object AND saveAsPrefab is true
GameObject prefabInstance = newGo; // Keep track of the instance potentially linked to the prefab GameObject finalInstance = newGo; // Use this for selection and return data
if (saveAsPrefab) 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 try
{ {
// Ensure directory exists // Ensure directory exists using the final saving path
string directoryPath = System.IO.Path.GetDirectoryName(prefabPath); string directoryPath = System.IO.Path.GetDirectoryName(finalPrefabPath);
if (!System.IO.Directory.Exists(directoryPath)) if (!string.IsNullOrEmpty(directoryPath) && !System.IO.Directory.Exists(directoryPath))
{ {
System.IO.Directory.CreateDirectory(directoryPath); System.IO.Directory.CreateDirectory(directoryPath);
AssetDatabase.Refresh(); // Refresh asset database to recognize the new folder AssetDatabase.Refresh(); // Refresh asset database to recognize the new folder
Debug.Log($"[ManageGameObject.Create] Created directory for prefab: {directoryPath}"); Debug.Log($"[ManageGameObject.Create] Created directory for prefab: {directoryPath}");
} }
// Save the GameObject as a prefab asset and connect the instance // Use SaveAsPrefabAssetAndConnect with the final saving path
// Use SaveAsPrefabAssetAndConnect to keep the instance in the scene linked finalInstance = PrefabUtility.SaveAsPrefabAssetAndConnect(newGo, finalPrefabPath, InteractionMode.UserAction);
prefabInstance = PrefabUtility.SaveAsPrefabAssetAndConnect(newGo, prefabPath, InteractionMode.UserAction);
if (prefabInstance == null) if (finalInstance == null)
{ {
// Destroy the original if saving failed somehow (shouldn't usually happen if path is valid) // Destroy the original if saving failed somehow (shouldn't usually happen if path is valid)
UnityEngine.Object.DestroyImmediate(newGo); 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. // 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) catch (Exception e)
{ {
// Clean up the instance if prefab saving fails // Clean up the instance if prefab saving fails
UnityEngine.Object.DestroyImmediate(newGo); // Destroy the original attempt 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) // Select the instance in the scene (either prefab instance or newly created/saved one)
Selection.activeGameObject = prefabInstance; Selection.activeGameObject = finalInstance;
string successMessage = saveAsPrefab // Determine appropriate success message using the potentially updated or original path
? $"GameObject '{name}' created and saved as prefab to '{prefabPath}'." string messagePrefabPath = finalInstance == null ? originalPrefabPath : AssetDatabase.GetAssetPath(PrefabUtility.GetCorrespondingObjectFromSource(finalInstance) ?? (UnityEngine.Object)finalInstance);
: $"GameObject '{name}' created successfully in scene."; 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 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) private static object ModifyGameObject(JObject @params, JToken targetToken, string searchMethod)
@ -967,10 +1141,13 @@ namespace UnityMCP.Editor.Tools
{ {
try try
{ {
// Basic types first
if (targetType == typeof(string)) return token.ToObject<string>(); if (targetType == typeof(string)) return token.ToObject<string>();
if (targetType == typeof(int)) return token.ToObject<int>(); if (targetType == typeof(int)) return token.ToObject<int>();
if (targetType == typeof(float)) return token.ToObject<float>(); if (targetType == typeof(float)) return token.ToObject<float>();
if (targetType == typeof(bool)) return token.ToObject<bool>(); if (targetType == typeof(bool)) return token.ToObject<bool>();
// Vector/Quaternion/Color types
if (targetType == typeof(Vector2) && token is JArray arrV2 && arrV2.Count == 2) if (targetType == typeof(Vector2) && token is JArray arrV2 && arrV2.Count == 2)
return new Vector2(arrV2[0].ToObject<float>(), arrV2[1].ToObject<float>()); return new Vector2(arrV2[0].ToObject<float>(), arrV2[1].ToObject<float>());
if (targetType == typeof(Vector3) && token is JArray arrV3 && arrV3.Count == 3) if (targetType == typeof(Vector3) && token is JArray arrV3 && arrV3.Count == 3)
@ -981,14 +1158,77 @@ namespace UnityMCP.Editor.Tools
return new Quaternion(arrQ[0].ToObject<float>(), arrQ[1].ToObject<float>(), arrQ[2].ToObject<float>(), arrQ[3].ToObject<float>()); return new Quaternion(arrQ[0].ToObject<float>(), arrQ[1].ToObject<float>(), arrQ[2].ToObject<float>(), arrQ[3].ToObject<float>());
if (targetType == typeof(Color) && token is JArray arrC && arrC.Count >= 3) // Allow RGB or RGBA if (targetType == typeof(Color) && token is JArray arrC && arrC.Count >= 3) // Allow RGB or RGBA
return new Color(arrC[0].ToObject<float>(), arrC[1].ToObject<float>(), arrC[2].ToObject<float>(), arrC.Count > 3 ? arrC[3].ToObject<float>() : 1.0f); return new Color(arrC[0].ToObject<float>(), arrC[1].ToObject<float>(), arrC[2].ToObject<float>(), arrC.Count > 3 ? arrC[3].ToObject<float>() : 1.0f);
// Enum types
if (targetType.IsEnum) if (targetType.IsEnum)
return Enum.Parse(targetType, token.ToString(), true); // Case-insensitive enum parsing 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)) if (typeof(UnityEngine.Object).IsAssignableFrom(targetType))
{ {
// Check if the input token is a string, which we'll assume is the asset path // CASE 1: Reference is a JSON Object specifying a scene object/component find criteria
if (token.Type == JTokenType.String) 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)
{
Debug.LogWarning($"[ConvertJTokenToType] Could not find component type '{componentTypeName}' specified in reference object: {token}");
return null;
}
// 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(); string assetPath = token.ToString();
if (!string.IsNullOrEmpty(assetPath)) if (!string.IsNullOrEmpty(assetPath))
@ -1012,16 +1252,27 @@ namespace UnityMCP.Editor.Tools
return null; // Assign null if the path is empty 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()))
{
return null;
}
// CASE 4: Invalid format for Unity Object reference
else else
{ {
// 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 or a reference object to assign Unity Object of type '{targetType.Name}', but received token type '{token.Type}'. Value: {token}");
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;
} }
} }
// Fallback: Try direct conversion (might work for simple value types) // 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); 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) catch (Exception ex)
{ {

View File

@ -317,7 +317,6 @@ namespace UnityMCP.Editor.Tools
childrenData.Add(GetGameObjectDataRecursive(child.gameObject)); childrenData.Add(GetGameObjectDataRecursive(child.gameObject));
} }
// Basic info
var gameObjectData = new Dictionary<string, object> var gameObjectData = new Dictionary<string, object>
{ {
{ "name", go.name }, { "name", go.name },
@ -328,14 +327,12 @@ namespace UnityMCP.Editor.Tools
{ "isStatic", go.isStatic }, { "isStatic", go.isStatic },
{ "instanceID", go.GetInstanceID() }, // Useful unique identifier { "instanceID", go.GetInstanceID() }, // Useful unique identifier
{ "transform", new { { "transform", new {
position = go.transform.localPosition, position = new { x = go.transform.localPosition.x, y = go.transform.localPosition.y, z = go.transform.localPosition.z },
rotation = go.transform.localRotation.eulerAngles, // Euler for simplicity rotation = new { x = go.transform.localRotation.eulerAngles.x, y = go.transform.localRotation.eulerAngles.y, z = go.transform.localRotation.eulerAngles.z }, // Euler for simplicity
scale = go.transform.localScale scale = new { x = go.transform.localScale.x, y = go.transform.localScale.y, z = go.transform.localScale.z }
} }
}, },
{ "children", childrenData } { "children", childrenData }
// Add components if needed - potentially large data
// { "components", go.GetComponents<Component>().Select(c => c.GetType().FullName).ToList() }
}; };
return gameObjectData; return gameObjectData;

View File

@ -43,7 +43,8 @@ namespace UnityMCP.Editor.Tools
} }
// Ensure path is relative to Assets/, removing any leading "Assets/" // 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)) if (!string.IsNullOrEmpty(relativeDir))
{ {
relativeDir = relativeDir.Replace('\\', '/').Trim('/'); relativeDir = relativeDir.Replace('\\', '/').Trim('/');
@ -52,6 +53,10 @@ namespace UnityMCP.Editor.Tools
relativeDir = relativeDir.Substring("Assets/".Length).TrimStart('/'); 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 // Construct paths
string scriptFileName = $"{name}.cs"; string scriptFileName = $"{name}.cs";

View File

@ -18,9 +18,9 @@ namespace UnityMCP.Editor.Tools
public static class ReadConsole public static class ReadConsole
{ {
// Reflection members for accessing internal LogEntry data // 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 _startGettingEntriesMethod;
private static MethodInfo _stopGettingEntriesMethod; private static MethodInfo _endGettingEntriesMethod; // Renamed from _stopGettingEntriesMethod, trying End...
private static MethodInfo _clearMethod; private static MethodInfo _clearMethod;
private static MethodInfo _getCountMethod; private static MethodInfo _getCountMethod;
private static MethodInfo _getEntryMethod; private static MethodInfo _getEntryMethod;
@ -38,33 +38,49 @@ namespace UnityMCP.Editor.Tools
Type logEntriesType = typeof(EditorApplication).Assembly.GetType("UnityEditor.LogEntries"); Type logEntriesType = typeof(EditorApplication).Assembly.GetType("UnityEditor.LogEntries");
if (logEntriesType == null) throw new Exception("Could not find internal type UnityEditor.LogEntries"); if (logEntriesType == null) throw new Exception("Could not find internal type UnityEditor.LogEntries");
_getEntriesMethod = logEntriesType.GetMethod("GetEntries", BindingFlags.Static | BindingFlags.Public); // Include NonPublic binding flags as internal APIs might change accessibility
_startGettingEntriesMethod = logEntriesType.GetMethod("StartGettingEntries", BindingFlags.Static | BindingFlags.Public); BindingFlags staticFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
_stopGettingEntriesMethod = logEntriesType.GetMethod("StopGettingEntries", BindingFlags.Static | BindingFlags.Public); BindingFlags instanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
_clearMethod = logEntriesType.GetMethod("Clear", BindingFlags.Static | BindingFlags.Public);
_getCountMethod = logEntriesType.GetMethod("GetCount", BindingFlags.Static | BindingFlags.Public); _startGettingEntriesMethod = logEntriesType.GetMethod("StartGettingEntries", staticFlags);
_getEntryMethod = logEntriesType.GetMethod("GetEntryInternal", BindingFlags.Static | BindingFlags.Public); 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"); Type logEntryType = typeof(EditorApplication).Assembly.GetType("UnityEditor.LogEntry");
if (logEntryType == null) throw new Exception("Could not find internal type UnityEditor.LogEntry"); if (logEntryType == null) throw new Exception("Could not find internal type UnityEditor.LogEntry");
_modeField = logEntryType.GetField("mode", BindingFlags.Instance | BindingFlags.Public); _modeField = logEntryType.GetField("mode", instanceFlags);
_messageField = logEntryType.GetField("message", BindingFlags.Instance | BindingFlags.Public); if (_modeField == null) throw new Exception("Failed to reflect LogEntry.mode");
_fileField = logEntryType.GetField("file", BindingFlags.Instance | BindingFlags.Public);
_lineField = logEntryType.GetField("line", BindingFlags.Instance | BindingFlags.Public);
_instanceIdField = logEntryType.GetField("instanceID", BindingFlags.Instance | BindingFlags.Public);
// Basic check if reflection worked _messageField = logEntryType.GetField("message", instanceFlags);
if (_getEntriesMethod == null || _clearMethod == null || _modeField == null || _messageField == null) if (_messageField == null) throw new Exception("Failed to reflect LogEntry.message");
{
throw new Exception("Failed to get required reflection members for LogEntries/LogEntry."); _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) 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. // 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; _modeField = _messageField = _fileField = _lineField = _instanceIdField = null;
} }
} }
@ -73,9 +89,13 @@ namespace UnityMCP.Editor.Tools
public static object HandleCommand(JObject @params) public static object HandleCommand(JObject @params)
{ {
// Check if reflection setup failed in static constructor // Check if ALL required reflection members were successfully initialized.
if (_clearMethod == null || _getEntriesMethod == null || _startGettingEntriesMethod == null || _stopGettingEntriesMethod == null || _getCountMethod == null || _getEntryMethod == null || _modeField == null || _messageField == null) 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."); 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); int mode = (int)_modeField.GetValue(logEntryInstance);
string message = (string)_messageField.GetValue(logEntryInstance); string message = (string)_messageField.GetValue(logEntryInstance);
string file = (string)_fileField.GetValue(logEntryInstance); string file = (string)_fileField.GetValue(logEntryInstance);
int line = (int)_lineField.GetValue(logEntryInstance); int line = (int)_lineField.GetValue(logEntryInstance);
// int instanceId = (int)_instanceIdField.GetValue(logEntryInstance); // int instanceId = (int)_instanceIdField.GetValue(logEntryInstance);
@ -220,15 +241,15 @@ namespace UnityMCP.Editor.Tools
} }
catch (Exception e) { catch (Exception e) {
Debug.LogError($"[ReadConsole] Error while retrieving log entries: {e}"); Debug.LogError($"[ReadConsole] Error while retrieving log entries: {e}");
// Ensure StopGettingEntries is called even if there's an error during iteration // Ensure EndGettingEntries is called even if there's an error during iteration
try { _stopGettingEntriesMethod.Invoke(null, null); } catch { /* Ignore nested exception */ } try { _endGettingEntriesMethod.Invoke(null, null); } catch { /* Ignore nested exception */ }
return Response.Error($"Error retrieving log entries: {e.Message}"); return Response.Error($"Error retrieving log entries: {e.Message}");
} }
finally finally
{ {
// Ensure we always call StopGettingEntries // Ensure we always call EndGettingEntries
try { _stopGettingEntriesMethod.Invoke(null, null); } catch (Exception e) { try { _endGettingEntriesMethod.Invoke(null, null); } catch (Exception e) {
Debug.LogError($"[ReadConsole] Failed to call StopGettingEntries: {e}"); Debug.LogError($"[ReadConsole] Failed to call EndGettingEntries: {e}");
// Don't return error here as we might have valid data, but log it. // 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) private static LogType GetLogTypeFromMode(int mode)
{ {
// Check for specific error/exception/assert types first // First, determine the type based on the original logic (most severe first)
// Combine general and scripting-specific bits for broader matching. LogType initialType;
if ((mode & (ModeBitError | ModeBitScriptingError | ModeBitException | ModeBitScriptingException)) != 0) { if ((mode & (ModeBitError | ModeBitScriptingError | ModeBitException | ModeBitScriptingException)) != 0) {
return LogType.Error; initialType = LogType.Error;
} }
if ((mode & (ModeBitAssert | ModeBitScriptingAssertion)) != 0) { else if ((mode & (ModeBitAssert | ModeBitScriptingAssertion)) != 0) {
return LogType.Assert; initialType = LogType.Assert;
} }
if ((mode & (ModeBitWarning | ModeBitScriptingWarning)) != 0) { else if ((mode & (ModeBitWarning | ModeBitScriptingWarning)) != 0) {
return LogType.Warning; 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;
} }
/// <summary> /// <summary>

View File

@ -55,14 +55,16 @@ def asset_creation_strategy() -> str:
"""Guide for discovering and using Unity MCP tools effectively.""" """Guide for discovering and using Unity MCP tools effectively."""
return ( return (
"Available Unity MCP Server Tools:\\n\\n" "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 and queries info.\\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.\\n"
"- `execute_menu_item`: Executes Unity Editor menu items by path (e.g., 'File/Save Project').\\n"
"- `read_console`: Reads or clears Unity console messages, with filtering options.\\n" "- `read_console`: Reads or clears Unity console messages, with filtering options.\\n"
"- `manage_scene`: Manages scenes (load, save, create, get hierarchy).\\n" "- `manage_scene`: Manages scenes.\\n"
"- `manage_gameobject`: Manages GameObjects in the scene (CRUD, find, components, assign properties).\\n" "- `manage_gameobject`: Manages GameObjects in the scene.\\n"
"- `manage_script`: Manages C# script files (CRUD).\\n" "- `manage_script`: Manages C# script files.\\n"
"- `manage_asset`: Manages project assets (import, create, modify, delete, search).\\n\\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 # Run the server

View File

@ -11,10 +11,8 @@ def register_execute_menu_item_tools(mcp: FastMCP):
async def execute_menu_item( async def execute_menu_item(
ctx: Context, ctx: Context,
menu_path: str, menu_path: str,
action: Optional[str] = 'execute', # Allows extending later (e.g., 'validate', 'get_available') action: Optional[str] = 'execute',
parameters: Optional[Dict[str, Any]] = None, # For menu items that might accept parameters (less common) parameters: Optional[Dict[str, Any]] = None,
# alias: Optional[str] = None, # Potential future addition for common commands
# context: Optional[Dict[str, Any]] = None # Potential future addition for context-specific menus
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Executes a Unity Editor menu item via its path (e.g., "File/Save Project"). """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, "action": action,
"menuPath": menu_path, "menuPath": menu_path,
"parameters": parameters if parameters else {}, "parameters": parameters if parameters else {},
# "alias": alias,
# "context": context
} }
# Remove None values # Remove None values

View File

@ -1,8 +1,11 @@
""" """
Defines the manage_asset tool for interacting with Unity assets. 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 typing import Optional, Dict, Any, List
from mcp.server.fastmcp import FastMCP, Context 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): def register_manage_asset_tools(mcp: FastMCP):
"""Registers the manage_asset tool with the MCP server.""" """Registers the manage_asset tool with the MCP server."""
@ -14,12 +17,11 @@ def register_manage_asset_tools(mcp: FastMCP):
path: str, path: str,
asset_type: Optional[str] = None, asset_type: Optional[str] = None,
properties: Optional[Dict[str, Any]] = None, properties: Optional[Dict[str, Any]] = None,
destination: Optional[str] = None, # Used for move/duplicate destination: Optional[str] = None,
generate_preview: Optional[bool] = False, generate_preview: Optional[bool] = False,
# Search specific parameters search_pattern: Optional[str] = None,
search_pattern: Optional[str] = None, # Replaces path for search action? Or use path as pattern? filter_type: Optional[str] = None,
filter_type: Optional[str] = None, # Redundant with asset_type? filter_date_after: Optional[str] = None,
filter_date_after: Optional[str] = None, # ISO 8601 format
page_size: Optional[int] = None, page_size: Optional[int] = None,
page_number: Optional[int] = None page_number: Optional[int] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
@ -27,7 +29,7 @@ def register_manage_asset_tools(mcp: FastMCP):
Args: Args:
ctx: The MCP context. 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. path: Asset path (e.g., "Materials/MyMaterial.mat") or search scope.
asset_type: Asset type (e.g., 'Material', 'Folder') - required for 'create'. asset_type: Asset type (e.g., 'Material', 'Folder') - required for 'create'.
properties: Dictionary of properties for 'create'/'modify'. 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 # Remove None values to avoid sending unnecessary nulls
params_dict = {k: v for k, v in params_dict.items() if v is not None} 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 # Get the current asyncio event loop
# The C# side expects a command type and parameters. loop = asyncio.get_running_loop()
return await ctx.send_command("manage_asset", params_dict) # 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

View File

@ -11,15 +11,9 @@ def register_manage_editor_tools(mcp: FastMCP):
action: str, action: str,
wait_for_completion: Optional[bool] = None, wait_for_completion: Optional[bool] = None,
# --- Parameters for specific actions --- # --- Parameters for specific actions ---
# For 'set_active_tool'
tool_name: Optional[str] = None, tool_name: Optional[str] = None,
# For 'add_tag', 'remove_tag'
tag_name: Optional[str] = None, tag_name: Optional[str] = None,
# For 'add_layer', 'remove_layer'
layer_name: Optional[str] = None, 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]: ) -> Dict[str, Any]:
"""Controls and queries the Unity editor's state and settings. """Controls and queries the Unity editor's state and settings.

View File

@ -9,50 +9,60 @@ def register_manage_gameobject_tools(mcp: FastMCP):
def manage_gameobject( def manage_gameobject(
ctx: Context, ctx: Context,
action: str, action: str,
target: Optional[Union[str, int]] = None, # Name, path, or instance ID target: Optional[Union[str, int]] = None,
search_method: Optional[str] = None, # by_name, by_tag, by_layer, by_component, by_id search_method: Optional[str] = None,
# --- Parameters for 'create' --- # --- Parameters for 'create' ---
name: Optional[str] = None, # Required for 'create' name: Optional[str] = None,
tag: Optional[str] = None, # Tag to assign during creation tag: Optional[str] = None,
parent: Optional[Union[str, int]] = None, # Name or ID of parent parent: Optional[Union[str, int]] = None,
position: Optional[List[float]] = None, # [x, y, z] position: Optional[List[float]] = None,
rotation: Optional[List[float]] = None, # [x, y, z] Euler angles rotation: Optional[List[float]] = None,
scale: Optional[List[float]] = None, # [x, y, z] scale: Optional[List[float]] = None,
components_to_add: Optional[List[Union[str, Dict[str, Any]]]] = None, # List of component names or dicts with properties components_to_add: Optional[List[Union[str, Dict[str, Any]]]] = None,
primitive_type: Optional[str] = None, # Optional: create primitive (Cube, Sphere, etc.) instead of empty primitive_type: Optional[str] = None,
save_as_prefab: Optional[bool] = False, # If True, save the created object as a prefab save_as_prefab: Optional[bool] = False,
prefab_path: Optional[str] = None, # Full path to save prefab (e.g., "Assets/Prefabs/MyObject.prefab"). Overrides prefab_folder. prefab_path: Optional[str] = None,
prefab_folder: Optional[str] = "Assets/Prefabs", # Default folder if prefab_path not set (e.g., "Assets/Prefabs") prefab_folder: Optional[str] = "Assets/Prefabs",
# --- Parameters for 'modify' --- # --- Parameters for 'modify' ---
new_name: Optional[str] = None, new_name: Optional[str] = None,
new_parent: Optional[Union[str, int]] = None, new_parent: Optional[Union[str, int]] = None,
set_active: Optional[bool] = None, set_active: Optional[bool] = None,
new_tag: Optional[str] = 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, 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' --- # --- Parameters for 'find' ---
search_term: Optional[str] = None, # Used with search_method (e.g., name, tag value, component type) search_term: Optional[str] = None,
find_all: Optional[bool] = False, # Find all matches or just the first? find_all: Optional[bool] = False,
search_in_children: Optional[bool] = False, # Limit search scope search_in_children: Optional[bool] = False,
search_inactive: Optional[bool] = False, # Include inactive GameObjects? search_inactive: Optional[bool] = False,
# -- Component Management Arguments -- # -- Component Management Arguments --
component_name: Optional[str] = None, # Target component for component actions component_name: Optional[str] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Manages GameObjects: create, modify, delete, find, and component operations. """Manages GameObjects: create, modify, delete, find, and component operations.
Args: 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. 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'. 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'; Action-specific arguments (e.g., name, parent, position for 'create';
component_name, component_properties for component actions; component_name for component actions;
search_term, find_all for 'find'). search_term, find_all for 'find').
Returns: Returns:
Dictionary with operation results ('success', 'message', 'data'). Dictionary with operation results ('success', 'message', 'data').
""" """
try: try:
# --- Early check for attempting to modify a prefab asset ---
# ----------------------------------------------------------
# Prepare parameters, removing None values # Prepare parameters, removing None values
params = { params = {
"action": action, "action": action,

View File

@ -12,8 +12,6 @@ def register_manage_scene_tools(mcp: FastMCP):
name: Optional[str] = None, name: Optional[str] = None,
path: Optional[str] = None, path: Optional[str] = None,
build_index: Optional[int] = 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]: ) -> Dict[str, Any]:
"""Manages Unity scenes (load, save, create, get hierarchy, etc.). """Manages Unity scenes (load, save, create, get hierarchy, etc.).

View File

@ -17,6 +17,7 @@ def register_manage_script_tools(mcp: FastMCP):
namespace: Optional[str] = None namespace: Optional[str] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Manages C# scripts in Unity (create, read, update, delete). """Manages C# scripts in Unity (create, read, update, delete).
Make reference variables public for easier access in the Unity Editor.
Args: Args:
action: Operation ('create', 'read', 'update', 'delete'). action: Operation ('create', 'read', 'update', 'delete').

View File

@ -3,21 +3,21 @@ Defines the read_console tool for accessing Unity Editor console messages.
""" """
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
from mcp.server.fastmcp import FastMCP, Context from mcp.server.fastmcp import FastMCP, Context
from unity_connection import get_unity_connection
def register_read_console_tools(mcp: FastMCP): def register_read_console_tools(mcp: FastMCP):
"""Registers the read_console tool with the MCP server.""" """Registers the read_console tool with the MCP server."""
@mcp.tool() @mcp.tool()
async def read_console( def read_console(
ctx: Context, ctx: Context,
action: Optional[str] = 'get', # Default action is to get messages action: Optional[str] = 'get',
types: Optional[List[str]] = ['error', 'warning', 'log'], # Default types to retrieve types: Optional[List[str]] = ['error', 'warning', 'log'],
count: Optional[int] = None, # Max number of messages to return (null for all matching) count: Optional[int] = None,
filter_text: Optional[str] = None, # Text to filter messages by filter_text: Optional[str] = None,
since_timestamp: Optional[str] = None, # ISO 8601 timestamp to get messages since since_timestamp: Optional[str] = None,
format: Optional[str] = 'detailed', # 'plain', 'detailed', 'json' format: Optional[str] = 'detailed',
include_stacktrace: Optional[bool] = True, # Whether to include stack traces in detailed/json formats include_stacktrace: Optional[bool] = True,
# context: Optional[Dict[str, Any]] = None # Future context
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Gets messages from or clears the Unity Editor console. """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). Dictionary with results. For 'get', includes 'data' (messages).
""" """
# Get the connection instance
bridge = get_unity_connection()
# Normalize action # Normalize action
action = action.lower() if action else 'get' action = action.lower() if action else 'get'
@ -55,6 +58,7 @@ def register_read_console_tools(mcp: FastMCP):
if 'count' not in params_dict: if 'count' not in params_dict:
params_dict['count'] = None params_dict['count'] = None
# Forward the command to the Unity editor handler # Forward the command using the bridge's send_command method
# The C# handler name might need adjustment (e.g., CommandRegistry) # The command type is the name of the tool itself in this case
return await ctx.bridge.unity_editor.HandleReadConsole(params_dict) # No await needed as send_command is synchronous
return bridge.send_command("read_console", params_dict)