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 ---
// 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)
{
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<bool>() ?? 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<UnityEngine.Object>(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}");
}
}
@ -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 ---
/// <summary>

View File

@ -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<bool>() ?? 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<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
}
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.");
}
// Set Parent (before potentially making it a prefab root)
// 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"];
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<string>();
if (targetType == typeof(int)) return token.ToObject<int>();
if (targetType == typeof(float)) return token.ToObject<float>();
if (targetType == typeof(bool)) return token.ToObject<bool>();
// Vector/Quaternion/Color types
if (targetType == typeof(Vector2) && token is JArray arrV2 && arrV2.Count == 2)
return new Vector2(arrV2[0].ToObject<float>(), arrV2[1].ToObject<float>());
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<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
return new Color(arrC[0].ToObject<float>(), arrC[1].ToObject<float>(), arrC[2].ToObject<float>(), arrC.Count > 3 ? arrC[3].ToObject<float>() : 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)
{

View File

@ -317,7 +317,6 @@ namespace UnityMCP.Editor.Tools
childrenData.Add(GetGameObjectDataRecursive(child.gameObject));
}
// Basic info
var gameObjectData = new Dictionary<string, object>
{
{ "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<Component>().Select(c => c.GetType().FullName).ToList() }
};
return gameObjectData;

View File

@ -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";

View File

@ -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.
// First, determine the type based on the original logic (most severe first)
LogType initialType;
if ((mode & (ModeBitError | ModeBitScriptingError | ModeBitException | ModeBitScriptingException)) != 0) {
return LogType.Error;
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;
}
/// <summary>

View File

@ -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

View File

@ -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

View File

@ -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)
# 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

View File

@ -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.

View File

@ -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'.
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, component_properties for component actions;
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,

View File

@ -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.).

View File

@ -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').

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 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)
# 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)