Skip to content

Commit e4218cb

Browse files
committed
Enhancements for returning gameObject information
1 parent 0d46436 commit e4218cb

File tree

6 files changed

+321
-15
lines changed

6 files changed

+321
-15
lines changed

Editor/Resources/GetGameObjectResource.cs

Lines changed: 123 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,39 @@ public static JObject GameObjectToJObject(GameObject gameObject, bool includeDet
119119
return gameObjectJson;
120120
}
121121

122+
/// <summary>
123+
/// Namespace prefixes for components with native/C++ code that crash when accessed via reflection.
124+
/// These components will only have basic info (type, enabled) serialized, not detailed properties.
125+
/// </summary>
126+
private static readonly string[] UnsafeNamespacePrefixes = new string[]
127+
{
128+
"Pathfinding", // A* Pathfinding Project
129+
"FMOD", // FMOD audio
130+
"FMODUnity", // FMOD Unity integration
131+
};
132+
133+
/// <summary>
134+
/// Check if a component type is from a native plugin that may crash when accessed via reflection
135+
/// </summary>
136+
private static bool IsUnsafeNativeComponent(Type componentType)
137+
{
138+
if (componentType == null) return true;
139+
140+
string fullName = componentType.FullName ?? "";
141+
string namespaceName = componentType.Namespace ?? "";
142+
143+
foreach (string prefix in UnsafeNamespacePrefixes)
144+
{
145+
if (namespaceName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) ||
146+
fullName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
147+
{
148+
return true;
149+
}
150+
}
151+
152+
return false;
153+
}
154+
122155
/// <summary>
123156
/// Get information about the components attached to a GameObject
124157
/// </summary>
@@ -134,16 +167,29 @@ private static JArray GetComponentsInfo(GameObject gameObject, bool includeDetai
134167
{
135168
if (component == null) continue;
136169

170+
Type componentType = component.GetType();
171+
bool isUnsafe = IsUnsafeNativeComponent(componentType);
172+
137173
JObject componentJson = new JObject
138174
{
139-
["type"] = component.GetType().Name,
175+
["type"] = componentType.Name,
140176
["enabled"] = IsComponentEnabled(component)
141177
};
142178

143-
// Add detailed information if requested
179+
// Add detailed information if requested and component is safe to inspect
144180
if (includeDetailedInfo)
145181
{
146-
componentJson["properties"] = GetComponentProperties(component);
182+
if (isUnsafe)
183+
{
184+
componentJson["properties"] = new JObject
185+
{
186+
["_skipped"] = "Native plugin component - serialization skipped for safety"
187+
};
188+
}
189+
else
190+
{
191+
componentJson["properties"] = GetComponentProperties(component);
192+
}
147193
}
148194

149195
componentsArray.Add(componentJson);
@@ -181,6 +227,16 @@ private static bool IsComponentEnabled(Component component)
181227
return true;
182228
}
183229

230+
/// <summary>
231+
/// Maximum depth for serializing nested objects to prevent stack overflow from circular references
232+
/// </summary>
233+
private const int MaxSerializationDepth = 5;
234+
235+
/// <summary>
236+
/// Maximum items to serialize in a collection to prevent excessive output
237+
/// </summary>
238+
private const int MaxCollectionItems = 50;
239+
184240
/// <summary>
185241
/// Get all serialized fields, public fields and public properties of a component
186242
/// </summary>
@@ -192,6 +248,9 @@ private static JObject GetComponentProperties(Component component)
192248

193249
JObject propertiesJson = new JObject();
194250
Type componentType = component.GetType();
251+
252+
// Track visited objects to prevent circular reference loops
253+
HashSet<object> visited = new HashSet<object>(new ReferenceEqualityComparer());
195254

196255
// Get serialized fields (both public and private with SerializeField attribute)
197256
FieldInfo[] fields = componentType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
@@ -204,7 +263,7 @@ private static JObject GetComponentProperties(Component component)
204263
try
205264
{
206265
object value = field.GetValue(component);
207-
propertiesJson[field.Name] = SerializeValue(value);
266+
propertiesJson[field.Name] = SerializeValue(value, 0, visited);
208267
}
209268
catch (Exception)
210269
{
@@ -223,7 +282,7 @@ private static JObject GetComponentProperties(Component component)
223282
try
224283
{
225284
object value = property.GetValue(component);
226-
propertiesJson[property.Name] = SerializeValue(value);
285+
propertiesJson[property.Name] = SerializeValue(value, 0, visited);
227286
}
228287
catch (Exception)
229288
{
@@ -234,6 +293,15 @@ private static JObject GetComponentProperties(Component component)
234293

235294
return propertiesJson;
236295
}
296+
297+
/// <summary>
298+
/// Reference equality comparer for tracking visited objects (prevents circular reference infinite loops)
299+
/// </summary>
300+
private class ReferenceEqualityComparer : IEqualityComparer<object>
301+
{
302+
public new bool Equals(object x, object y) => ReferenceEquals(x, y);
303+
public int GetHashCode(object obj) => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
304+
}
237305

238306
/// <summary>
239307
/// Determine if a property should be skipped during serialization
@@ -254,14 +322,34 @@ private static bool ShouldSkipProperty(PropertyInfo property)
254322
}
255323

256324
/// <summary>
257-
/// Serialize a value to a JToken
325+
/// Serialize a value to a JToken with depth limiting and circular reference protection
258326
/// </summary>
259327
/// <param name="value">The value to serialize</param>
328+
/// <param name="depth">Current recursion depth</param>
329+
/// <param name="visited">Set of already visited reference objects to detect circular references</param>
260330
/// <returns>A JToken representing the value</returns>
261-
private static JToken SerializeValue(object value)
331+
private static JToken SerializeValue(object value, int depth = 0, HashSet<object> visited = null)
262332
{
263333
if (value == null)
264334
return JValue.CreateNull();
335+
336+
// Depth limit check to prevent stack overflow
337+
if (depth > MaxSerializationDepth)
338+
return "[max depth exceeded]";
339+
340+
Type valueType = value.GetType();
341+
342+
// For reference types (excluding strings), check for circular references
343+
if (!valueType.IsValueType && !(value is string))
344+
{
345+
if (visited == null)
346+
visited = new HashSet<object>(new ReferenceEqualityComparer());
347+
348+
if (visited.Contains(value))
349+
return "[circular reference]";
350+
351+
visited.Add(value);
352+
}
265353

266354
// Handle common Unity types
267355
if (value is Vector2 vector2)
@@ -281,8 +369,8 @@ private static JToken SerializeValue(object value)
281369

282370
if (value is Bounds bounds)
283371
return new JObject {
284-
["center"] = SerializeValue(bounds.center),
285-
["size"] = SerializeValue(bounds.size)
372+
["center"] = SerializeValue(bounds.center, depth + 1, visited),
373+
["size"] = SerializeValue(bounds.size, depth + 1, visited)
286374
};
287375

288376
if (value is Rect rect)
@@ -291,24 +379,38 @@ private static JToken SerializeValue(object value)
291379
if (value is UnityEngine.Object unityObject)
292380
return unityObject != null ? unityObject.name : null;
293381

294-
// Handle arrays and lists
382+
// Handle arrays and lists with item limit
295383
if (value is System.Collections.IList list)
296384
{
297385
JArray array = new JArray();
386+
int count = 0;
298387
foreach (var item in list)
299388
{
300-
array.Add(SerializeValue(item));
389+
if (count >= MaxCollectionItems)
390+
{
391+
array.Add($"[... and {list.Count - count} more items]");
392+
break;
393+
}
394+
array.Add(SerializeValue(item, depth + 1, visited));
395+
count++;
301396
}
302397
return array;
303398
}
304399

305-
// Handle dictionaries
400+
// Handle dictionaries with item limit
306401
if (value is System.Collections.IDictionary dict)
307402
{
308403
JObject obj = new JObject();
404+
int count = 0;
309405
foreach (System.Collections.DictionaryEntry entry in dict)
310406
{
311-
obj[entry.Key.ToString()] = SerializeValue(entry.Value);
407+
if (count >= MaxCollectionItems)
408+
{
409+
obj["_truncated"] = $"{dict.Count - count} more entries";
410+
break;
411+
}
412+
obj[entry.Key.ToString()] = SerializeValue(entry.Value, depth + 1, visited);
413+
count++;
312414
}
313415
return obj;
314416
}
@@ -317,8 +419,14 @@ private static JToken SerializeValue(object value)
317419
if (value is Enum enumValue)
318420
return enumValue.ToString();
319421

320-
// Use default serialization for primitive types
321-
return JToken.FromObject(value);
422+
// Handle primitive types directly
423+
if (valueType.IsPrimitive || value is string || value is decimal)
424+
{
425+
return JToken.FromObject(value);
426+
}
427+
428+
// For complex types we don't recognize, return type name to avoid unsafe deep serialization
429+
return $"[{valueType.Name}]";
322430
}
323431
}
324432
}

