Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ All notable public changes to Nexus Unity are documented here.
- Consolidated duplicate internal Ollama-review and serialized-property write helpers.

### Fixed
- `find_objects` now safely constructs name search regexes with a 100ms match timeout and falls back gracefully to literal substring search on invalid regex patterns or match timeouts (#119).
- Restrict type resolution (`FindType`) to user project assemblies and standard public `UnityEngine` assemblies, disallow internal system/editor assemblies and namespaces, and enforce strict type allowlist constraints for component and ScriptableObject creation/inspection tools (#139).
- Resolve symlinks and directory junctions to their real filesystem targets in `ValidatePath` before checking project boundaries to prevent path traversal.
- Reject abstract ScriptableObject types before calling `CreateInstance` in `create_scriptable_object_asset` and `list_fields_for_type`.
Expand Down
163 changes: 163 additions & 0 deletions Editor/MCPServerMethods.References.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;

namespace UnityMCP.Editor
{
public static partial class MCPServerMethods
{
private static JToken FindReferences(JToken p)
{
string targetGuid = p?["target_guid"]?.ToString();
EntityId targetId = MCPServerMethods.ExtractId(p, "target_id");

if (string.IsNullOrEmpty(targetGuid) && targetId == default)
throw new System.Exception("Either target_guid or target_id is required.");

Object targetObject = ResolveTargetObject(targetGuid, targetId);
if (targetObject == null)
throw new System.Exception("Could not find the target object.");

string searchGuid = GetSearchGuid(targetGuid, targetObject);
JArray assetRefs = FindAssetReferences(searchGuid);

HashSet<EntityId> targetInstanceIds = BuildTargetInstanceIds(targetObject, searchGuid);
JArray sceneRefs = FindSceneReferences(targetInstanceIds);

var result = new JObject();
result["status"] = "Success";
result["asset_references"] = assetRefs;
result["scene_references"] = sceneRefs;
return result;
}

private static Object ResolveTargetObject(string targetGuid, EntityId targetId)
{
if (!string.IsNullOrEmpty(targetGuid))
{
string path = AssetDatabase.GUIDToAssetPath(targetGuid);
if (!string.IsNullOrEmpty(path))
return AssetDatabase.LoadAssetAtPath<Object>(path);
}
else if (targetId != default)
{
return MCPServerMethods.IdToObject(targetId);
}
return null;
}

private static string GetSearchGuid(string targetGuid, Object targetObject)
{
if (!string.IsNullOrEmpty(targetGuid)) return targetGuid;
string path = AssetDatabase.GetAssetPath(targetObject);
return !string.IsNullOrEmpty(path) ? AssetDatabase.AssetPathToGUID(path) : null;
}

private static JArray FindAssetReferences(string searchGuid)
{
var assetRefs = new JArray();
if (!string.IsNullOrEmpty(searchGuid))
{
string[] dependencies = AssetDatabase.FindAssets("ref:" + searchGuid);
foreach (var guid in dependencies)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
if (!string.IsNullOrEmpty(path))
assetRefs.Add(path);
}
}
return assetRefs;
}

private static HashSet<EntityId> BuildTargetInstanceIds(Object targetObject, string searchGuid)
{
var targetInstanceIds = new HashSet<EntityId>();
if (targetObject != null)
targetInstanceIds.Add(targetObject.GetId());

if (!string.IsNullOrEmpty(searchGuid))
{
string path = AssetDatabase.GUIDToAssetPath(searchGuid);
if (!string.IsNullOrEmpty(path))
{
var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
foreach (var a in allAssets)
{
if (a != null)
targetInstanceIds.Add(a.GetId());
}
}
}
return targetInstanceIds;
}

private static JArray FindSceneReferences(HashSet<EntityId> targetInstanceIds)
{
var sceneRefs = new JArray();
var allGameObjects = Resources.FindObjectsOfTypeAll<GameObject>()
.Where(go => go.hideFlags == HideFlags.None || go.hideFlags == HideFlags.NotEditable);

using (UnityEngine.Pool.ListPool<Component>.Get(out var components))
{
foreach (var go in allGameObjects)
{
if (go.scene == null || !go.scene.isLoaded) continue;

go.GetComponents(components);
var goMatches = InspectGameObjectReferences(components, targetInstanceIds);
if (goMatches.Count > 0)
{
var goData = new JObject
{
["name"] = go.name,
["instance_id"] = go.GetRawId(),
["references"] = goMatches
};
sceneRefs.Add(goData);
}
}
}
return sceneRefs;
}

private static JArray InspectGameObjectReferences(List<Component> components, HashSet<EntityId> targetInstanceIds)
{
var goMatches = new JArray();
foreach (var comp in components)
{
if (comp == null) continue;

using (var so = new SerializedObject(comp))
{
var prop = so.GetIterator();
bool enterChildren = true;
while (prop.Next(enterChildren))
{
enterChildren = false;
if (prop.propertyType == SerializedPropertyType.ObjectReference)
{
var propId = MCPServerMethods.GetObjectReferenceId(prop);
var objRef = prop.objectReferenceValue;

bool directMatch = targetInstanceIds.Contains(propId);
bool indirectMatch = objRef != null && targetInstanceIds.Contains(objRef.GetId());

if (directMatch || indirectMatch)
{
var matchDetail = new JObject
{
["component"] = GetTypeName(comp.GetType()),
["field"] = prop.propertyPath
};
goMatches.Add(matchDetail);
}
}
}
}
}
return goMatches;
}
}
}
2 changes: 2 additions & 0 deletions Editor/MCPServerMethods.References.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

162 changes: 33 additions & 129 deletions Editor/MCPServerMethods.Search.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,18 +82,46 @@ private static JToken FindObjects(JToken p)
.Where(go => go.hideFlags == HideFlags.None);
}

