using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEditor;
namespace LocalMcp.UnityServer
{
/// <summary>
/// Provides reflection-based operations for Unity Components.
/// Handles component inspection, field/property manipulation, and method invocation.
/// </summary>
public static class ComponentReflection
{
/// <summary>
/// Lists all components attached to a GameObject.
/// </summary>
/// <param name="paramsJson">JSON parameters containing gameObjectPath</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response containing list of component names</returns>
public static string ListComponents(string paramsJson, object id)
{
try
{
var parameters = JsonUtility.FromJson<ComponentListParams>(paramsJson);
if (string.IsNullOrEmpty(parameters.gameObjectPath))
{
return JsonRpcResponseHelper.InvalidParams("gameObjectPath is required", id);
}
GameObject go = GameObject.Find(parameters.gameObjectPath);
if (go == null)
{
return JsonRpcResponseHelper.ErrorMessage($"GameObject '{parameters.gameObjectPath}' not found", id);
}
Component[] components = go.GetComponents<Component>();
List<string> componentNames = new List<string>();
foreach (Component comp in components)
{
if (comp != null)
{
componentNames.Add(comp.GetType().Name);
}
}
var result = new ComponentListResult
{
gameObject = parameters.gameObjectPath,
components = componentNames.ToArray()
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] ListComponents error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to list components: {e.Message}", id);
}
}
/// <summary>
/// Inspects a component and returns all its fields, properties, and methods.
/// </summary>
/// <param name="paramsJson">JSON parameters containing gameObjectPath and componentType</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response containing component details</returns>
public static string InspectComponent(string paramsJson, object id)
{
try
{
var parameters = JsonUtility.FromJson<ComponentInspectParams>(paramsJson);
if (string.IsNullOrEmpty(parameters.gameObjectPath))
{
return JsonRpcResponseHelper.InvalidParams("gameObjectPath is required", id);
}
if (string.IsNullOrEmpty(parameters.componentType))
{
return JsonRpcResponseHelper.InvalidParams("componentType is required", id);
}
GameObject go = GameObject.Find(parameters.gameObjectPath);
if (go == null)
{
return JsonRpcResponseHelper.ErrorMessage($"GameObject '{parameters.gameObjectPath}' not found", id);
}
Component comp = go.GetComponent(parameters.componentType);
if (comp == null)
{
return JsonRpcResponseHelper.ErrorMessage($"Component '{parameters.componentType}' not found on GameObject", id);
}
Type type = comp.GetType();
// Get fields using SerializedObject (includes private [SerializeField] fields)
List<FieldInfoData> fields = new List<FieldInfoData>();
SerializedObject serializedObject = new SerializedObject(comp);
SerializedProperty iterator = serializedObject.GetIterator();
// Iterate through all serialized properties
if (iterator.NextVisible(true))
{
do
{
// Skip the script reference
if (iterator.name == "m_Script")
continue;
object value = null;
try
{
value = GetSerializedPropertyValue(iterator);
}
catch (Exception e)
{
Debug.LogWarning($"[MCP] Failed to get property value for {iterator.name}: {e.Message}");
}
// Get actual field info for type and access level
FieldInfo fieldInfo = type.GetField(iterator.name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
string typeName = iterator.propertyType.ToString();
bool isPublic = fieldInfo != null && fieldInfo.IsPublic;
if (fieldInfo != null)
{
typeName = fieldInfo.FieldType.Name;
}
fields.Add(new FieldInfoData
{
name = iterator.name,
type = typeName,
value = value,
isPublic = isPublic
});
}
while (iterator.NextVisible(false));
}
// Get properties
List<PropertyInfoData> properties = new List<PropertyInfoData>();
PropertyInfo[] propertyInfos = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (PropertyInfo prop in propertyInfos)
{
object value = null;
if (prop.CanRead)
{
try
{
value = prop.GetValue(comp);
value = SerializeValue(value);
}
catch (Exception e)
{
Debug.LogWarning($"[MCP] Failed to get property value for {prop.Name}: {e.Message}");
}
}
properties.Add(new PropertyInfoData
{
name = prop.Name,
type = prop.PropertyType.Name,
value = value,
canRead = prop.CanRead,
canWrite = prop.CanWrite
});
}
// Get methods
List<MethodInfoData> methods = new List<MethodInfoData>();
MethodInfo[] methodInfos = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
foreach (MethodInfo method in methodInfos)
{
// Skip property accessors and special methods
if (method.IsSpecialName)
continue;
ParameterInfo[] parameters_method = method.GetParameters();
string[] parameterTypes = parameters_method.Select(p => p.ParameterType.Name).ToArray();
methods.Add(new MethodInfoData
{
name = method.Name,
returnType = method.ReturnType.Name,
parameters = parameterTypes
});
}
var result = new ComponentInspectResult
{
componentType = parameters.componentType,
fields = fields.ToArray(),
properties = properties.ToArray(),
methods = methods.ToArray()
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] InspectComponent error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to inspect component: {e.Message}", id);
}
}
/// <summary>
/// Sets a property value on a component using reflection.
/// Supports HEX color strings (#RRGGBB, #RRGGBBAA) and various data types.
/// This is optimized for runtime properties like TextMeshPro that don't use SerializedProperty.
/// </summary>
/// <param name="paramsJson">JSON parameters containing gameObjectPath, componentType, propertyName, value</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response with result</returns>
public static string SetProperty(string paramsJson, object id)
{
try
{
var parameters = JsonUtility.FromJson<ComponentSetPropertyParams>(paramsJson);
if (string.IsNullOrEmpty(parameters.gameObjectPath))
{
return JsonRpcResponseHelper.InvalidParams("gameObjectPath is required", id);
}
if (string.IsNullOrEmpty(parameters.componentType))
{
return JsonRpcResponseHelper.InvalidParams("componentType is required", id);
}
if (string.IsNullOrEmpty(parameters.propertyName))
{
return JsonRpcResponseHelper.InvalidParams("propertyName is required", id);
}
GameObject go = GameObject.Find(parameters.gameObjectPath);
if (go == null)
{
return JsonRpcResponseHelper.ErrorMessage($"GameObject '{parameters.gameObjectPath}' not found", id);
}
Component comp = go.GetComponent(parameters.componentType);
if (comp == null)
{
// Try to find component type from all assemblies
Type componentType = FindComponentType(parameters.componentType);
if (componentType != null)
{
comp = go.GetComponent(componentType);
}
}
if (comp == null)
{
return JsonRpcResponseHelper.ErrorMessage($"Component '{parameters.componentType}' not found on GameObject", id);
}
Type type = comp.GetType();
// Find property using reflection
PropertyInfo propInfo = type.GetProperty(parameters.propertyName, BindingFlags.Public | BindingFlags.Instance);
if (propInfo == null)
{
// Try case-insensitive search
propInfo = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.FirstOrDefault(p => p.Name.Equals(parameters.propertyName, StringComparison.OrdinalIgnoreCase));
}
if (propInfo == null)
{
return JsonRpcResponseHelper.ErrorMessage($"Property '{parameters.propertyName}' not found on component '{parameters.componentType}'", id);
}
if (!propInfo.CanWrite)
{
return JsonRpcResponseHelper.ErrorMessage($"Property '{parameters.propertyName}' is read-only", id);
}
// Convert value to appropriate type
object convertedValue = ConvertValueForProperty(parameters.value, propInfo.PropertyType);
// Set the property value
propInfo.SetValue(comp, convertedValue);
// Mark object as dirty for Undo support
EditorUtility.SetDirty(comp);
// Get the new value for confirmation
object newValue = propInfo.GetValue(comp);
object serializedNewValue = SerializeValue(newValue);
var result = new ComponentSetPropertyResult
{
status = "success",
message = $"Property '{parameters.propertyName}' set successfully",
propertyName = parameters.propertyName,
newValue = serializedNewValue
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] SetProperty error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to set property: {e.Message}", id);
}
}
/// <summary>
/// Converts a value to the target property type, with support for HEX colors.
/// </summary>
private static object ConvertValueForProperty(object value, Type targetType)
{
if (value == null)
return null;
string stringValue = value?.ToString();
// Handle Color with HEX support
if (targetType == typeof(Color))
{
// Check if it's a HEX color string
if (!string.IsNullOrEmpty(stringValue) && stringValue.StartsWith("#"))
{
return ParseHexColor(stringValue);
}
// Try JSON format for Color
if (!string.IsNullOrEmpty(stringValue) && stringValue.Contains("{"))
{
try
{
var data = JsonUtility.FromJson<ColorData>(stringValue);
return new Color(data.r, data.g, data.b, data.a);
}
catch { }
}
// Fallback to standard conversion
return ConvertValue(value, targetType);
}
// Handle Color32 with HEX support
if (targetType == typeof(Color32))
{
if (!string.IsNullOrEmpty(stringValue) && stringValue.StartsWith("#"))
{
Color c = ParseHexColor(stringValue);
return (Color32)c;
}
}
// Handle Vector3
if (targetType == typeof(Vector3))
{
if (!string.IsNullOrEmpty(stringValue) && stringValue.Contains("{"))
{
try
{
Vector3Data data = JsonUtility.FromJson<Vector3Data>(stringValue);
return new Vector3(data.x, data.y, data.z);
}
catch { }
}
return ConvertValue(value, targetType);
}
// Handle Vector2
if (targetType == typeof(Vector2))
{
if (!string.IsNullOrEmpty(stringValue) && stringValue.Contains("{"))
{
try
{
var data = JsonUtility.FromJson<Vector2Data>(stringValue);
return new Vector2(data.x, data.y);
}
catch { }
}
}
// Handle Vector4
if (targetType == typeof(Vector4))
{
if (!string.IsNullOrEmpty(stringValue) && stringValue.Contains("{"))
{
try
{
var data = JsonUtility.FromJson<Vector4Data>(stringValue);
return new Vector4(data.x, data.y, data.z, data.w);
}
catch { }
}
}
// Handle Quaternion
if (targetType == typeof(Quaternion))
{
if (!string.IsNullOrEmpty(stringValue) && stringValue.Contains("{"))
{
try
{
QuaternionData data = JsonUtility.FromJson<QuaternionData>(stringValue);
return new Quaternion(data.x, data.y, data.z, data.w);
}
catch { }
}
return ConvertValue(value, targetType);
}
// Handle enum
if (targetType.IsEnum)
{
return Enum.Parse(targetType, stringValue, true); // Case insensitive
}
// Handle boolean
if (targetType == typeof(bool))
{
if (bool.TryParse(stringValue, out bool boolResult))
return boolResult;
// Handle numeric boolean (0/1)
if (int.TryParse(stringValue, out int intResult))
return intResult != 0;
}
// Handle float
if (targetType == typeof(float))
{
if (float.TryParse(stringValue, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float floatResult))
return floatResult;
}
// Handle int
if (targetType == typeof(int))
{
if (int.TryParse(stringValue, out int intResult))
return intResult;
}
// Handle string
if (targetType == typeof(string))
{
return stringValue;
}
// Fallback to standard conversion
return Convert.ChangeType(value, targetType);
}
/// <summary>
/// Parses a HEX color string to Unity Color.
/// Supports #RGB, #RGBA, #RRGGBB, #RRGGBBAA formats.
/// </summary>
private static Color ParseHexColor(string hex)
{
hex = hex.TrimStart('#');
float r, g, b, a = 1f;
if (hex.Length == 3)
{
// #RGB format
r = Convert.ToInt32(hex.Substring(0, 1) + hex.Substring(0, 1), 16) / 255f;
g = Convert.ToInt32(hex.Substring(1, 1) + hex.Substring(1, 1), 16) / 255f;
b = Convert.ToInt32(hex.Substring(2, 1) + hex.Substring(2, 1), 16) / 255f;
}
else if (hex.Length == 4)
{
// #RGBA format
r = Convert.ToInt32(hex.Substring(0, 1) + hex.Substring(0, 1), 16) / 255f;
g = Convert.ToInt32(hex.Substring(1, 1) + hex.Substring(1, 1), 16) / 255f;
b = Convert.ToInt32(hex.Substring(2, 1) + hex.Substring(2, 1), 16) / 255f;
a = Convert.ToInt32(hex.Substring(3, 1) + hex.Substring(3, 1), 16) / 255f;
}
else if (hex.Length == 6)
{
// #RRGGBB format
r = Convert.ToInt32(hex.Substring(0, 2), 16) / 255f;
g = Convert.ToInt32(hex.Substring(2, 2), 16) / 255f;
b = Convert.ToInt32(hex.Substring(4, 2), 16) / 255f;
}
else if (hex.Length == 8)
{
// #RRGGBBAA format
r = Convert.ToInt32(hex.Substring(0, 2), 16) / 255f;
g = Convert.ToInt32(hex.Substring(2, 2), 16) / 255f;
b = Convert.ToInt32(hex.Substring(4, 2), 16) / 255f;
a = Convert.ToInt32(hex.Substring(6, 2), 16) / 255f;
}
else
{
throw new ArgumentException($"Invalid HEX color format: #{hex}");
}
return new Color(r, g, b, a);
}
/// <summary>
/// Sets a field or property value on a component.
/// </summary>
/// <param name="paramsJson">JSON parameters containing gameObjectPath, componentType, fieldName, value</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response with result</returns>
public static string SetField(string paramsJson, object id)
{
try
{
var parameters = JsonUtility.FromJson<ComponentSetFieldParams>(paramsJson);
if (string.IsNullOrEmpty(parameters.gameObjectPath))
{
return JsonRpcResponseHelper.InvalidParams("gameObjectPath is required", id);
}
if (string.IsNullOrEmpty(parameters.componentType))
{
return JsonRpcResponseHelper.InvalidParams("componentType is required", id);
}
if (string.IsNullOrEmpty(parameters.fieldName))
{
return JsonRpcResponseHelper.InvalidParams("fieldName is required", id);
}
GameObject go = GameObject.Find(parameters.gameObjectPath);
if (go == null)
{
return JsonRpcResponseHelper.ErrorMessage($"GameObject '{parameters.gameObjectPath}' not found", id);
}
Component comp = go.GetComponent(parameters.componentType);
if (comp == null)
{
return JsonRpcResponseHelper.ErrorMessage($"Component '{parameters.componentType}' not found on GameObject", id);
}
// Try using SerializedObject first (supports both public and private fields)
SerializedObject serializedObject = new SerializedObject(comp);
SerializedProperty property = serializedObject.FindProperty(parameters.fieldName);
if (property != null && property.propertyType != SerializedPropertyType.ObjectReference)
{
// Set value via SerializedProperty
bool success = SetSerializedPropertyValue(property, parameters.value);
if (success)
{
serializedObject.ApplyModifiedProperties();
var result = new ComponentSetFieldResult
{
status = "success",
message = $"Field '{parameters.fieldName}' set to {parameters.value}",
newValue = GetSerializedPropertyValue(property)
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
}
// Fallback to reflection for public properties (not fields)
Type type = comp.GetType();
PropertyInfo propInfo = type.GetProperty(parameters.fieldName, BindingFlags.Public | BindingFlags.Instance);
if (propInfo != null && propInfo.CanWrite)
{
object convertedValue = ConvertValue(parameters.value, propInfo.PropertyType);
propInfo.SetValue(comp, convertedValue);
var result = new ComponentSetFieldResult
{
status = "success",
message = $"Property '{parameters.fieldName}' set to {parameters.value}",
newValue = SerializeValue(convertedValue)
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
return JsonRpcResponseHelper.ErrorMessage($"Field or property '{parameters.fieldName}' not found or not writable", id);
}
catch (Exception e)
{
Debug.LogError($"[MCP] SetField error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to set field: {e.Message}", id);
}
}
/// <summary>
/// Invokes a method on a component with optional arguments.
/// </summary>
/// <param name="paramsJson">JSON parameters containing gameObjectPath, componentType, methodName, arguments</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response with method result</returns>
public static string InvokeMethod(string paramsJson, object id)
{
try
{
var parameters = JsonUtility.FromJson<ComponentInvokeMethodParams>(paramsJson);
if (string.IsNullOrEmpty(parameters.gameObjectPath))
{
return JsonRpcResponseHelper.InvalidParams("gameObjectPath is required", id);
}
if (string.IsNullOrEmpty(parameters.componentType))
{
return JsonRpcResponseHelper.InvalidParams("componentType is required", id);
}
if (string.IsNullOrEmpty(parameters.methodName))
{
return JsonRpcResponseHelper.InvalidParams("methodName is required", id);
}
GameObject go = GameObject.Find(parameters.gameObjectPath);
if (go == null)
{
return JsonRpcResponseHelper.ErrorMessage($"GameObject '{parameters.gameObjectPath}' not found", id);
}
Component comp = go.GetComponent(parameters.componentType);
if (comp == null)
{
return JsonRpcResponseHelper.ErrorMessage($"Component '{parameters.componentType}' not found on GameObject", id);
}
Type type = comp.GetType();
MethodInfo method = type.GetMethod(parameters.methodName, BindingFlags.Public | BindingFlags.Instance);
if (method == null)
{
return JsonRpcResponseHelper.ErrorMessage($"Method '{parameters.methodName}' not found", id);
}
// Convert arguments
ParameterInfo[] methodParams = method.GetParameters();
object[] convertedArgs = null;
if (parameters.arguments != null && parameters.arguments.Length > 0)
{
if (methodParams.Length != parameters.arguments.Length)
{
return JsonRpcResponseHelper.ErrorMessage(
$"Method '{parameters.methodName}' expects {methodParams.Length} arguments, but {parameters.arguments.Length} were provided",
id
);
}
convertedArgs = new object[parameters.arguments.Length];
for (int i = 0; i < parameters.arguments.Length; i++)
{
convertedArgs[i] = ConvertValue(parameters.arguments[i], methodParams[i].ParameterType);
}
}
else
{
convertedArgs = new object[0];
}
// Invoke method
object returnValue = method.Invoke(comp, convertedArgs);
var result = new ComponentInvokeMethodResult
{
status = "success",
returnValue = SerializeValue(returnValue)
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] InvokeMethod error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to invoke method: {e.Message}", id);
}
}
/// <summary>
/// Adds a component to a GameObject.
/// </summary>
/// <param name="paramsJson">JSON parameters containing gameObjectPath and componentType</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response with result</returns>
public static string AddComponent(string paramsJson, object id)
{
try
{
var parameters = JsonUtility.FromJson<ComponentAddParams>(paramsJson);
if (string.IsNullOrEmpty(parameters.gameObjectPath))
{
return JsonRpcResponseHelper.InvalidParams("gameObjectPath is required", id);
}
if (string.IsNullOrEmpty(parameters.componentType))
{
return JsonRpcResponseHelper.InvalidParams("componentType is required", id);
}
// Check if Unity is compiling
if (EditorApplication.isCompiling)
{
return JsonRpcResponseHelper.ErrorMessage(
$"Unity is currently compiling scripts. Component type '{parameters.componentType}' may not be available yet. " +
"Please use 'unity.compile.status' to check compilation status, then retry after compilation completes.",
id
);
}
GameObject go = GameObject.Find(parameters.gameObjectPath);
if (go == null)
{
return JsonRpcResponseHelper.ErrorMessage($"GameObject '{parameters.gameObjectPath}' not found", id);
}
// Try to find the component type
Type componentType = FindComponentType(parameters.componentType);
if (componentType == null)
{
string errorMessage = $"Component type '{parameters.componentType}' not found";
// Check if Unity is updating assets (might be finishing compilation)
if (EditorApplication.isUpdating)
{
errorMessage += ". Unity is currently updating assets. Please retry after update completes.";
}
else
{
errorMessage += ". Make sure the script is compiled and the type name is correct.";
}
return JsonRpcResponseHelper.ErrorMessage(errorMessage, id);
}
// Check if component already exists
if (go.GetComponent(componentType) != null)
{
return JsonRpcResponseHelper.ErrorMessage($"Component '{parameters.componentType}' already exists on GameObject", id);
}
// Add component
Component component = go.AddComponent(componentType);
var result = new ComponentAddResult
{
status = "success",
componentType = component.GetType().FullName,
gameObjectPath = parameters.gameObjectPath
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] AddComponent error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to add component: {e.Message}", id);
}
}
/// <summary>
/// Removes a component from a GameObject.
/// </summary>
/// <param name="paramsJson">JSON parameters containing gameObjectPath and componentType</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response with result</returns>
public static string RemoveComponent(string paramsJson, object id)
{
try
{
var parameters = JsonUtility.FromJson<ComponentRemoveParams>(paramsJson);
if (string.IsNullOrEmpty(parameters.gameObjectPath))
{
return JsonRpcResponseHelper.InvalidParams("gameObjectPath is required", id);
}
if (string.IsNullOrEmpty(parameters.componentType))
{
return JsonRpcResponseHelper.InvalidParams("componentType is required", id);
}
GameObject go = GameObject.Find(parameters.gameObjectPath);
if (go == null)
{
return JsonRpcResponseHelper.ErrorMessage($"GameObject '{parameters.gameObjectPath}' not found", id);
}
// Try to find the component type
Type componentType = FindComponentType(parameters.componentType);
if (componentType == null)
{
return JsonRpcResponseHelper.ErrorMessage($"Component type '{parameters.componentType}' not found", id);
}
// Get the component
Component component = go.GetComponent(componentType);
if (component == null)
{
return JsonRpcResponseHelper.ErrorMessage($"Component '{parameters.componentType}' not found on GameObject", id);
}
// Cannot remove Transform component
if (component is Transform)
{
return JsonRpcResponseHelper.ErrorMessage("Cannot remove Transform component", id);
}
// Remove component
UnityEngine.Object.DestroyImmediate(component);
var result = new ComponentRemoveResult
{
status = "success",
componentType = componentType.FullName,
gameObjectPath = parameters.gameObjectPath
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] RemoveComponent error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to remove component: {e.Message}", id);
}
}
/// <summary>
/// Sets an object reference (GameObject, Component, or Asset) on a component field/property.
/// </summary>
/// <param name="paramsJson">JSON parameters containing gameObjectPath, componentType, fieldName, and reference info</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response with result</returns>
public static string SetReference(string paramsJson, object id)
{
try
{
var parameters = JsonUtility.FromJson<ComponentSetReferenceParams>(paramsJson);
if (string.IsNullOrEmpty(parameters.gameObjectPath))
{
return JsonRpcResponseHelper.InvalidParams("gameObjectPath is required", id);
}
if (string.IsNullOrEmpty(parameters.componentType))
{
return JsonRpcResponseHelper.InvalidParams("componentType is required", id);
}
if (string.IsNullOrEmpty(parameters.fieldName))
{
return JsonRpcResponseHelper.InvalidParams("fieldName is required", id);
}
if (string.IsNullOrEmpty(parameters.referenceType))
{
return JsonRpcResponseHelper.InvalidParams("referenceType is required (asset, component, or gameObject)", id);
}
GameObject go = GameObject.Find(parameters.gameObjectPath);
if (go == null)
{
return JsonRpcResponseHelper.ErrorMessage($"GameObject '{parameters.gameObjectPath}' not found", id);
}
Component comp = go.GetComponent(parameters.componentType);
if (comp == null)
{
return JsonRpcResponseHelper.ErrorMessage($"Component '{parameters.componentType}' not found on GameObject", id);
}
// Resolve the reference based on type
UnityEngine.Object referenceObject = null;
if (parameters.referenceType == "asset")
{
if (string.IsNullOrEmpty(parameters.referencePath))
{
return JsonRpcResponseHelper.InvalidParams("referencePath is required for asset references", id);
}
referenceObject = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(parameters.referencePath);
if (referenceObject == null)
{
return JsonRpcResponseHelper.ErrorMessage($"Asset not found at path: {parameters.referencePath}", id);
}
}
else if (parameters.referenceType == "component")
{
if (string.IsNullOrEmpty(parameters.referenceGameObjectPath))
{
return JsonRpcResponseHelper.InvalidParams("referenceGameObjectPath is required for component references", id);
}
GameObject refGo = GameObject.Find(parameters.referenceGameObjectPath);
if (refGo == null)
{
return JsonRpcResponseHelper.ErrorMessage($"Reference GameObject '{parameters.referenceGameObjectPath}' not found", id);
}
if (!string.IsNullOrEmpty(parameters.referenceComponentType))
{
Type refCompType = FindComponentType(parameters.referenceComponentType);
if (refCompType == null)
{
return JsonRpcResponseHelper.ErrorMessage($"Reference component type '{parameters.referenceComponentType}' not found", id);
}
referenceObject = refGo.GetComponent(refCompType);
if (referenceObject == null)
{
return JsonRpcResponseHelper.ErrorMessage($"Component '{parameters.referenceComponentType}' not found on reference GameObject", id);
}
}
else
{
// Default to Transform if no component type specified
referenceObject = refGo.transform;
}
}
else if (parameters.referenceType == "gameObject")
{
if (string.IsNullOrEmpty(parameters.referenceGameObjectPath))
{
return JsonRpcResponseHelper.InvalidParams("referenceGameObjectPath is required for gameObject references", id);
}
GameObject refGo = GameObject.Find(parameters.referenceGameObjectPath);
if (refGo == null)
{
return JsonRpcResponseHelper.ErrorMessage($"Reference GameObject '{parameters.referenceGameObjectPath}' not found", id);
}
referenceObject = refGo;
}
else
{
return JsonRpcResponseHelper.InvalidParams($"Invalid referenceType '{parameters.referenceType}'. Must be 'asset', 'component', or 'gameObject'", id);
}
// Set the reference using SerializedObject (supports both public and private fields)
SerializedObject serializedObject = new SerializedObject(comp);
SerializedProperty property = serializedObject.FindProperty(parameters.fieldName);
if (property == null)
{
return JsonRpcResponseHelper.ErrorMessage($"Field '{parameters.fieldName}' not found", id);
}
if (property.propertyType != SerializedPropertyType.ObjectReference)
{
return JsonRpcResponseHelper.ErrorMessage($"Field '{parameters.fieldName}' is not an object reference field (type: {property.propertyType})", id);
}
// Type check
Type fieldType = GetFieldType(comp.GetType(), parameters.fieldName);
if (fieldType != null && !fieldType.IsAssignableFrom(referenceObject.GetType()))
{
return JsonRpcResponseHelper.ErrorMessage($"Type mismatch: Field expects {fieldType.Name} but got {referenceObject.GetType().Name}", id);
}
property.objectReferenceValue = referenceObject;
serializedObject.ApplyModifiedProperties();
var result = new ComponentSetReferenceResult
{
status = "success",
message = $"Reference '{parameters.fieldName}' set to {referenceObject.name}",
referenceName = referenceObject.name,
referenceType = referenceObject.GetType().Name
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] SetReference error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to set reference: {e.Message}", id);
}
}
/// <summary>
/// Sets an element in an array or list field.
/// </summary>
/// <param name="paramsJson">JSON parameters containing gameObjectPath, componentType, fieldName, index, and value</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response</returns>
public static string SetArrayElement(string paramsJson, object id)
{
try
{
var parameters = JsonUtility.FromJson<ArrayElementParams>(paramsJson);
if (string.IsNullOrEmpty(parameters.gameObjectPath) || string.IsNullOrEmpty(parameters.componentType) || string.IsNullOrEmpty(parameters.fieldName))
{
return JsonRpcResponseHelper.InvalidParams("gameObjectPath, componentType, and fieldName are required", id);
}
GameObject go = GameObject.Find(parameters.gameObjectPath);
if (go == null)
{
return JsonRpcResponseHelper.ErrorMessage($"GameObject '{parameters.gameObjectPath}' not found", id);
}
Component comp = go.GetComponent(parameters.componentType);
if (comp == null)
{
return JsonRpcResponseHelper.ErrorMessage($"Component '{parameters.componentType}' not found on GameObject", id);
}
SerializedObject serializedObject = new SerializedObject(comp);
SerializedProperty property = serializedObject.FindProperty(parameters.fieldName);
if (property == null || !property.isArray)
{
return JsonRpcResponseHelper.ErrorMessage($"Field '{parameters.fieldName}' is not an array or list", id);
}
if (parameters.index < 0 || parameters.index >= property.arraySize)
{
return JsonRpcResponseHelper.ErrorMessage($"Index {parameters.index} is out of range (size: {property.arraySize})", id);
}
SerializedProperty element = property.GetArrayElementAtIndex(parameters.index);
SetSerializedPropertyValue(element, parameters.value);
serializedObject.ApplyModifiedProperties();
var result = new ArrayOperationResult
{
status = "success",
message = $"Set element at index {parameters.index} in '{parameters.fieldName}'",
newSize = property.arraySize
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] SetArrayElement error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to set array element: {e.Message}", id);
}
}
/// <summary>
/// Adds an element to an array or list field.
/// </summary>
/// <param name="paramsJson">JSON parameters containing gameObjectPath, componentType, fieldName, and value</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response</returns>
public static string AddArrayElement(string paramsJson, object id)
{
try
{
var parameters = JsonUtility.FromJson<ArrayAddParams>(paramsJson);
if (string.IsNullOrEmpty(parameters.gameObjectPath) || string.IsNullOrEmpty(parameters.componentType) || string.IsNullOrEmpty(parameters.fieldName))
{
return JsonRpcResponseHelper.InvalidParams("gameObjectPath, componentType, and fieldName are required", id);
}
GameObject go = GameObject.Find(parameters.gameObjectPath);
if (go == null)
{
return JsonRpcResponseHelper.ErrorMessage($"GameObject '{parameters.gameObjectPath}' not found", id);
}
Component comp = go.GetComponent(parameters.componentType);
if (comp == null)
{
return JsonRpcResponseHelper.ErrorMessage($"Component '{parameters.componentType}' not found on GameObject", id);
}
SerializedObject serializedObject = new SerializedObject(comp);
SerializedProperty property = serializedObject.FindProperty(parameters.fieldName);
if (property == null || !property.isArray)
{
return JsonRpcResponseHelper.ErrorMessage($"Field '{parameters.fieldName}' is not an array or list", id);
}
property.arraySize++;
SerializedProperty newElement = property.GetArrayElementAtIndex(property.arraySize - 1);
if (!string.IsNullOrEmpty(parameters.value))
{
SetSerializedPropertyValue(newElement, parameters.value);
}
serializedObject.ApplyModifiedProperties();
var result = new ArrayOperationResult
{
status = "success",
message = $"Added element to '{parameters.fieldName}'",
newSize = property.arraySize
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] AddArrayElement error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to add array element: {e.Message}", id);
}
}
/// <summary>
/// Removes an element from an array or list field.
/// </summary>
/// <param name="paramsJson">JSON parameters containing gameObjectPath, componentType, fieldName, and index</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response</returns>
public static string RemoveArrayElement(string paramsJson, object id)
{
try
{
var parameters = JsonUtility.FromJson<ArrayRemoveParams>(paramsJson);
if (string.IsNullOrEmpty(parameters.gameObjectPath) || string.IsNullOrEmpty(parameters.componentType) || string.IsNullOrEmpty(parameters.fieldName))
{
return JsonRpcResponseHelper.InvalidParams("gameObjectPath, componentType, and fieldName are required", id);
}
GameObject go = GameObject.Find(parameters.gameObjectPath);
if (go == null)
{
return JsonRpcResponseHelper.ErrorMessage($"GameObject '{parameters.gameObjectPath}' not found", id);
}
Component comp = go.GetComponent(parameters.componentType);
if (comp == null)
{
return JsonRpcResponseHelper.ErrorMessage($"Component '{parameters.componentType}' not found on GameObject", id);
}
SerializedObject serializedObject = new SerializedObject(comp);
SerializedProperty property = serializedObject.FindProperty(parameters.fieldName);
if (property == null || !property.isArray)
{
return JsonRpcResponseHelper.ErrorMessage($"Field '{parameters.fieldName}' is not an array or list", id);
}
if (parameters.index < 0 || parameters.index >= property.arraySize)
{
return JsonRpcResponseHelper.ErrorMessage($"Index {parameters.index} is out of range (size: {property.arraySize})", id);
}
property.DeleteArrayElementAtIndex(parameters.index);
serializedObject.ApplyModifiedProperties();
var result = new ArrayOperationResult
{
status = "success",
message = $"Removed element at index {parameters.index} from '{parameters.fieldName}'",
newSize = property.arraySize
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] RemoveArrayElement error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to remove array element: {e.Message}", id);
}
}
/// <summary>
/// Gets the Type of a field (public or private) using reflection.
/// </summary>
private static Type GetFieldType(Type componentType, string fieldName)
{
FieldInfo field = componentType.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (field != null)
{
return field.FieldType;
}
PropertyInfo property = componentType.GetProperty(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (property != null)
{
return property.PropertyType;
}
return null;
}
/// <summary>
/// Finds a component type by name, trying various assembly-qualified variations.
/// </summary>
public static Type FindComponentType(string typeName)
{
// Try direct Type.GetType first
Type type = Type.GetType(typeName);
if (type != null)
return type;
// Try with Unity assemblies
string[] assemblyNames = new string[]
{
"UnityEngine",
"UnityEngine.CoreModule",
"Assembly-CSharp",
"Assembly-CSharp-firstpass"
};
foreach (string assemblyName in assemblyNames)
{
type = Type.GetType($"{typeName}, {assemblyName}");
if (type != null)
return type;
}
// Try searching all loaded assemblies
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
type = assembly.GetType(typeName);
if (type != null)
return type;
}
return null;
}
/// <summary>
/// Serializes a value to a JSON-compatible format.
/// </summary>
private static object SerializeValue(object value)
{
if (value == null)
return null;
Type type = value.GetType();
// Handle Vector3
if (type == typeof(Vector3))
{
Vector3 v = (Vector3)value;
return new Vector3Data { x = v.x, y = v.y, z = v.z };
}
// Handle Vector2
if (type == typeof(Vector2))
{
Vector2 v = (Vector2)value;
return new { x = v.x, y = v.y };
}
// Handle Color
if (type == typeof(Color))
{
Color c = (Color)value;
return new { r = c.r, g = c.g, b = c.b, a = c.a };
}
// Handle Quaternion
if (type == typeof(Quaternion))
{
Quaternion q = (Quaternion)value;
return new QuaternionData { x = q.x, y = q.y, z = q.z, w = q.w };
}
// Handle primitives and strings
if (type.IsPrimitive || type == typeof(string))
{
return value;
}
// For complex types, return type name
return $"<{type.Name}>";
}
/// <summary>
/// Gets the value from a SerializedProperty.
/// </summary>
private static object GetSerializedPropertyValue(SerializedProperty property)
{
switch (property.propertyType)
{
case SerializedPropertyType.Integer:
return property.intValue;
case SerializedPropertyType.Boolean:
return property.boolValue;
case SerializedPropertyType.Float:
return property.floatValue;
case SerializedPropertyType.String:
return property.stringValue;
case SerializedPropertyType.Color:
return new { r = property.colorValue.r, g = property.colorValue.g, b = property.colorValue.b, a = property.colorValue.a };
case SerializedPropertyType.ObjectReference:
return property.objectReferenceValue != null ? $"<{property.objectReferenceValue.GetType().Name}: {property.objectReferenceValue.name}>" : "<null>";
case SerializedPropertyType.Enum:
return property.enumNames[property.enumValueIndex];
case SerializedPropertyType.Vector2:
return new { x = property.vector2Value.x, y = property.vector2Value.y };
case SerializedPropertyType.Vector3:
return new { x = property.vector3Value.x, y = property.vector3Value.y, z = property.vector3Value.z };
case SerializedPropertyType.Vector4:
return new { x = property.vector4Value.x, y = property.vector4Value.y, z = property.vector4Value.z, w = property.vector4Value.w };
case SerializedPropertyType.Quaternion:
return new { x = property.quaternionValue.x, y = property.quaternionValue.y, z = property.quaternionValue.z, w = property.quaternionValue.w };
case SerializedPropertyType.Rect:
return new { x = property.rectValue.x, y = property.rectValue.y, width = property.rectValue.width, height = property.rectValue.height };
case SerializedPropertyType.Bounds:
return $"<Bounds>";
case SerializedPropertyType.AnimationCurve:
return "<AnimationCurve>";
case SerializedPropertyType.LayerMask:
return property.intValue;
default:
return $"<{property.propertyType}>";
}
}
/// <summary>
/// Sets the value of a SerializedProperty.
/// </summary>
private static bool SetSerializedPropertyValue(SerializedProperty property, object value)
{
try
{
switch (property.propertyType)
{
case SerializedPropertyType.Integer:
case SerializedPropertyType.LayerMask:
property.intValue = Convert.ToInt32(value);
return true;
case SerializedPropertyType.Boolean:
property.boolValue = Convert.ToBoolean(value);
return true;
case SerializedPropertyType.Float:
property.floatValue = Convert.ToSingle(value);
return true;
case SerializedPropertyType.String:
property.stringValue = value.ToString();
return true;
case SerializedPropertyType.Color:
string colorJson = JsonUtility.ToJson(value);
var colorData = JsonUtility.FromJson<ColorData>(colorJson);
property.colorValue = new Color(colorData.r, colorData.g, colorData.b, colorData.a);
return true;
case SerializedPropertyType.Enum:
if (value is string)
{
int enumIndex = System.Array.IndexOf(property.enumNames, value.ToString());
if (enumIndex >= 0)
{
property.enumValueIndex = enumIndex;
return true;
}
}
else
{
property.enumValueIndex = Convert.ToInt32(value);
return true;
}
return false;
case SerializedPropertyType.Vector2:
string v2Json = JsonUtility.ToJson(value);
var v2Data = JsonUtility.FromJson<Vector2Data>(v2Json);
property.vector2Value = new Vector2(v2Data.x, v2Data.y);
return true;
case SerializedPropertyType.Vector3:
string v3Json = JsonUtility.ToJson(value);
var v3Data = JsonUtility.FromJson<Vector3Data>(v3Json);
property.vector3Value = new Vector3(v3Data.x, v3Data.y, v3Data.z);
return true;
case SerializedPropertyType.Vector4:
string v4Json = JsonUtility.ToJson(value);
var v4Data = JsonUtility.FromJson<Vector4Data>(v4Json);
property.vector4Value = new Vector4(v4Data.x, v4Data.y, v4Data.z, v4Data.w);
return true;
case SerializedPropertyType.Quaternion:
string qJson = JsonUtility.ToJson(value);
var qData = JsonUtility.FromJson<QuaternionData>(qJson);
property.quaternionValue = new Quaternion(qData.x, qData.y, qData.z, qData.w);
return true;
default:
return false;
}
}
catch (Exception e)
{
Debug.LogError($"[MCP] Error setting SerializedProperty value: {e.Message}");
return false;
}
}
/// <summary>
/// Converts a value to the target type.
/// </summary>
private static object ConvertValue(object value, Type targetType)
{
if (value == null)
return null;
// Handle Vector3
if (targetType == typeof(Vector3))
{
string json = JsonUtility.ToJson(value);
Vector3Data data = JsonUtility.FromJson<Vector3Data>(json);
return new Vector3(data.x, data.y, data.z);
}
// Handle Color
if (targetType == typeof(Color))
{
string json = JsonUtility.ToJson(value);
var data = JsonUtility.FromJson<ColorData>(json);
return new Color(data.r, data.g, data.b, data.a);
}
// Handle Quaternion
if (targetType == typeof(Quaternion))
{
string json = JsonUtility.ToJson(value);
QuaternionData data = JsonUtility.FromJson<QuaternionData>(json);
return new Quaternion(data.x, data.y, data.z, data.w);
}
// Handle enum
if (targetType.IsEnum)
{
return Enum.Parse(targetType, value.ToString());
}
// Handle primitives and strings
return Convert.ChangeType(value, targetType);
}
#region Data Structures
/// <summary>
/// Parameters for listing components.
/// </summary>
[Serializable]
public class ComponentListParams
{
public string gameObjectPath;
}
/// <summary>
/// Parameters for inspecting a component.
/// </summary>
[Serializable]
public class ComponentInspectParams
{
public string gameObjectPath;
public string componentType;
}
/// <summary>
/// Parameters for setting a field.
/// </summary>
[Serializable]
public class ComponentSetFieldParams
{
public string gameObjectPath;
public string componentType;
public string fieldName;
public object value;
}
/// <summary>
/// Parameters for invoking a method.
/// </summary>
[Serializable]
public class ComponentInvokeMethodParams
{
public string gameObjectPath;
public string componentType;
public string methodName;
public object[] arguments;
}
/// <summary>
/// Result of listing components.
/// </summary>
[Serializable]
public class ComponentListResult
{
public string gameObject;
public string[] components;
}
/// <summary>
/// Result of inspecting a component.
/// </summary>
[Serializable]
public class ComponentInspectResult
{
public string componentType;
public FieldInfoData[] fields;
public PropertyInfoData[] properties;
public MethodInfoData[] methods;
}
/// <summary>
/// Result of setting a field.
/// </summary>
[Serializable]
public class ComponentSetFieldResult
{
public string status;
public string message;
public object newValue;
}
/// <summary>
/// Result of invoking a method.
/// </summary>
[Serializable]
public class ComponentInvokeMethodResult
{
public string status;
public object returnValue;
}
/// <summary>
/// Serializable field information.
/// </summary>
[Serializable]
public class FieldInfoData
{
public string name;
public string type;
public object value;
public bool isPublic;
}
/// <summary>
/// Serializable property information.
/// </summary>
[Serializable]
public class PropertyInfoData
{
public string name;
public string type;
public object value;
public bool canRead;
public bool canWrite;
}
/// <summary>
/// Serializable method information.
/// </summary>
[Serializable]
public class MethodInfoData
{
public string name;
public string returnType;
public string[] parameters;
}
/// <summary>
/// Serializable Color data.
/// </summary>
[Serializable]
public class ColorData
{
public float r;
public float g;
public float b;
public float a;
}
/// <summary>
/// Parameters for adding a component.
/// </summary>
[Serializable]
public class ComponentAddParams
{
public string gameObjectPath;
public string componentType;
}
/// <summary>
/// Result of adding a component.
/// </summary>
[Serializable]
public class ComponentAddResult
{
public string status;
public string componentType;
public string gameObjectPath;
}
/// <summary>
/// Parameters for removing a component.
/// </summary>
[Serializable]
public class ComponentRemoveParams
{
public string gameObjectPath;
public string componentType;
}
/// <summary>
/// Result of removing a component.
/// </summary>
[Serializable]
public class ComponentRemoveResult
{
public string status;
public string componentType;
public string gameObjectPath;
}
/// <summary>
/// Parameters for setting an object reference.
/// </summary>
[Serializable]
public class ComponentSetReferenceParams
{
public string gameObjectPath;
public string componentType;
public string fieldName;
public string referenceType; // "asset", "component", or "gameObject"
public string referencePath; // For asset references
public string referenceGameObjectPath; // For component/gameObject references
public string referenceComponentType; // For component references (optional)
}
/// <summary>
/// Result of setting an object reference.
/// </summary>
[Serializable]
public class ComponentSetReferenceResult
{
public string status;
public string message;
public string referenceName;
public string referenceType;
}
/// <summary>
/// Parameters for array/list element operations.
/// </summary>
[Serializable]
public class ArrayElementParams
{
public string gameObjectPath;
public string componentType;
public string fieldName;
public int index;
public string value; // JSON string for complex types
}
/// <summary>
/// Parameters for array/list add operation.
/// </summary>
[Serializable]
public class ArrayAddParams
{
public string gameObjectPath;
public string componentType;
public string fieldName;
public string value; // JSON string for complex types
}
/// <summary>
/// Parameters for array/list remove operation.
/// </summary>
[Serializable]
public class ArrayRemoveParams
{
public string gameObjectPath;
public string componentType;
public string fieldName;
public int index;
}
/// <summary>
/// Result of array/list operation.
/// </summary>
[Serializable]
public class ArrayOperationResult
{
public string status;
public string message;
public int newSize;
}
/// <summary>
/// Parameters for setting component field value.
/// </summary>
[Serializable]
public class ComponentSetParams
{
public string gameObjectPath;
public string componentType;
public string fieldName;
public object value;
}
/// <summary>
/// Parameters for setting a property value using reflection.
/// </summary>
[Serializable]
public class ComponentSetPropertyParams
{
public string gameObjectPath;
public string componentType;
public string propertyName;
public string value;
}
/// <summary>
/// Result of setting a property value.
/// </summary>
[Serializable]
public class ComponentSetPropertyResult
{
public string status;
public string message;
public string propertyName;
public object newValue;
}
#endregion
}
}