Editor/Tools/GetGameObjectTool.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using McpUnity.Resources;
2+
using McpUnity.Unity;
3+
using UnityEngine;
4+
using UnityEditor;
5+
using Newtonsoft.Json.Linq;
6+
7+
namespace McpUnity.Tools
8+
{
9+
/// <summary>
10+
/// Tool for retrieving detailed information about a specific GameObject.
11+
/// This tool provides the same functionality as the get_gameobject resource,
12+
/// but as a tool that can be invoked directly without URI template parameters.
13+
/// </summary>
14+
public class GetGameObjectTool : McpToolBase
15+
{
16+
public GetGameObjectTool()
17+
{
18+
Name = "get_gameobject";
19+
Description = "Retrieves detailed information about a specific GameObject by instance ID, name, or hierarchical path (e.g., \"Parent/Child/MyObject\"). Returns all component properties including Transform position, rotation, scale, and more.";
20+
}
21+
22+
/// <summary>
23+
/// Execute the GetGameObject tool with the provided parameters
24+
/// </summary>
25+
/// <param name="parameters">Tool parameters as a JObject. Should include 'idOrName' which can be an instance ID, name, or path</param>
26+
/// <returns>A JObject containing the GameObject data</returns>
27+
public override JObject Execute(JObject parameters)
28+
{
29+
// Validate parameters
30+
if (parameters == null || !parameters.ContainsKey("idOrName"))
31+
{
32+
return McpUnitySocketHandler.CreateErrorResponse(
33+
"Missing required parameter: idOrName",
34+
"validation_error"
35+
);
36+
}
37+
38+
string idOrName = parameters["idOrName"]?.ToObject<string>();
39+
40+
if (string.IsNullOrEmpty(idOrName))
41+
{
42+
return McpUnitySocketHandler.CreateErrorResponse(
43+
"Parameter 'idOrName' cannot be null or empty",
44+
"validation_error"
45+
);
46+
}
47+
48+
GameObject gameObject = null;
49+
50+
// Try to parse as an instance ID first
51+
if (int.TryParse(idOrName, out int instanceId))
52+
{
53+
// Unity Instance IDs are typically negative, but we'll accept any integer
54+
UnityEngine.Object unityObject = EditorUtility.InstanceIDToObject(instanceId);
55+
gameObject = unityObject as GameObject;
56+
}
57+
else
58+
{
59+
// Otherwise, treat it as a name or hierarchical path
60+
gameObject = GameObject.Find(idOrName);
61+
}
62+
63+
// Check if the GameObject was found
64+
if (gameObject == null)
65+
{
66+
return McpUnitySocketHandler.CreateErrorResponse(
67+
$"GameObject with '{idOrName}' reference not found. Make sure the GameObject exists and is loaded in the current scene(s).",
68+
"not_found_error"
69+
);
70+
}
71+
72+
// Convert the GameObject to a JObject using the resource's static method
73+
JObject gameObjectData = GetGameObjectResource.GameObjectToJObject(gameObject, true);
74+
75+
// Create the response
76+
return new JObject
77+
{
78+
["success"] = true,
79+
["message"] = $"Retrieved GameObject data for '{gameObject.name}'",
80+
["gameObject"] = gameObjectData,
81+
["instanceId"] = gameObject.GetInstanceID()
82+
};
83+
}
84+
}
85+
}
86+
87+

