Skip to main content
Glama
ComponentReflection.cs69 kB
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 } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dsgarage/UniMCP4CC'

If you have feedback or need assistance with the MCP directory API, please join our Discord server