if (!string.IsNullOrEmpty(name))
{
var regex = new System.Text.RegularExpressions.Regex(name, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
results = results.Where(go => regex.IsMatch(go.name));
}
results = FilterByName(results, name);

if (!string.IsNullOrEmpty(tag))
results = results.Where(go => go.CompareTag(tag));

return new JObject { ["objects"] = new JArray(results.Take(50).Select(SerializeGameObject)) };
}

private static IEnumerable<GameObject> FilterByName(IEnumerable<GameObject> results, string name)
{
if (string.IsNullOrEmpty(name)) return results;

System.Text.RegularExpressions.Regex regex = null;
try
{
regex = new System.Text.RegularExpressions.Regex(name, System.Text.RegularExpressions.RegexOptions.IgnoreCase, System.TimeSpan.FromMilliseconds(100));
}
catch (System.ArgumentException)
{
// Invalid regex pattern (e.g. "Player (1)"), fallback to literal substring search
}

if (regex != null)
{
return results.Where(go =>
{
try
{
return regex.IsMatch(go.name);
}
catch (System.Text.RegularExpressions.RegexMatchTimeoutException)
{
return go.name.IndexOf(name, System.StringComparison.OrdinalIgnoreCase) >= 0;
}
});
}

return results.Where(go => go.name.IndexOf(name, System.StringComparison.OrdinalIgnoreCase) >= 0);
}

private static Stack<string> _pathStackCache = new Stack<string>();

private static JToken GetObjectPath(JToken p)
Expand Down Expand Up @@ -121,129 +149,5 @@ private static JToken PingObject(JToken p)
EditorGUIUtility.PingObject(obj);
return new JObject { ["status"] = "Success", ["message"] = "Pinged" };
}

private static JToken FindReferences(JToken p)
{
string targetGuid = p?["target_guid"]?.ToString();
EntityId targetId = MCPServerMethods.ExtractId(p, "target_id");

if (string.IsNullOrEmpty(targetGuid) && targetId == default)
throw new System.Exception("Either target_guid or target_id is required.");

Object targetObject = null;
if (!string.IsNullOrEmpty(targetGuid))
{
string path = AssetDatabase.GUIDToAssetPath(targetGuid);
if (!string.IsNullOrEmpty(path))
targetObject = AssetDatabase.LoadAssetAtPath<Object>(path);
}
else if (targetId != default)
{
targetObject = MCPServerMethods.IdToObject(targetId);
}

if (targetObject == null)
throw new System.Exception("Could not find the target object.");

var assetRefs = new JArray();
string searchGuid = targetGuid;

if (string.IsNullOrEmpty(searchGuid))
{
string path = AssetDatabase.GetAssetPath(targetObject);
if (!string.IsNullOrEmpty(path))
searchGuid = AssetDatabase.AssetPathToGUID(path);
}

if (!string.IsNullOrEmpty(searchGuid))
{
string[] dependencies = AssetDatabase.FindAssets("ref:" + searchGuid);
foreach (var guid in dependencies)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
if (!string.IsNullOrEmpty(path))
assetRefs.Add(path);
}
}

var sceneRefs = new JArray();
var allGameObjects = Resources.FindObjectsOfTypeAll<GameObject>()
.Where(go => go.hideFlags == HideFlags.None || go.hideFlags == HideFlags.NotEditable);

var targetInstanceIds = new HashSet<EntityId>();
if (targetObject != null)
targetInstanceIds.Add(targetObject.GetId());

if (!string.IsNullOrEmpty(searchGuid))
{
string path = AssetDatabase.GUIDToAssetPath(searchGuid);
if (!string.IsNullOrEmpty(path))
{
var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
foreach (var a in allAssets)
{
if (a != null)
targetInstanceIds.Add(a.GetId());
}
}
}

using (UnityEngine.Pool.ListPool<Component>.Get(out var components))
{
foreach (var go in allGameObjects)
{
if (go.scene == null || !go.scene.isLoaded) continue;

go.GetComponents(components);
var goMatches = new JArray();

foreach (var comp in components)
{
if (comp == null) continue;

using (var so = new SerializedObject(comp))
{
var prop = so.GetIterator();
bool enterChildren = true;
while (prop.Next(enterChildren))
{
enterChildren = false;
if (prop.propertyType == SerializedPropertyType.ObjectReference)
{
var propId = MCPServerMethods.GetObjectReferenceId(prop);
var objRef = prop.objectReferenceValue;

bool directMatch = targetInstanceIds.Contains(propId);
bool indirectMatch = objRef != null && targetInstanceIds.Contains(objRef.GetId());

if (directMatch || indirectMatch)
{
var matchDetail = new JObject();
matchDetail["component"] = GetTypeName(comp.GetType());
matchDetail["field"] = prop.propertyPath;
goMatches.Add(matchDetail);
}
}
}
}
}

if (goMatches.Count > 0)
{
var goData = new JObject();
goData["name"] = go.name;
goData["instance_id"] = go.GetRawId();
goData["references"] = goMatches;
sceneRefs.Add(goData);
}
}
}

var result = new JObject();
result["status"] = "Success";
result["asset_references"] = assetRefs;
result["scene_references"] = sceneRefs;
return result;
}
}
}
Loading
Loading