using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
namespace MCPForUnity.Editor.Tools
{
/// <summary>
/// Tool for managing components on GameObjects.
/// Actions: add, remove, set_property
///
/// This is a focused tool for component lifecycle operations.
/// For reading component data, use the unity://scene/gameobject/{id}/components resource.
/// </summary>
[McpForUnityTool("manage_components")]
public static class ManageComponents
{
/// <summary>
/// Handles the manage_components command.
/// </summary>
/// <param name="params">Command parameters</param>
/// <returns>Result of the component operation</returns>
public static object HandleCommand(JObject @params)
{
if (@params == null)
{
return new ErrorResponse("Parameters cannot be null.");
}
string action = ParamCoercion.CoerceString(@params["action"], null)?.ToLowerInvariant();
if (string.IsNullOrEmpty(action))
{
return new ErrorResponse("'action' parameter is required (add, remove, set_property).");
}
// Target resolution
JToken targetToken = @params["target"];
string searchMethod = ParamCoercion.CoerceString(@params["searchMethod"] ?? @params["search_method"], null);
if (targetToken == null)
{
return new ErrorResponse("'target' parameter is required.");
}
try
{
return action switch
{
"add" => AddComponent(@params, targetToken, searchMethod),
"remove" => RemoveComponent(@params, targetToken, searchMethod),
"set_property" => SetProperty(@params, targetToken, searchMethod),
_ => new ErrorResponse($"Unknown action: '{action}'. Supported actions: add, remove, set_property")
};
}
catch (Exception e)
{
McpLog.Error($"[ManageComponents] Action '{action}' failed: {e}");
return new ErrorResponse($"Internal error processing action '{action}': {e.Message}");
}
}
#region Action Implementations
private static object AddComponent(JObject @params, JToken targetToken, string searchMethod)
{
GameObject targetGo = FindTarget(targetToken, searchMethod);
if (targetGo == null)
{
return new ErrorResponse($"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'.");
}
string componentTypeName = ParamCoercion.CoerceString(@params["componentType"] ?? @params["component_type"], null);
if (string.IsNullOrEmpty(componentTypeName))
{
return new ErrorResponse("'componentType' parameter is required for 'add' action.");
}
// Resolve component type using unified type resolver
Type type = UnityTypeResolver.ResolveComponent(componentTypeName);
if (type == null)
{
return new ErrorResponse($"Component type '{componentTypeName}' not found. Use a fully-qualified name if needed.");
}
// Use ComponentOps for the actual operation
Component newComponent = ComponentOps.AddComponent(targetGo, type, out string error);
if (newComponent == null)
{
return new ErrorResponse(error ?? $"Failed to add component '{componentTypeName}'.");
}
// Set properties if provided
JObject properties = @params["properties"] as JObject ?? @params["componentProperties"] as JObject;
if (properties != null && properties.HasValues)
{
// Record for undo before modifying properties
Undo.RecordObject(newComponent, "Modify Component Properties");
SetPropertiesOnComponent(newComponent, properties);
}
EditorUtility.SetDirty(targetGo);
MarkOwningSceneDirty(targetGo);
return new
{
success = true,
message = $"Component '{componentTypeName}' added to '{targetGo.name}'.",
data = new
{
instanceID = targetGo.GetInstanceID(),
componentType = type.FullName,
componentInstanceID = newComponent.GetInstanceID()
}
};
}
private static object RemoveComponent(JObject @params, JToken targetToken, string searchMethod)
{
GameObject targetGo = FindTarget(targetToken, searchMethod);
if (targetGo == null)
{
return new ErrorResponse($"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'.");
}
string componentTypeName = ParamCoercion.CoerceString(@params["componentType"] ?? @params["component_type"], null);
if (string.IsNullOrEmpty(componentTypeName))
{
return new ErrorResponse("'componentType' parameter is required for 'remove' action.");
}
// Resolve component type using unified type resolver
Type type = UnityTypeResolver.ResolveComponent(componentTypeName);
if (type == null)
{
return new ErrorResponse($"Component type '{componentTypeName}' not found.");
}
// Use ComponentOps for the actual operation
bool removed = ComponentOps.RemoveComponent(targetGo, type, out string error);
if (!removed)
{
return new ErrorResponse(error ?? $"Failed to remove component '{componentTypeName}'.");
}
EditorUtility.SetDirty(targetGo);
MarkOwningSceneDirty(targetGo);
return new
{
success = true,
message = $"Component '{componentTypeName}' removed from '{targetGo.name}'.",
data = new
{
instanceID = targetGo.GetInstanceID()
}
};
}
private static object SetProperty(JObject @params, JToken targetToken, string searchMethod)
{
GameObject targetGo = FindTarget(targetToken, searchMethod);
if (targetGo == null)
{
return new ErrorResponse($"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'.");
}
string componentType = ParamCoercion.CoerceString(@params["componentType"] ?? @params["component_type"], null);
if (string.IsNullOrEmpty(componentType))
{
return new ErrorResponse("'componentType' parameter is required for 'set_property' action.");
}
// Resolve component type using unified type resolver
Type type = UnityTypeResolver.ResolveComponent(componentType);
if (type == null)
{
return new ErrorResponse($"Component type '{componentType}' not found.");
}
Component component = targetGo.GetComponent(type);
if (component == null)
{
return new ErrorResponse($"Component '{componentType}' not found on '{targetGo.name}'.");
}
// Get property and value
string propertyName = ParamCoercion.CoerceString(@params["property"], null);
JToken valueToken = @params["value"];
// Support both single property or properties object
JObject properties = @params["properties"] as JObject;
if (string.IsNullOrEmpty(propertyName) && (properties == null || !properties.HasValues))
{
return new ErrorResponse("Either 'property'+'value' or 'properties' object is required for 'set_property' action.");
}
var errors = new List<string>();
try
{
Undo.RecordObject(component, $"Set property on {componentType}");
if (!string.IsNullOrEmpty(propertyName) && valueToken != null)
{
// Single property mode
var error = TrySetProperty(component, propertyName, valueToken);
if (error != null)
{
errors.Add(error);
}
}
if (properties != null && properties.HasValues)
{
// Multiple properties mode
foreach (var prop in properties.Properties())
{
var error = TrySetProperty(component, prop.Name, prop.Value);
if (error != null)
{
errors.Add(error);
}
}
}
EditorUtility.SetDirty(component);
MarkOwningSceneDirty(targetGo);
if (errors.Count > 0)
{
return new
{
success = false,
message = $"Some properties failed to set on '{componentType}'.",
data = new
{
instanceID = targetGo.GetInstanceID(),
errors = errors
}
};
}
return new
{
success = true,
message = $"Properties set on component '{componentType}' on '{targetGo.name}'.",
data = new
{
instanceID = targetGo.GetInstanceID()
}
};
}
catch (Exception e)
{
return new ErrorResponse($"Error setting properties on component '{componentType}': {e.Message}");
}
}
#endregion
#region Helpers
/// <summary>
/// Marks the appropriate scene as dirty for the given GameObject.
/// Handles both regular scenes and prefab stages.
/// </summary>
private static void MarkOwningSceneDirty(GameObject targetGo)
{
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
if (prefabStage != null)
{
EditorSceneManager.MarkSceneDirty(prefabStage.scene);
}
else
{
EditorSceneManager.MarkSceneDirty(targetGo.scene);
}
}
private static GameObject FindTarget(JToken targetToken, string searchMethod)
{
if (targetToken == null)
return null;
// Try instance ID first
if (targetToken.Type == JTokenType.Integer)
{
int instanceId = targetToken.Value<int>();
return GameObjectLookup.FindById(instanceId);
}
string targetStr = targetToken.ToString();
// Try parsing as instance ID
if (int.TryParse(targetStr, out int parsedId))
{
var byId = GameObjectLookup.FindById(parsedId);
if (byId != null)
return byId;
}
// Use GameObjectLookup for search
return GameObjectLookup.FindByTarget(targetToken, searchMethod ?? "by_name", true);
}
private static void SetPropertiesOnComponent(Component component, JObject properties)
{
if (component == null || properties == null)
return;
var errors = new List<string>();
foreach (var prop in properties.Properties())
{
var error = TrySetProperty(component, prop.Name, prop.Value);
if (error != null)
errors.Add(error);
}
if (errors.Count > 0)
{
McpLog.Warn($"[ManageComponents] Some properties failed to set on {component.GetType().Name}: {string.Join(", ", errors)}");
}
}
/// <summary>
/// Attempts to set a property or field on a component.
/// Delegates to ComponentOps.SetProperty for unified implementation.
/// </summary>
private static string TrySetProperty(Component component, string propertyName, JToken value)
{
if (component == null || string.IsNullOrEmpty(propertyName))
return "Invalid component or property name";
if (ComponentOps.SetProperty(component, propertyName, value, out string error))
{
return null; // Success
}
McpLog.Warn($"[ManageComponents] {error}");
return error;
}
#endregion
}
}