From 219789ee4b291ec8816f0bb09aaa1ce76f878ebe Mon Sep 17 00:00:00 2001 From: Daliys Date: Wed, 22 Jul 2026 23:48:25 +0200 Subject: [PATCH 1/4] fix(search): handle invalid regex and add match timeout in find_objects (#119) --- CHANGELOG.md | 1 + Editor/MCPServerMethods.Search.cs | 30 ++++++++++++++++++++++++-- Tests~/Editor/AgentToolingTests.cs | 34 ++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79c3df3..039e0d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. diff --git a/Editor/MCPServerMethods.Search.cs b/Editor/MCPServerMethods.Search.cs index 21a8b16..223cd41 100644 --- a/Editor/MCPServerMethods.Search.cs +++ b/Editor/MCPServerMethods.Search.cs @@ -84,8 +84,34 @@ private static JToken FindObjects(JToken p) 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)); + 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) + { + results = results.Where(go => + { + try + { + return regex.IsMatch(go.name); + } + catch (System.Text.RegularExpressions.RegexMatchTimeoutException) + { + return go.name.IndexOf(name, System.StringComparison.OrdinalIgnoreCase) >= 0; + } + }); + } + else + { + results = results.Where(go => go.name.IndexOf(name, System.StringComparison.OrdinalIgnoreCase) >= 0); + } } if (!string.IsNullOrEmpty(tag)) diff --git a/Tests~/Editor/AgentToolingTests.cs b/Tests~/Editor/AgentToolingTests.cs index 3f74d46..f3e6fe1 100644 --- a/Tests~/Editor/AgentToolingTests.cs +++ b/Tests~/Editor/AgentToolingTests.cs @@ -253,6 +253,40 @@ public void ListFieldsForTypeRejectsAbstractTypes() Assert.IsTrue(message.Contains("is abstract and cannot be instantiated"), message); } + [Test] + public void FindObjectsHandlesInvalidRegexAndPerformsLiteralSearch() + { + var go = new GameObject("TestObject (1)"); + try + { + JObject result = RpcResult("find_objects", new JObject { ["name"] = "TestObject (1)" }); + JArray objects = (JArray)result["objects"]; + Assert.IsNotNull(objects); + Assert.IsTrue(objects.Any(o => o["name"]?.ToString() == "TestObject (1)")); + } + finally + { + Object.DestroyImmediate(go); + } + } + + [Test] + public void FindObjectsMatchesValidRegexPattern() + { + var go = new GameObject("RegexObject123"); + try + { + JObject result = RpcResult("find_objects", new JObject { ["name"] = "^RegexObject\\d+$" }); + JArray objects = (JArray)result["objects"]; + Assert.IsNotNull(objects); + Assert.IsTrue(objects.Any(o => o["name"]?.ToString() == "RegexObject123")); + } + finally + { + Object.DestroyImmediate(go); + } + } + private void WriteTestResults(string xml) { Directory.CreateDirectory(Path.GetDirectoryName(_resultPath)); From 0a11fec4695d73910ffaad3373c90c3b03cd6009 Mon Sep 17 00:00:00 2001 From: Daliys Date: Thu, 23 Jul 2026 00:57:32 +0200 Subject: [PATCH 2/4] refactor(search): extract helper methods for readability gate limits --- Editor/MCPServerMethods.Search.cs | 204 +++++++++++++++++------------- 1 file changed, 117 insertions(+), 87 deletions(-) diff --git a/Editor/MCPServerMethods.Search.cs b/Editor/MCPServerMethods.Search.cs index 223cd41..4f83621 100644 --- a/Editor/MCPServerMethods.Search.cs +++ b/Editor/MCPServerMethods.Search.cs @@ -82,42 +82,44 @@ private static JToken FindObjects(JToken p) .Where(go => go.hideFlags == HideFlags.None); } - if (!string.IsNullOrEmpty(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 FilterByName(IEnumerable results, string name) + { + if (string.IsNullOrEmpty(name)) return results; + + System.Text.RegularExpressions.Regex regex = null; + try { - 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 - } + 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) + if (regex != null) + { + return results.Where(go => { - results = results.Where(go => + try { - try - { - return regex.IsMatch(go.name); - } - catch (System.Text.RegularExpressions.RegexMatchTimeoutException) - { - return go.name.IndexOf(name, System.StringComparison.OrdinalIgnoreCase) >= 0; - } - }); - } - else - { - results = results.Where(go => go.name.IndexOf(name, System.StringComparison.OrdinalIgnoreCase) >= 0); - } + return regex.IsMatch(go.name); + } + catch (System.Text.RegularExpressions.RegexMatchTimeoutException) + { + return go.name.IndexOf(name, System.StringComparison.OrdinalIgnoreCase) >= 0; + } + }); } - if (!string.IsNullOrEmpty(tag)) - results = results.Where(go => go.CompareTag(tag)); - - return new JObject { ["objects"] = new JArray(results.Take(50).Select(SerializeGameObject)) }; + return results.Where(go => go.name.IndexOf(name, System.StringComparison.OrdinalIgnoreCase) >= 0); } private static Stack _pathStackCache = new Stack(); @@ -156,31 +158,48 @@ private static JToken FindReferences(JToken p) if (string.IsNullOrEmpty(targetGuid) && targetId == default) throw new System.Exception("Either target_guid or target_id is required."); - Object targetObject = null; + 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 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)) - targetObject = AssetDatabase.LoadAssetAtPath(path); + return AssetDatabase.LoadAssetAtPath(path); } else if (targetId != default) { - targetObject = MCPServerMethods.IdToObject(targetId); + return MCPServerMethods.IdToObject(targetId); } + return null; + } - if (targetObject == null) - throw new System.Exception("Could not find the target object."); + 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(); - 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); @@ -191,11 +210,11 @@ private static JToken FindReferences(JToken p) assetRefs.Add(path); } } + return assetRefs; + } - var sceneRefs = new JArray(); - var allGameObjects = Resources.FindObjectsOfTypeAll() - .Where(go => go.hideFlags == HideFlags.None || go.hideFlags == HideFlags.NotEditable); - + private static HashSet BuildTargetInstanceIds(Object targetObject, string searchGuid) + { var targetInstanceIds = new HashSet(); if (targetObject != null) targetInstanceIds.Add(targetObject.GetId()); @@ -213,6 +232,14 @@ private static JToken FindReferences(JToken p) } } } + return targetInstanceIds; + } + + private static JArray FindSceneReferences(HashSet targetInstanceIds) + { + var sceneRefs = new JArray(); + var allGameObjects = Resources.FindObjectsOfTypeAll() + .Where(go => go.hideFlags == HideFlags.None || go.hideFlags == HideFlags.NotEditable); using (UnityEngine.Pool.ListPool.Get(out var components)) { @@ -221,55 +248,58 @@ private static JToken FindReferences(JToken p) if (go.scene == null || !go.scene.isLoaded) continue; go.GetComponents(components); - var goMatches = new JArray(); - - foreach (var comp in components) + var goMatches = InspectGameObjectReferences(components, targetInstanceIds); + if (goMatches.Count > 0) { - if (comp == null) continue; + var goData = new JObject + { + ["name"] = go.name, + ["instance_id"] = go.GetRawId(), + ["references"] = goMatches + }; + sceneRefs.Add(goData); + } + } + } + return sceneRefs; + } + + private static JArray InspectGameObjectReferences(List components, HashSet targetInstanceIds) + { + var goMatches = new JArray(); + foreach (var comp in components) + { + if (comp == null) continue; - using (var so = new SerializedObject(comp)) + 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 prop = so.GetIterator(); - bool enterChildren = true; - while (prop.Next(enterChildren)) + 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) { - enterChildren = false; - if (prop.propertyType == SerializedPropertyType.ObjectReference) + var matchDetail = new JObject { - 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); - } - } + ["component"] = GetTypeName(comp.GetType()), + ["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; + return goMatches; } } } From 288e126c88d6a97bd7ba8edb570029f086ae9a33 Mon Sep 17 00:00:00 2001 From: Daliys Date: Thu, 23 Jul 2026 01:00:56 +0200 Subject: [PATCH 3/4] refactor(search): split FindReferences into References.cs and isolate FindObjectsTests for readability limits --- Editor/MCPServerMethods.References.cs | 163 ++++++++++++++++++++++++++ Editor/MCPServerMethods.Search.cs | 152 ------------------------ Tests~/Editor/AgentToolingTests.cs | 34 ------ Tests~/Editor/FindObjectsTests.cs | 65 ++++++++++ 4 files changed, 228 insertions(+), 186 deletions(-) create mode 100644 Editor/MCPServerMethods.References.cs create mode 100644 Tests~/Editor/FindObjectsTests.cs diff --git a/Editor/MCPServerMethods.References.cs b/Editor/MCPServerMethods.References.cs new file mode 100644 index 0000000..111fd26 --- /dev/null +++ b/Editor/MCPServerMethods.References.cs @@ -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 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(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 BuildTargetInstanceIds(Object targetObject, string searchGuid) + { + var targetInstanceIds = new HashSet(); + 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 targetInstanceIds) + { + var sceneRefs = new JArray(); + var allGameObjects = Resources.FindObjectsOfTypeAll() + .Where(go => go.hideFlags == HideFlags.None || go.hideFlags == HideFlags.NotEditable); + + using (UnityEngine.Pool.ListPool.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 components, HashSet 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; + } + } +} diff --git a/Editor/MCPServerMethods.Search.cs b/Editor/MCPServerMethods.Search.cs index 4f83621..508d74e 100644 --- a/Editor/MCPServerMethods.Search.cs +++ b/Editor/MCPServerMethods.Search.cs @@ -149,157 +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 = 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 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(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 BuildTargetInstanceIds(Object targetObject, string searchGuid) - { - var targetInstanceIds = new HashSet(); - 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 targetInstanceIds) - { - var sceneRefs = new JArray(); - var allGameObjects = Resources.FindObjectsOfTypeAll() - .Where(go => go.hideFlags == HideFlags.None || go.hideFlags == HideFlags.NotEditable); - - using (UnityEngine.Pool.ListPool.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 components, HashSet 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; - } } } diff --git a/Tests~/Editor/AgentToolingTests.cs b/Tests~/Editor/AgentToolingTests.cs index f3e6fe1..3f74d46 100644 --- a/Tests~/Editor/AgentToolingTests.cs +++ b/Tests~/Editor/AgentToolingTests.cs @@ -253,40 +253,6 @@ public void ListFieldsForTypeRejectsAbstractTypes() Assert.IsTrue(message.Contains("is abstract and cannot be instantiated"), message); } - [Test] - public void FindObjectsHandlesInvalidRegexAndPerformsLiteralSearch() - { - var go = new GameObject("TestObject (1)"); - try - { - JObject result = RpcResult("find_objects", new JObject { ["name"] = "TestObject (1)" }); - JArray objects = (JArray)result["objects"]; - Assert.IsNotNull(objects); - Assert.IsTrue(objects.Any(o => o["name"]?.ToString() == "TestObject (1)")); - } - finally - { - Object.DestroyImmediate(go); - } - } - - [Test] - public void FindObjectsMatchesValidRegexPattern() - { - var go = new GameObject("RegexObject123"); - try - { - JObject result = RpcResult("find_objects", new JObject { ["name"] = "^RegexObject\\d+$" }); - JArray objects = (JArray)result["objects"]; - Assert.IsNotNull(objects); - Assert.IsTrue(objects.Any(o => o["name"]?.ToString() == "RegexObject123")); - } - finally - { - Object.DestroyImmediate(go); - } - } - private void WriteTestResults(string xml) { Directory.CreateDirectory(Path.GetDirectoryName(_resultPath)); diff --git a/Tests~/Editor/FindObjectsTests.cs b/Tests~/Editor/FindObjectsTests.cs new file mode 100644 index 0000000..b9187d7 --- /dev/null +++ b/Tests~/Editor/FindObjectsTests.cs @@ -0,0 +1,65 @@ +using System.Linq; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using UnityEngine; + +namespace UnityMCP.Editor.Tests +{ + public class FindObjectsTests + { + [SetUp] + public void SetUp() + { + MCPServerMethods.Init(); + } + + [Test] + public void FindObjectsHandlesInvalidRegexAndPerformsLiteralSearch() + { + var go = new GameObject("TestObject (1)"); + try + { + JObject result = RpcResult("find_objects", new JObject { ["name"] = "TestObject (1)" }); + JArray objects = (JArray)result["objects"]; + Assert.IsNotNull(objects); + Assert.IsTrue(objects.Any(o => o["name"]?.ToString() == "TestObject (1)")); + } + finally + { + Object.DestroyImmediate(go); + } + } + + [Test] + public void FindObjectsMatchesValidRegexPattern() + { + var go = new GameObject("RegexObject123"); + try + { + JObject result = RpcResult("find_objects", new JObject { ["name"] = "^RegexObject\\d+$" }); + JArray objects = (JArray)result["objects"]; + Assert.IsNotNull(objects); + Assert.IsTrue(objects.Any(o => o["name"]?.ToString() == "RegexObject123")); + } + finally + { + Object.DestroyImmediate(go); + } + } + + private static JObject RpcResult(string method, JObject parameters = null) + { + var request = new JObject + { + ["jsonrpc"] = "2.0", + ["method"] = method, + ["params"] = parameters ?? new JObject(), + ["id"] = 1 + }; + + JObject response = JObject.Parse(MCPServerMethods.ProcessJsonRpc(request.ToString(Formatting.None))); + Assert.IsNull(response["error"], response.ToString(Formatting.None)); + return (JObject)response["result"]; + } + } +} From d10b867f249f5c6e2ebe90a47113ccc2984673d3 Mon Sep 17 00:00:00 2001 From: Daliys Date: Thu, 23 Jul 2026 01:01:07 +0200 Subject: [PATCH 4/4] chore: add missing Unity .meta files --- Editor/MCPServerMethods.References.cs.meta | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Editor/MCPServerMethods.References.cs.meta diff --git a/Editor/MCPServerMethods.References.cs.meta b/Editor/MCPServerMethods.References.cs.meta new file mode 100644 index 0000000..f1a9a50 --- /dev/null +++ b/Editor/MCPServerMethods.References.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a7d83f219b4e4c91a2578b930d8c11e2