diff --git a/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs
new file mode 100644
index 00000000..8a65ada3
--- /dev/null
+++ b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs
@@ -0,0 +1,378 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using UnityEditor;
+using UnityEngine;
+using UnityMcpBridge.Runtime.Serialization; // For Converters
+
+namespace UnityMcpBridge.Editor.Helpers
+{
+ ///
+ /// Handles serialization of GameObjects and Components for MCP responses.
+ /// Includes reflection helpers and caching for performance.
+ ///
+ public static class GameObjectSerializer
+ {
+ // --- Data Serialization ---
+
+ ///
+ /// Creates a serializable representation of a GameObject.
+ ///
+ public static object GetGameObjectData(GameObject go)
+ {
+ if (go == null)
+ return null;
+ return new
+ {
+ name = go.name,
+ instanceID = go.GetInstanceID(),
+ tag = go.tag,
+ layer = go.layer,
+ activeSelf = go.activeSelf,
+ activeInHierarchy = go.activeInHierarchy,
+ isStatic = go.isStatic,
+ scenePath = go.scene.path, // Identify which scene it belongs to
+ transform = new // Serialize transform components carefully to avoid JSON issues
+ {
+ // Serialize Vector3 components individually to prevent self-referencing loops.
+ // The default serializer can struggle with properties like Vector3.normalized.
+ position = new
+ {
+ x = go.transform.position.x,
+ y = go.transform.position.y,
+ z = go.transform.position.z,
+ },
+ localPosition = new
+ {
+ x = go.transform.localPosition.x,
+ y = go.transform.localPosition.y,
+ z = go.transform.localPosition.z,
+ },
+ rotation = new
+ {
+ x = go.transform.rotation.eulerAngles.x,
+ y = go.transform.rotation.eulerAngles.y,
+ z = go.transform.rotation.eulerAngles.z,
+ },
+ localRotation = new
+ {
+ x = go.transform.localRotation.eulerAngles.x,
+ y = go.transform.localRotation.eulerAngles.y,
+ z = go.transform.localRotation.eulerAngles.z,
+ },
+ scale = new
+ {
+ x = go.transform.localScale.x,
+ y = go.transform.localScale.y,
+ z = go.transform.localScale.z,
+ },
+ forward = new
+ {
+ x = go.transform.forward.x,
+ y = go.transform.forward.y,
+ z = go.transform.forward.z,
+ },
+ up = new
+ {
+ x = go.transform.up.x,
+ y = go.transform.up.y,
+ z = go.transform.up.z,
+ },
+ right = new
+ {
+ x = go.transform.right.x,
+ y = go.transform.right.y,
+ z = go.transform.right.z,
+ },
+ },
+ parentInstanceID = go.transform.parent?.gameObject.GetInstanceID() ?? 0, // 0 if no parent
+ // Optionally include components, but can be large
+ // components = go.GetComponents().Select(c => GetComponentData(c)).ToList()
+ // Or just component names:
+ componentNames = go.GetComponents()
+ .Select(c => c.GetType().FullName)
+ .ToList(),
+ };
+ }
+
+ // --- Metadata Caching for Reflection ---
+ private class CachedMetadata
+ {
+ public readonly List SerializableProperties;
+ public readonly List SerializableFields;
+
+ public CachedMetadata(List properties, List fields)
+ {
+ SerializableProperties = properties;
+ SerializableFields = fields;
+ }
+ }
+ // Key becomes Tuple
+ private static readonly Dictionary, CachedMetadata> _metadataCache = new Dictionary, CachedMetadata>();
+ // --- End Metadata Caching ---
+
+ ///
+ /// Creates a serializable representation of a Component, attempting to serialize
+ /// public properties and fields using reflection, with caching and control over non-public fields.
+ ///
+ // Add the flag parameter here
+ public static object GetComponentData(Component c, bool includeNonPublicSerializedFields = true)
+ {
+ if (c == null) return null;
+ Type componentType = c.GetType();
+
+ var data = new Dictionary
+ {
+ { "typeName", componentType.FullName },
+ { "instanceID", c.GetInstanceID() }
+ };
+
+ // --- Get Cached or Generate Metadata (using new cache key) ---
+ Tuple cacheKey = new Tuple(componentType, includeNonPublicSerializedFields);
+ if (!_metadataCache.TryGetValue(cacheKey, out CachedMetadata cachedData))
+ {
+ var propertiesToCache = new List();
+ var fieldsToCache = new List();
+
+ // Traverse the hierarchy from the component type up to MonoBehaviour
+ Type currentType = componentType;
+ while (currentType != null && currentType != typeof(MonoBehaviour) && currentType != typeof(object))
+ {
+ // Get properties declared only at the current type level
+ BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
+ foreach (var propInfo in currentType.GetProperties(propFlags))
+ {
+ // Basic filtering (readable, not indexer, not transform which is handled elsewhere)
+ if (!propInfo.CanRead || propInfo.GetIndexParameters().Length > 0 || propInfo.Name == "transform") continue;
+ // Add if not already added (handles overrides - keep the most derived version)
+ if (!propertiesToCache.Any(p => p.Name == propInfo.Name)) {
+ propertiesToCache.Add(propInfo);
+ }
+ }
+
+ // Get fields declared only at the current type level (both public and non-public)
+ BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly;
+ var declaredFields = currentType.GetFields(fieldFlags);
+
+ // Process the declared Fields for caching
+ foreach (var fieldInfo in declaredFields)
+ {
+ if (fieldInfo.Name.EndsWith("k__BackingField")) continue; // Skip backing fields
+
+ // Add if not already added (handles hiding - keep the most derived version)
+ if (fieldsToCache.Any(f => f.Name == fieldInfo.Name)) continue;
+
+ bool shouldInclude = false;
+ if (includeNonPublicSerializedFields)
+ {
+ // If TRUE, include Public OR NonPublic with [SerializeField]
+ shouldInclude = fieldInfo.IsPublic || (fieldInfo.IsPrivate && fieldInfo.IsDefined(typeof(SerializeField), inherit: false));
+ }
+ else // includeNonPublicSerializedFields is FALSE
+ {
+ // If FALSE, include ONLY if it is explicitly Public.
+ shouldInclude = fieldInfo.IsPublic;
+ }
+
+ if (shouldInclude)
+ {
+ fieldsToCache.Add(fieldInfo);
+ }
+ }
+
+ // Move to the base type
+ currentType = currentType.BaseType;
+ }
+ // --- End Hierarchy Traversal ---
+
+ cachedData = new CachedMetadata(propertiesToCache, fieldsToCache);
+ _metadataCache[cacheKey] = cachedData; // Add to cache with combined key
+ }
+ // --- End Get Cached or Generate Metadata ---
+
+ // --- Use cached metadata ---
+ var serializablePropertiesOutput = new Dictionary();
+ // Use cached properties
+ foreach (var propInfo in cachedData.SerializableProperties)
+ {
+ // --- Skip known obsolete/problematic Component shortcut properties ---
+ string propName = propInfo.Name;
+ if (propName == "rigidbody" || propName == "rigidbody2D" || propName == "camera" ||
+ propName == "light" || propName == "animation" || propName == "constantForce" ||
+ propName == "renderer" || propName == "audio" || propName == "networkView" ||
+ propName == "collider" || propName == "collider2D" || propName == "hingeJoint" ||
+ propName == "particleSystem" ||
+ // Also skip potentially problematic Matrix properties prone to cycles/errors
+ propName == "worldToLocalMatrix" || propName == "localToWorldMatrix")
+ {
+ continue; // Skip these properties
+ }
+ // --- End Skip ---
+
+ try
+ {
+ object value = propInfo.GetValue(c);
+ Type propType = propInfo.PropertyType;
+ AddSerializableValue(serializablePropertiesOutput, propName, propType, value);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"Could not read property {propName} on {componentType.Name}: {ex.Message}");
+ }
+ }
+
+ // Use cached fields
+ foreach (var fieldInfo in cachedData.SerializableFields)
+ {
+ try
+ {
+ object value = fieldInfo.GetValue(c);
+ string fieldName = fieldInfo.Name;
+ Type fieldType = fieldInfo.FieldType;
+ AddSerializableValue(serializablePropertiesOutput, fieldName, fieldType, value);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}: {ex.Message}");
+ }
+ }
+ // --- End Use cached metadata ---
+
+ if (serializablePropertiesOutput.Count > 0)
+ {
+ data["properties"] = serializablePropertiesOutput;
+ }
+
+ return data;
+ }
+
+ // Helper function to decide how to serialize different types
+ private static void AddSerializableValue(Dictionary dict, string name, Type type, object value)
+ {
+ // Simplified: Directly use CreateTokenFromValue which uses the serializer
+ if (value == null)
+ {
+ dict[name] = null;
+ return;
+ }
+
+ try
+ {
+ // Use the helper that employs our custom serializer settings
+ JToken token = CreateTokenFromValue(value, type);
+ if (token != null) // Check if serialization succeeded in the helper
+ {
+ // Convert JToken back to a basic object structure for the dictionary
+ dict[name] = ConvertJTokenToPlainObject(token);
+ }
+ // If token is null, it means serialization failed and a warning was logged.
+ }
+ catch (Exception e)
+ {
+ // Catch potential errors during JToken conversion or addition to dictionary
+ Debug.LogWarning($"[AddSerializableValue] Error processing value for '{name}' (Type: {type.FullName}): {e.Message}. Skipping.");
+ }
+ }
+
+ // Helper to convert JToken back to basic object structure
+ private static object ConvertJTokenToPlainObject(JToken token)
+ {
+ if (token == null) return null;
+
+ switch (token.Type)
+ {
+ case JTokenType.Object:
+ var objDict = new Dictionary();
+ foreach (var prop in ((JObject)token).Properties())
+ {
+ objDict[prop.Name] = ConvertJTokenToPlainObject(prop.Value);
+ }
+ return objDict;
+
+ case JTokenType.Array:
+ var list = new List