Editor/Tools/GetGameObjectTool.cs.meta

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Editor/UnityBridge/McpUnityServer.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,10 @@ private void RegisterTools()
267267
// Register RecompileScriptsTool
268268
RecompileScriptsTool recompileScriptsTool = new RecompileScriptsTool();
269269
_tools.Add(recompileScriptsTool.Name, recompileScriptsTool);
270+
271+
// Register GetGameObjectTool
272+
GetGameObjectTool getGameObjectTool = new GetGameObjectTool();
273+
_tools.Add(getGameObjectTool.Name, getGameObjectTool);
270274
}
271275

272276
/// <summary>

Server~/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { registerCreatePrefabTool } from './tools/createPrefabTool.js';
1717
import { registerDeleteSceneTool } from './tools/deleteSceneTool.js';
1818
import { registerLoadSceneTool } from './tools/loadSceneTool.js';
1919
import { registerRecompileScriptsTool } from './tools/recompileScriptsTool.js';
20+
import { registerGetGameObjectTool } from './tools/getGameObjectTool.js';
2021
import { registerGetMenuItemsResource } from './resources/getMenuItemResource.js';
2122
import { registerGetConsoleLogsResource } from './resources/getConsoleLogsResource.js';
2223
import { registerGetHierarchyResource } from './resources/getScenesHierarchyResource.js';
@@ -65,6 +66,7 @@ registerCreateSceneTool(server, mcpUnity, toolLogger);
6566
registerDeleteSceneTool(server, mcpUnity, toolLogger);
6667
registerLoadSceneTool(server, mcpUnity, toolLogger);
6768
registerRecompileScriptsTool(server, mcpUnity, toolLogger);
69+
registerGetGameObjectTool(server, mcpUnity, toolLogger);
6870

6971
// Register all resources into the MCP server
7072
registerGetTestsResource(server, mcpUnity, resourceLogger);

0 commit comments

Comments
 (0)