diff --git a/TestProjects/UnityMCPTests/Assets/Scripts/TestAsmdef/CustomComponent.cs b/TestProjects/UnityMCPTests/Assets/Scripts/TestAsmdef/CustomComponent.cs
new file mode 100644
index 0000000..b38e518
--- /dev/null
+++ b/TestProjects/UnityMCPTests/Assets/Scripts/TestAsmdef/CustomComponent.cs
@@ -0,0 +1,18 @@
+using UnityEngine;
+
+namespace TestNamespace
+{
+ public class CustomComponent : MonoBehaviour
+ {
+ [SerializeField]
+ private string customText = "Hello from custom asmdef!";
+
+ [SerializeField]
+ private float customFloat = 42.0f;
+
+ void Start()
+ {
+ Debug.Log($"CustomComponent started: {customText}, value: {customFloat}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/TestProjects/UnityMCPTests/Assets/Scripts/TestAsmdef/CustomComponent.cs.meta b/TestProjects/UnityMCPTests/Assets/Scripts/TestAsmdef/CustomComponent.cs.meta
new file mode 100644
index 0000000..7d94964
--- /dev/null
+++ b/TestProjects/UnityMCPTests/Assets/Scripts/TestAsmdef/CustomComponent.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 78ee39b9744834fe390a4c7c5634eb5a
\ No newline at end of file
diff --git a/TestProjects/UnityMCPTests/Assets/Scripts/TestAsmdef/TestAsmdef.asmdef b/TestProjects/UnityMCPTests/Assets/Scripts/TestAsmdef/TestAsmdef.asmdef
new file mode 100644
index 0000000..04cfdce
--- /dev/null
+++ b/TestProjects/UnityMCPTests/Assets/Scripts/TestAsmdef/TestAsmdef.asmdef
@@ -0,0 +1,14 @@
+{
+ "name": "TestAsmdef",
+ "rootNamespace": "TestNamespace",
+ "references": [],
+ "includePlatforms": [],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
\ No newline at end of file
diff --git a/TestProjects/UnityMCPTests/Assets/Scripts/TestAsmdef/TestAsmdef.asmdef.meta b/TestProjects/UnityMCPTests/Assets/Scripts/TestAsmdef/TestAsmdef.asmdef.meta
new file mode 100644
index 0000000..21f1fe3
--- /dev/null
+++ b/TestProjects/UnityMCPTests/Assets/Scripts/TestAsmdef/TestAsmdef.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 72f6376fa7bdc4220b11ccce20108cdc
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Tools/ManageAsset.cs b/UnityMcpBridge/Editor/Tools/ManageAsset.cs
index bef4f9a..a05931f 100644
--- a/UnityMcpBridge/Editor/Tools/ManageAsset.cs
+++ b/UnityMcpBridge/Editor/Tools/ManageAsset.cs
@@ -201,7 +201,7 @@ namespace MCPForUnity.Editor.Tools
"'scriptClass' property required when creating ScriptableObject asset."
);
- Type scriptType = FindType(scriptClassName);
+ Type scriptType = ComponentResolver.TryResolve(scriptClassName, out var resolvedType, out var error) ? resolvedType : null;
if (
scriptType == null
|| !typeof(ScriptableObject).IsAssignableFrom(scriptType)
@@ -355,7 +355,7 @@ namespace MCPForUnity.Editor.Tools
{
// Find the component on the GameObject using the name from the JSON key
// Using GetComponent(string) is convenient but might require exact type name or be ambiguous.
- // Consider using FindType helper if needed for more complex scenarios.
+ // Consider using ComponentResolver if needed for more complex scenarios.
Component targetComponent = gameObject.GetComponent(componentName);
if (targetComponent != null)
@@ -1220,46 +1220,6 @@ namespace MCPForUnity.Editor.Tools
}
}
- ///
- /// Helper to find a Type by name, searching relevant assemblies.
- /// Needed for creating ScriptableObjects or finding component types by name.
- ///
- private static Type FindType(string typeName)
- {
- if (string.IsNullOrEmpty(typeName))
- return null;
-
- // Try direct lookup first (common Unity types often don't need assembly qualified name)
- var type =
- Type.GetType(typeName)
- ?? Type.GetType($"UnityEngine.{typeName}, UnityEngine.CoreModule")
- ?? Type.GetType($"UnityEngine.UI.{typeName}, UnityEngine.UI")
- ?? Type.GetType($"UnityEditor.{typeName}, UnityEditor.CoreModule");
-
- if (type != null)
- return type;
-
- // If not found, search loaded assemblies (slower but more robust for user scripts)
- foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
- {
- // Look for non-namespaced first
- type = assembly.GetType(typeName, false, true); // throwOnError=false, ignoreCase=true
- if (type != null)
- return type;
-
- // Check common namespaces if simple name given
- type = assembly.GetType("UnityEngine." + typeName, false, true);
- if (type != null)
- return type;
- type = assembly.GetType("UnityEditor." + typeName, false, true);
- if (type != null)
- return type;
- // Add other likely namespaces if needed (e.g., specific plugins)
- }
-
- Debug.LogWarning($"[FindType] Type '{typeName}' not found in any loaded assembly.");
- return null; // Not found
- }
// --- Data Serialization ---
diff --git a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs
index 970eca8..3e0def8 100644
--- a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs
+++ b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs
@@ -1,3 +1,4 @@
+#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
@@ -5,6 +6,7 @@ using System.Reflection;
using Newtonsoft.Json; // Added for JsonSerializationException
using Newtonsoft.Json.Linq;
using UnityEditor;
+using UnityEditor.Compilation; // For CompilationPipeline
using UnityEditor.SceneManagement;
using UnityEditorInternal;
using UnityEngine;
@@ -1097,6 +1099,29 @@ namespace MCPForUnity.Editor.Tools
// --- Internal Helpers ---
+ ///
+ /// Parses a JArray like [x, y, z] into a Vector3.
+ ///
+ private static Vector3? ParseVector3(JArray array)
+ {
+ if (array != null && array.Count == 3)
+ {
+ try
+ {
+ return new Vector3(
+ array[0].ToObject(),
+ array[1].ToObject(),
+ array[2].ToObject()
+ );
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"Failed to parse JArray as Vector3: {array}. Error: {ex.Message}");
+ }
+ }
+ return null;
+ }
+
///
/// Finds a single GameObject based on token (ID, name, path) and search method.
///
@@ -2070,58 +2095,137 @@ namespace MCPForUnity.Editor.Tools
///
- /// Helper to find a Type by name, searching relevant assemblies.
+ /// Robust component resolver that avoids Assembly.LoadFrom and works with asmdefs.
+ /// Searches already-loaded assemblies, prioritizing runtime script assemblies.
///
private static Type FindType(string typeName)
{
- if (string.IsNullOrEmpty(typeName))
- return null;
-
- // Handle fully qualified names first
- Type type = Type.GetType(typeName);
- if (type != null) return type;
-
- // Handle common namespaces implicitly (add more as needed)
- string[] namespaces = { "UnityEngine", "UnityEngine.UI", "UnityEngine.AI", "UnityEngine.Animations", "UnityEngine.Audio", "UnityEngine.EventSystems", "UnityEngine.InputSystem", "UnityEngine.Networking", "UnityEngine.Rendering", "UnityEngine.SceneManagement", "UnityEngine.Tilemaps", "UnityEngine.U2D", "UnityEngine.Video", "UnityEditor", "UnityEditor.AI", "UnityEditor.Animations", "UnityEditor.Experimental.GraphView", "UnityEditor.IMGUI.Controls", "UnityEditor.PackageManager.UI", "UnityEditor.SceneManagement", "UnityEditor.UI", "UnityEditor.U2D", "UnityEditor.VersionControl" }; // Add more relevant namespaces
-
- foreach (string ns in namespaces) {
- type = Type.GetType($"{ns}.{typeName}, {ns.Split('.')[0]}.CoreModule") ?? // Heuristic: Check CoreModule first for UnityEngine/UnityEditor
- Type.GetType($"{ns}.{typeName}, {ns.Split('.')[0]}"); // Try assembly matching namespace root
- if (type != null) return type;
- }
-
-
- // If not found, search all loaded assemblies (slower, last resort)
- // Prioritize assemblies likely to contain game/editor types
- Assembly[] priorityAssemblies = {
- Assembly.Load("Assembly-CSharp"), // Main game scripts
- Assembly.Load("Assembly-CSharp-Editor"), // Main editor scripts
- // Add other important project assemblies if known
- };
- foreach (var assembly in priorityAssemblies.Where(a => a != null))
- {
- type = assembly.GetType(typeName) ?? assembly.GetType("UnityEngine." + typeName) ?? assembly.GetType("UnityEditor." + typeName);
- if (type != null) return type;
- }
-
- // Search remaining assemblies
- foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Except(priorityAssemblies))
+ if (ComponentResolver.TryResolve(typeName, out Type resolvedType, out string error))
{
- try { // Protect against assembly loading errors
- type = assembly.GetType(typeName);
- if (type != null) return type;
- // Also check with common namespaces if simple name given
- foreach (string ns in namespaces) {
- type = assembly.GetType($"{ns}.{typeName}");
- if (type != null) return type;
- }
- } catch (Exception ex) {
- Debug.LogWarning($"[FindType] Error searching assembly {assembly.FullName}: {ex.Message}");
- }
+ return resolvedType;
+ }
+
+ // Log the resolver error if type wasn't found
+ if (!string.IsNullOrEmpty(error))
+ {
+ Debug.LogWarning($"[FindType] {error}");
+ }
+
+ return null;
+ }
+ }
+
+ ///
+ /// Robust component resolver that avoids Assembly.LoadFrom and supports assembly definitions.
+ /// Prioritizes runtime (Player) assemblies over Editor assemblies.
+ ///
+ internal static class ComponentResolver
+ {
+ private static readonly Dictionary CacheByFqn = new(StringComparer.Ordinal);
+ private static readonly Dictionary CacheByName = new(StringComparer.Ordinal);
+
+ ///
+ /// Resolve a Component/MonoBehaviour type by short or fully-qualified name.
+ /// Prefers runtime (Player) script assemblies; falls back to Editor assemblies.
+ /// Never uses Assembly.LoadFrom.
+ ///
+ public static bool TryResolve(string nameOrFullName, out Type type, out string error)
+ {
+ error = string.Empty;
+
+ // 1) Exact FQN via Type.GetType
+ if (CacheByFqn.TryGetValue(nameOrFullName, out type)) return true;
+ type = Type.GetType(nameOrFullName, throwOnError: false);
+ if (IsValidComponent(type)) { Cache(type); return true; }
+
+ // 2) Search loaded assemblies (prefer Player assemblies)
+ var candidates = FindCandidates(nameOrFullName);
+ if (candidates.Count == 1) { type = candidates[0]; Cache(type); return true; }
+ if (candidates.Count > 1) { error = Ambiguity(nameOrFullName, candidates); type = null!; return false; }
+
+#if UNITY_EDITOR
+ // 3) Last resort: Editor-only TypeCache (fast index)
+ var tc = TypeCache.GetTypesDerivedFrom()
+ .Where(t => NamesMatch(t, nameOrFullName));
+ candidates = PreferPlayer(tc).ToList();
+ if (candidates.Count == 1) { type = candidates[0]; Cache(type); return true; }
+ if (candidates.Count > 1) { error = Ambiguity(nameOrFullName, candidates); type = null!; return false; }
+#endif
+
+ error = $"Component type '{nameOrFullName}' not found in loaded runtime assemblies. " +
+ "Use a fully-qualified name (Namespace.TypeName) and ensure the script compiled.";
+ type = null!;
+ return false;
+ }
+
+ private static bool NamesMatch(Type t, string q) =>
+ t.Name.Equals(q, StringComparison.Ordinal) ||
+ (t.FullName?.Equals(q, StringComparison.Ordinal) ?? false);
+
+ private static bool IsValidComponent(Type? t) =>
+ t != null && typeof(Component).IsAssignableFrom(t);
+
+ private static void Cache(Type t)
+ {
+ if (t.FullName != null) CacheByFqn[t.FullName] = t;
+ CacheByName[t.Name] = t;
+ }
+
+ private static List FindCandidates(string query)
+ {
+ bool isShort = !query.Contains('.');
+ var loaded = AppDomain.CurrentDomain.GetAssemblies();
+
+#if UNITY_EDITOR
+ // Names of Player (runtime) script assemblies (asmdefs + Assembly-CSharp)
+ var playerAsmNames = new HashSet(
+ UnityEditor.Compilation.CompilationPipeline.GetAssemblies(UnityEditor.Compilation.AssembliesType.Player).Select(a => a.name),
+ StringComparer.Ordinal);
+
+ IEnumerable playerAsms = loaded.Where(a => playerAsmNames.Contains(a.GetName().Name));
+ IEnumerable editorAsms = loaded.Except(playerAsms);
+#else
+ IEnumerable playerAsms = loaded;
+ IEnumerable editorAsms = Array.Empty();
+#endif
+ static IEnumerable SafeGetTypes(System.Reflection.Assembly a)
+ {
+ try { return a.GetTypes(); }
+ catch (ReflectionTypeLoadException rtle) { return rtle.Types.Where(t => t != null)!; }
}
- Debug.LogWarning($"[FindType] Type not found after extensive search: '{typeName}'");
- return null; // Not found
+ Func match = isShort
+ ? (t => t.Name.Equals(query, StringComparison.Ordinal))
+ : (t => t.FullName!.Equals(query, StringComparison.Ordinal));
+
+ var fromPlayer = playerAsms.SelectMany(SafeGetTypes)
+ .Where(IsValidComponent)
+ .Where(match);
+ var fromEditor = editorAsms.SelectMany(SafeGetTypes)
+ .Where(IsValidComponent)
+ .Where(match);
+
+ var list = new List(fromPlayer);
+ if (list.Count == 0) list.AddRange(fromEditor);
+ return list;
+ }
+
+#if UNITY_EDITOR
+ private static IEnumerable PreferPlayer(IEnumerable seq)
+ {
+ var player = new HashSet(
+ UnityEditor.Compilation.CompilationPipeline.GetAssemblies(UnityEditor.Compilation.AssembliesType.Player).Select(a => a.name),
+ StringComparer.Ordinal);
+
+ return seq.OrderBy(t => player.Contains(t.Assembly.GetName().Name) ? 0 : 1);
+ }
+#endif
+
+ private static string Ambiguity(string query, IEnumerable cands)
+ {
+ var lines = cands.Select(t => $"{t.FullName} (assembly {t.Assembly.GetName().Name})");
+ return $"Multiple component types matched '{query}':\n - " + string.Join("\n - ", lines) +
+ "\nProvide a fully qualified type name to disambiguate.";
}
///