From b80b9abe46adb822e31141478572777047d7949f Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Sat, 25 Oct 2025 01:13:58 -0400 Subject: [PATCH 1/8] Update .Bat file and Bug fix on ManageScript * Update the .Bat file to include runtime folder * Fix the inconsistent EditorPrefs variable so the GUI change on Script Validation could cause real change. --- MCPForUnity/Editor/Tools/ManageScript.cs | 2 +- deploy-dev.bat | 4 ++-- restore-dev.bat | 22 ++++++++++++++++++---- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/MCPForUnity/Editor/Tools/ManageScript.cs b/MCPForUnity/Editor/Tools/ManageScript.cs index 51669c657..db5679c07 100644 --- a/MCPForUnity/Editor/Tools/ManageScript.cs +++ b/MCPForUnity/Editor/Tools/ManageScript.cs @@ -1933,7 +1933,7 @@ string namespaceName /// private static ValidationLevel GetValidationLevelFromGUI() { - string savedLevel = EditorPrefs.GetString("MCPForUnity_ScriptValidationLevel", "standard"); + string savedLevel = EditorPrefs.GetString("MCPForUnity.ValidationLevel", "standard"); return savedLevel.ToLower() switch { "basic" => ValidationLevel.Basic, diff --git a/deploy-dev.bat b/deploy-dev.bat index 60a398bd9..300856d3d 100644 --- a/deploy-dev.bat +++ b/deploy-dev.bat @@ -11,7 +11,7 @@ set "SCRIPT_DIR=%~dp0" set "BRIDGE_SOURCE=%SCRIPT_DIR%MCPForUnity" set "SERVER_SOURCE=%SCRIPT_DIR%MCPForUnity\UnityMcpServer~\src" set "DEFAULT_BACKUP_DIR=%USERPROFILE%\Desktop\unity-mcp-backup" -set "DEFAULT_SERVER_PATH=%LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer\src" +set "DEFAULT_SERVER_PATH=%LOCALAPPDATA%\UnityMCP\UnityMcpServer\src" :: Get user inputs echo Please provide the following paths: @@ -19,7 +19,7 @@ echo. :: Package cache location echo Unity Package Cache Location: -echo Example: X:\UnityProject\Library\PackageCache\com.coplaydev.unity-mcp@1.0.0 +echo Example: X:\Unity\Projects\UnityMCPTestbed2\Library\PackageCache\com.coplaydev.unity-mcp@4c106125b342 set /p "PACKAGE_CACHE_PATH=Enter Unity package cache path: " if "%PACKAGE_CACHE_PATH%"=="" ( diff --git a/restore-dev.bat b/restore-dev.bat index 6f68be0ba..15184412c 100644 --- a/restore-dev.bat +++ b/restore-dev.bat @@ -102,7 +102,8 @@ echo =============================================== echo WARNING: This will overwrite current files! echo =============================================== echo Restoring from: %SELECTED_BACKUP% -echo Unity Bridge target: %PACKAGE_CACHE_PATH%\Editor +echo Unity Bridge Editor target: %PACKAGE_CACHE_PATH%\Editor +echo Unity Bridge Runtime target: %PACKAGE_CACHE_PATH%\Runtime echo Python Server target: %SERVER_PATH% echo. set /p "confirm=Continue with restore? (y/N): " @@ -119,16 +120,29 @@ echo =============================================== :: Restore Unity Bridge if exist "%SELECTED_BACKUP%\UnityBridge\Editor" ( - echo Restoring Unity Bridge files... + echo Restoring Unity Bridge Editor files... rd /s /q "%PACKAGE_CACHE_PATH%\Editor" 2>nul xcopy "%SELECTED_BACKUP%\UnityBridge\Editor\*" "%PACKAGE_CACHE_PATH%\Editor\" /E /I /Y > nul if !errorlevel! neq 0 ( - echo Error: Failed to restore Unity Bridge files + echo Error: Failed to restore Unity Bridge Editor files pause exit /b 1 ) ) else ( - echo Warning: No Unity Bridge backup found, skipping... + echo Warning: No Unity Bridge Editor backup found, skipping... +) + +if exist "%SELECTED_BACKUP%\UnityBridge\Runtime" ( + echo Restoring Unity Bridge Runtime files... + rd /s /q "%PACKAGE_CACHE_PATH%\Runtime" 2>nul + xcopy "%SELECTED_BACKUP%\UnityBridge\Runtime\*" "%PACKAGE_CACHE_PATH%\Runtime\" /E /I /Y > nul + if !errorlevel! neq 0 ( + echo Error: Failed to restore Unity Bridge Runtime files + pause + exit /b 1 + ) +) else ( + echo Warning: No Unity Bridge Runtime backup found, skipping... ) :: Restore Python Server From 4e9604f249cf9e13fd0750bef747f84ad9c35efa Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Sat, 25 Oct 2025 11:19:11 -0400 Subject: [PATCH 2/8] Further changes String to Int for consistency --- MCPForUnity/Editor/Tools/ManageScript.cs | 11 ++--------- restore-dev.bat | 2 +- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/MCPForUnity/Editor/Tools/ManageScript.cs b/MCPForUnity/Editor/Tools/ManageScript.cs index db5679c07..b5cbbb1d3 100644 --- a/MCPForUnity/Editor/Tools/ManageScript.cs +++ b/MCPForUnity/Editor/Tools/ManageScript.cs @@ -1933,15 +1933,8 @@ string namespaceName /// private static ValidationLevel GetValidationLevelFromGUI() { - string savedLevel = EditorPrefs.GetString("MCPForUnity.ValidationLevel", "standard"); - return savedLevel.ToLower() switch - { - "basic" => ValidationLevel.Basic, - "standard" => ValidationLevel.Standard, - "comprehensive" => ValidationLevel.Comprehensive, - "strict" => ValidationLevel.Strict, - _ => ValidationLevel.Standard // Default fallback - }; + int savedLevel = EditorPrefs.GetInt("MCPForUnity.ValidationLevel", (int)ValidationLevel.Standard); + return (ValidationLevel)Mathf.Clamp(savedLevel, 0, 3); } /// diff --git a/restore-dev.bat b/restore-dev.bat index 15184412c..81311f6c2 100644 --- a/restore-dev.bat +++ b/restore-dev.bat @@ -11,7 +11,7 @@ echo. :: Configuration set "DEFAULT_BACKUP_DIR=%USERPROFILE%\Desktop\unity-mcp-backup" -set "DEFAULT_SERVER_PATH=%LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer\src" +set "DEFAULT_SERVER_PATH=%LOCALAPPDATA%\UnityMCP\UnityMcpServer\src" :: Get user inputs echo Please provide the following paths: From c5b2d35254baa6316ae6e35dcb671515e9cbdb6b Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Sun, 9 Nov 2025 23:31:59 -0500 Subject: [PATCH 3/8] [Custom Tool] Roslyn Runtime Compilation Allows users to generate/compile codes during Playmode --- .../ManageRuntimeCompilation.cs | 526 +++++++ .../ManageRuntimeCompilation.cs.meta | 2 + .../PythonTools.asset.meta | 8 + .../RoslynRuntimeCompilation/RoslynRuntime.md | 3 + .../RoslynRuntimeCompiler.cs | 1211 +++++++++++++++++ .../RoslynRuntimeCompiler.cs.meta | 2 + .../runtime_compilation_tool.py | 276 ++++ .../runtime_compilation_tool.py.meta | 10 + 8 files changed, 2038 insertions(+) create mode 100644 CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs create mode 100644 CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs.meta create mode 100644 CustomTools/RoslynRuntimeCompilation/PythonTools.asset.meta create mode 100644 CustomTools/RoslynRuntimeCompilation/RoslynRuntime.md create mode 100644 CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs create mode 100644 CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs.meta create mode 100644 CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py create mode 100644 CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py.meta diff --git a/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs b/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs new file mode 100644 index 000000000..401c7530d --- /dev/null +++ b/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs @@ -0,0 +1,526 @@ +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Newtonsoft.Json.Linq; +using UnityEngine; +using UnityEditor; +using MCPForUnity.Editor.Helpers; + +#if USE_ROSLYN +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; +#endif + +namespace MCPForUnity.Editor.Tools +{ + /// + /// Runtime compilation tool for MCP Unity. + /// Compiles and loads C# code at runtime without triggering domain reload. + /// + [McpForUnityTool("runtime_compilation")] + public static class ManageRuntimeCompilation + { + private static readonly Dictionary LoadedAssemblies = new Dictionary(); + private static string DynamicAssembliesPath => Path.Combine(Application.temporaryCachePath, "DynamicAssemblies"); + + private class LoadedAssemblyInfo + { + public string Name; + public Assembly Assembly; + public string DllPath; + public DateTime LoadedAt; + public List TypeNames; + } + + public static object HandleCommand(JObject @params) + { + string action = @params["action"]?.ToString()?.ToLower(); + + if (string.IsNullOrEmpty(action)) + { + return Response.Error("Action parameter is required. Valid actions: compile_and_load, list_loaded, get_types, execute_with_roslyn, get_history, save_history, clear_history"); + } + + switch (action) + { + case "compile_and_load": + return CompileAndLoad(@params); + + case "list_loaded": + return ListLoadedAssemblies(); + + case "get_types": + return GetAssemblyTypes(@params); + + case "execute_with_roslyn": + return ExecuteWithRoslyn(@params); + + case "get_history": + return GetCompilationHistory(); + + case "save_history": + return SaveCompilationHistory(); + + case "clear_history": + return ClearCompilationHistory(); + + default: + return Response.Error($"Unknown action '{action}'. Valid actions: compile_and_load, list_loaded, get_types, execute_with_roslyn, get_history, save_history, clear_history"); + } + } + + private static object CompileAndLoad(JObject @params) + { +#if !USE_ROSLYN + return Response.Error( + "Runtime compilation requires Roslyn. Please install Microsoft.CodeAnalysis.CSharp NuGet package and add USE_ROSLYN to Scripting Define Symbols. " + + "See ManageScript.cs header for installation instructions." + ); +#else + try + { + string code = @params["code"]?.ToString(); + string assemblyName = @params["assembly_name"]?.ToString() ?? $"DynamicAssembly_{DateTime.Now.Ticks}"; + string attachTo = @params["attach_to"]?.ToString(); + bool loadImmediately = @params["load_immediately"]?.ToObject() ?? true; + + if (string.IsNullOrEmpty(code)) + { + return Response.Error("'code' parameter is required"); + } + + // Ensure unique assembly name + if (LoadedAssemblies.ContainsKey(assemblyName)) + { + assemblyName = $"{assemblyName}_{DateTime.Now.Ticks}"; + } + + // Create output directory + Directory.CreateDirectory(DynamicAssembliesPath); + string dllPath = Path.Combine(DynamicAssembliesPath, $"{assemblyName}.dll"); + + // Parse code + var syntaxTree = CSharpSyntaxTree.ParseText(code); + + // Get references + var references = GetDefaultReferences(); + + // Create compilation + var compilation = CSharpCompilation.Create( + assemblyName, + new[] { syntaxTree }, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) + .WithOptimizationLevel(OptimizationLevel.Debug) + .WithPlatform(Platform.AnyCpu) + ); + + // Emit to file + EmitResult emitResult; + using (var stream = new FileStream(dllPath, FileMode.Create)) + { + emitResult = compilation.Emit(stream); + } + + // Check for compilation errors + if (!emitResult.Success) + { + var errors = emitResult.Diagnostics + .Where(d => d.Severity == DiagnosticSeverity.Error) + .Select(d => new + { + line = d.Location.GetLineSpan().StartLinePosition.Line + 1, + column = d.Location.GetLineSpan().StartLinePosition.Character + 1, + message = d.GetMessage(), + id = d.Id + }) + .ToList(); + + return Response.Error("Compilation failed", new + { + errors = errors, + error_count = errors.Count + }); + } + + // Load assembly if requested + Assembly loadedAssembly = null; + List typeNames = new List(); + + if (loadImmediately) + { + loadedAssembly = Assembly.LoadFrom(dllPath); + typeNames = loadedAssembly.GetTypes().Select(t => t.FullName).ToList(); + + // Store info + LoadedAssemblies[assemblyName] = new LoadedAssemblyInfo + { + Name = assemblyName, + Assembly = loadedAssembly, + DllPath = dllPath, + LoadedAt = DateTime.Now, + TypeNames = typeNames + }; + + Debug.Log($"[MCP] Runtime compilation successful: {assemblyName} ({typeNames.Count} types)"); + } + + // Optionally attach to GameObject + GameObject attachedTo = null; + Type attachedType = null; + + if (!string.IsNullOrEmpty(attachTo) && loadedAssembly != null) + { + var go = GameObject.Find(attachTo); + if (go == null) + { + // Try hierarchical path search + go = FindGameObjectByPath(attachTo); + } + + if (go != null) + { + // Find first MonoBehaviour type + var behaviourType = loadedAssembly.GetTypes() + .FirstOrDefault(t => t.IsSubclassOf(typeof(MonoBehaviour)) && !t.IsAbstract); + + if (behaviourType != null) + { + go.AddComponent(behaviourType); + attachedTo = go; + attachedType = behaviourType; + Debug.Log($"[MCP] Attached {behaviourType.Name} to {go.name}"); + } + else + { + Debug.LogWarning($"[MCP] No MonoBehaviour types found in {assemblyName} to attach"); + } + } + else + { + Debug.LogWarning($"[MCP] GameObject '{attachTo}' not found"); + } + } + + return Response.Success("Runtime compilation completed successfully", new + { + assembly_name = assemblyName, + dll_path = dllPath, + loaded = loadImmediately, + type_count = typeNames.Count, + types = typeNames, + attached_to = attachedTo != null ? attachedTo.name : null, + attached_type = attachedType != null ? attachedType.FullName : null + }); + } + catch (Exception ex) + { + return Response.Error($"Runtime compilation failed: {ex.Message}", new + { + exception = ex.GetType().Name, + stack_trace = ex.StackTrace + }); + } +#endif + } + + private static object ListLoadedAssemblies() + { + var assemblies = LoadedAssemblies.Values.Select(info => new + { + name = info.Name, + dll_path = info.DllPath, + loaded_at = info.LoadedAt.ToString("o"), + type_count = info.TypeNames.Count, + types = info.TypeNames + }).ToList(); + + return Response.Success($"Found {assemblies.Count} loaded dynamic assemblies", new + { + count = assemblies.Count, + assemblies = assemblies + }); + } + + private static object GetAssemblyTypes(JObject @params) + { + string assemblyName = @params["assembly_name"]?.ToString(); + + if (string.IsNullOrEmpty(assemblyName)) + { + return Response.Error("'assembly_name' parameter is required"); + } + + if (!LoadedAssemblies.TryGetValue(assemblyName, out var info)) + { + return Response.Error($"Assembly '{assemblyName}' not found in loaded assemblies"); + } + + var types = info.Assembly.GetTypes().Select(t => new + { + full_name = t.FullName, + name = t.Name, + @namespace = t.Namespace, + is_class = t.IsClass, + is_abstract = t.IsAbstract, + is_monobehaviour = t.IsSubclassOf(typeof(MonoBehaviour)), + base_type = t.BaseType?.FullName + }).ToList(); + + return Response.Success($"Retrieved {types.Count} types from {assemblyName}", new + { + assembly_name = assemblyName, + type_count = types.Count, + types = types + }); + } + + /// + /// Execute code using RoslynRuntimeCompiler with full GUI tool integration + /// Supports MonoBehaviours, static methods, and coroutines + /// + private static object ExecuteWithRoslyn(JObject @params) + { + try + { + string code = @params["code"]?.ToString(); + string className = @params["class_name"]?.ToString() ?? "AIGenerated"; + string methodName = @params["method_name"]?.ToString() ?? "Run"; + string targetObjectName = @params["target_object"]?.ToString(); + bool attachAsComponent = @params["attach_as_component"]?.ToObject() ?? false; + + if (string.IsNullOrEmpty(code)) + { + return Response.Error("'code' parameter is required"); + } + + // Get or create the RoslynRuntimeCompiler instance + var compiler = GetOrCreateRoslynCompiler(); + + // Find target GameObject if specified + GameObject targetObject = null; + if (!string.IsNullOrEmpty(targetObjectName)) + { + targetObject = GameObject.Find(targetObjectName); + if (targetObject == null) + { + targetObject = FindGameObjectByPath(targetObjectName); + } + + if (targetObject == null) + { + return Response.Error($"Target GameObject '{targetObjectName}' not found"); + } + } + + // Use the RoslynRuntimeCompiler's CompileAndExecute method + bool success = compiler.CompileAndExecute( + code, + className, + methodName, + targetObject, + attachAsComponent, + out string errorMessage + ); + + if (success) + { + return Response.Success($"Code compiled and executed successfully", new + { + class_name = className, + method_name = methodName, + target_object = targetObject != null ? targetObject.name : "compiler_host", + attached_as_component = attachAsComponent, + diagnostics = compiler.lastCompileDiagnostics + }); + } + else + { + return Response.Error($"Execution failed: {errorMessage}", new + { + diagnostics = compiler.lastCompileDiagnostics + }); + } + } + catch (Exception ex) + { + return Response.Error($"Failed to execute with Roslyn: {ex.Message}", new + { + exception = ex.GetType().Name, + stack_trace = ex.StackTrace + }); + } + } + + /// + /// Get compilation history from RoslynRuntimeCompiler + /// + private static object GetCompilationHistory() + { + try + { + var compiler = GetOrCreateRoslynCompiler(); + var history = compiler.CompilationHistory; + + var historyData = history.Select(entry => new + { + timestamp = entry.timestamp, + type_name = entry.typeName, + method_name = entry.methodName, + success = entry.success, + diagnostics = entry.diagnostics, + execution_target = entry.executionTarget, + source_code_preview = entry.sourceCode.Length > 200 + ? entry.sourceCode.Substring(0, 200) + "..." + : entry.sourceCode + }).ToList(); + + return Response.Success($"Retrieved {historyData.Count} history entries", new + { + count = historyData.Count, + history = historyData + }); + } + catch (Exception ex) + { + return Response.Error($"Failed to get history: {ex.Message}"); + } + } + + /// + /// Save compilation history to JSON file + /// + private static object SaveCompilationHistory() + { + try + { + var compiler = GetOrCreateRoslynCompiler(); + + if (compiler.SaveHistoryToFile(out string savedPath, out string error)) + { + return Response.Success($"History saved successfully", new + { + path = savedPath, + entry_count = compiler.CompilationHistory.Count + }); + } + else + { + return Response.Error($"Failed to save history: {error}"); + } + } + catch (Exception ex) + { + return Response.Error($"Failed to save history: {ex.Message}"); + } + } + + /// + /// Clear compilation history + /// + private static object ClearCompilationHistory() + { + try + { + var compiler = GetOrCreateRoslynCompiler(); + int count = compiler.CompilationHistory.Count; + compiler.ClearHistory(); + + return Response.Success($"Cleared {count} history entries"); + } + catch (Exception ex) + { + return Response.Error($"Failed to clear history: {ex.Message}"); + } + } + +#if USE_ROSLYN + private static List GetDefaultReferences() + { + var references = new List(); + + // Add core .NET references + references.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); + references.Add(MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location)); + + // Add Unity references + var unityEngine = typeof(UnityEngine.Object).Assembly.Location; + references.Add(MetadataReference.CreateFromFile(unityEngine)); + + // Add UnityEditor if available + try + { + var unityEditor = typeof(UnityEditor.Editor).Assembly.Location; + references.Add(MetadataReference.CreateFromFile(unityEditor)); + } + catch { /* Editor assembly not always needed */ } + + // Add Assembly-CSharp (user scripts) + try + { + var assemblyCSharp = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name == "Assembly-CSharp"); + if (assemblyCSharp != null) + { + references.Add(MetadataReference.CreateFromFile(assemblyCSharp.Location)); + } + } + catch { /* User assembly not always needed */ } + + return references; + } +#endif + + private static GameObject FindGameObjectByPath(string path) + { + // Handle hierarchical paths like "Canvas/Panel/Button" + var parts = path.Split('/'); + GameObject current = null; + + foreach (var part in parts) + { + if (current == null) + { + // Find root object + current = GameObject.Find(part); + } + else + { + // Find child + var transform = current.transform.Find(part); + if (transform == null) + return null; + current = transform.gameObject; + } + } + + return current; + } + + /// + /// Get or create a RoslynRuntimeCompiler instance for GUI integration + /// This allows MCP commands to leverage the existing GUI tool + /// + private static RoslynRuntimeCompiler GetOrCreateRoslynCompiler() + { + var existing = UnityEngine.Object.FindFirstObjectByType(); + if (existing != null) + { + return existing; + } + + var go = new GameObject("MCPRoslynCompiler"); + var compiler = go.AddComponent(); + compiler.enableHistory = true; // Enable history tracking for MCP operations + if (!Application.isPlaying) + { + go.hideFlags = HideFlags.HideAndDontSave; + } + return compiler; + } + } +} diff --git a/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs.meta b/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs.meta new file mode 100644 index 000000000..b33b4f643 --- /dev/null +++ b/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1c3b2419382faa04481f4a631c510ee6 \ No newline at end of file diff --git a/CustomTools/RoslynRuntimeCompilation/PythonTools.asset.meta b/CustomTools/RoslynRuntimeCompilation/PythonTools.asset.meta new file mode 100644 index 000000000..58dd3a6f9 --- /dev/null +++ b/CustomTools/RoslynRuntimeCompilation/PythonTools.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a3b463767742cdf43b366f68a656e42e +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/CustomTools/RoslynRuntimeCompilation/RoslynRuntime.md b/CustomTools/RoslynRuntimeCompilation/RoslynRuntime.md new file mode 100644 index 000000000..91d05d5b8 --- /dev/null +++ b/CustomTools/RoslynRuntimeCompilation/RoslynRuntime.md @@ -0,0 +1,3 @@ +# Roslyn Runtime Compilation Tool + +This custom tool uses Roslyn Runtime Compilation to have users run script generation and compilation during Playmode in realtime, where in traditional Unity workflow it would take seconds to reload assets and reset script states for each script change. diff --git a/CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs b/CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs new file mode 100644 index 000000000..79a26dcde --- /dev/null +++ b/CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs @@ -0,0 +1,1211 @@ +// RoslynRuntimeCompiler.cs +// Single-file Unity tool for Editor+PlayMode dynamic C# compilation using Roslyn. +// Features: +// - EditorWindow GUI with a large text area for LLM-generated code +// - Compile button (compiles in-memory using Roslyn) +// - Run button (invokes a well-known entry point in the compiled assembly) +// - Shows compile errors and runtime exceptions +// - Safe: Does NOT write .cs files to Assets (no Domain Reload) +// +// Requirements: +// 1) Add Microsoft.CodeAnalysis.CSharp.dll and Microsoft.CodeAnalysis.dll to your Unity project +// (place under Assets/Plugins or Packages and target the Editor). These come from the Roslyn nuget package. +// 2) This tool is designed to run in the Unity Editor (Play Mode or Edit Mode). It uses Assembly.Load(byte[]). +// 3) Generated code should expose a public type and a public static entry method matching one of the supported signatures: +// - public static void Run(UnityEngine.GameObject host) +// - public static void Run(UnityEngine.MonoBehaviour host) +// - public static System.Collections.IEnumerator RunCoroutine(UnityEngine.MonoBehaviour host) // if you want a coroutine +// By convention this demo looks for a type name you specify in the window (default: "AIGenerated"). +// +// Usage: +// - Window -> Roslyn Runtime Compiler +// - Paste code into the big text area (or use LLM output pasted there) +// - Optionally set Entry Type (default AIGenerated) and Entry Method (default Run) +// - Press "Compile". Compiler diagnostics appear below. +// - In Play Mode, press "Run" to invoke the entry method. In Edit Mode it will attempt to run if valid. +// +// Security note: Any dynamically compiled code runs with the same permissions as the editor. Be careful when running untrusted code. + +#if UNITY_EDITOR +using UnityEditor; +#endif +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Collections.Generic; +using UnityEngine; + +#if UNITY_EDITOR +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +#endif + +public class RoslynRuntimeCompiler : MonoBehaviour +{ + [TextArea(8, 20)] + [Tooltip("Code to compile at runtime. Example class name: AIGenerated with public static void Run(GameObject host)")] + public string code = "using UnityEngine;\npublic class AIGenerated {\n public static void Run(GameObject host) {\n Debug.Log($\"Hello from AI - {host.name}\");\n host.transform.Rotate(Vector3.up * 45f * Time.deltaTime);\n }\n}"; + + [Tooltip("Fully qualified type name to invoke (default: AIGenerated)")] + public string entryTypeName = "AIGenerated"; + [Tooltip("Method name to call on entry type (default: Run)")] + public string entryMethodName = "Run"; + + [Header("MonoBehaviour Support")] + [Tooltip("If true, attempts to attach generated MonoBehaviour to target GameObject")] + public bool attachAsComponent = false; + [Tooltip("Target GameObject to attach component to (if null, uses this.gameObject)")] + public GameObject targetGameObject; + + [Header("History & Tracing")] + [Tooltip("Enable automatic history tracking of compiled scripts")] + public bool enableHistory = true; + [Tooltip("Maximum number of history entries to keep")] + public int maxHistoryEntries = 20; + + // compiled assembly & method cache + private Assembly compiledAssembly; + private MethodInfo entryMethod; + private Type entryType; + private Component attachedComponent; // Track dynamically attached component + + public bool HasCompiledAssembly => compiledAssembly != null; + public bool HasEntryMethod => entryMethod != null; + public bool HasEntryType => entryType != null; + public Type EntryType => entryType; // Public accessor for editor + + // compile result diagnostics (string-friendly) + public string lastCompileDiagnostics = ""; + + // History tracking - SHARED across all instances + [System.Serializable] + public class CompilationHistoryEntry + { + public string timestamp; + public string sourceCode; + public string typeName; + public string methodName; + public bool success; + public string diagnostics; + public string executionTarget; + } + + // Static shared history + private static System.Collections.Generic.List _sharedHistory = new System.Collections.Generic.List(); + private static int _maxHistoryEntries = 50; + + public System.Collections.Generic.List CompilationHistory => _sharedHistory; + + // public wrapper so EditorWindow or other runtime UI can call compile/run + public bool CompileInMemory(out string diagnostics) + { +#if UNITY_EDITOR + diagnostics = string.Empty; + lastCompileDiagnostics = string.Empty; + + try + { + var syntaxTree = CSharpSyntaxTree.ParseText(code ?? string.Empty); + + // collect references from loaded assemblies (Editor-safe) + var refs = new List(); + + // Always include mscorlib / system.runtime + refs.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); + + // Add all currently loaded assemblies' locations that are not dynamic and have a location + var assemblies = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location)) + .Distinct(); + + foreach (var a in assemblies) + { + try + { + refs.Add(MetadataReference.CreateFromFile(a.Location)); + } + catch { } + } + + var compilation = CSharpCompilation.Create( + assemblyName: "RoslynRuntimeAssembly_" + Guid.NewGuid().ToString("N"), + syntaxTrees: new[] { syntaxTree }, + references: refs, + options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) + ); + + using (var ms = new MemoryStream()) + { + var result = compilation.Emit(ms); + if (!result.Success) + { + var diagText = string.Join("\n", result.Diagnostics.Select(d => d.ToString())); + lastCompileDiagnostics = diagText; + diagnostics = diagText; + Debug.LogError("Roslyn compile failed:\n" + diagText); + return false; + } + + ms.Seek(0, SeekOrigin.Begin); + var assemblyData = ms.ToArray(); + compiledAssembly = Assembly.Load(assemblyData); + + // find entry type + var type = compiledAssembly.GetType(entryTypeName); + if (type == null) + { + lastCompileDiagnostics = $"Type '{entryTypeName}' not found in compiled assembly."; + diagnostics = lastCompileDiagnostics; + return false; + } + + entryType = type; + + // Check if it's a MonoBehaviour + if (typeof(MonoBehaviour).IsAssignableFrom(type)) + { + lastCompileDiagnostics = $"Compilation OK. Type '{entryTypeName}' is a MonoBehaviour and can be attached as a component."; + diagnostics = lastCompileDiagnostics; + Debug.Log(diagnostics); + return true; + } + + // try various method signatures for non-MonoBehaviour types + entryMethod = type.GetMethod(entryMethodName, BindingFlags.Public | BindingFlags.Static); + if (entryMethod == null) + { + lastCompileDiagnostics = $"Static method '{entryMethodName}' not found on type '{entryTypeName}'.\n" + + $"For MonoBehaviour types, set 'attachAsComponent' to true instead."; + diagnostics = lastCompileDiagnostics; + return false; + } + + lastCompileDiagnostics = "Compilation OK."; + diagnostics = lastCompileDiagnostics; + Debug.Log("Roslyn compilation successful."); + return true; + } + } + catch (Exception ex) + { + diagnostics = ex.ToString(); + lastCompileDiagnostics = diagnostics; + Debug.LogError("Roslyn compile exception: " + diagnostics); + return false; + } +#else + diagnostics = "Roslyn compilation is only supported in the Unity Editor when referencing Roslyn assemblies."; + lastCompileDiagnostics = diagnostics; + Debug.LogError(diagnostics); + return false; +#endif + } + + public bool InvokeEntry(GameObject host, out string runtimeError) + { + runtimeError = null; + if (compiledAssembly == null || entryType == null) + { + runtimeError = "No compiled assembly / entry type. Call CompileInMemory first."; + return false; + } + + // Handle MonoBehaviour types + if (typeof(MonoBehaviour).IsAssignableFrom(entryType)) + { + return AttachMonoBehaviour(host, out runtimeError); + } + + // Handle static method invocation + if (entryMethod == null) + { + runtimeError = "No entry method found. For MonoBehaviour types, use attachAsComponent=true."; + return false; + } + + try + { + var parameters = entryMethod.GetParameters(); + if (parameters.Length == 0) + { + entryMethod.Invoke(null, null); + return true; + } + else if (parameters.Length == 1) + { + var pType = parameters[0].ParameterType; + if (pType == typeof(GameObject)) + entryMethod.Invoke(null, new object[] { host }); + else if (typeof(MonoBehaviour).IsAssignableFrom(pType)) + { + var component = host.GetComponent(pType); + entryMethod.Invoke(null, new object[] { component != null ? component : (object)host }); + } + else if (pType == typeof(Transform)) + entryMethod.Invoke(null, new object[] { host.transform }); + else if (pType == typeof(object)) + entryMethod.Invoke(null, new object[] { host }); + else + entryMethod.Invoke(null, new object[] { host }); // best effort + + return true; + } + else + { + runtimeError = "Entry method has unsupported parameter signature."; + return false; + } + } + catch (TargetInvocationException tie) + { + runtimeError = tie.InnerException?.ToString() ?? tie.ToString(); + Debug.LogError("Runtime invocation error: " + runtimeError); + return false; + } + catch (Exception ex) + { + runtimeError = ex.ToString(); + Debug.LogError("Runtime invocation error: " + runtimeError); + return false; + } + } + + /// + /// Attaches a dynamically compiled MonoBehaviour to a GameObject + /// + public bool AttachMonoBehaviour(GameObject host, out string runtimeError) + { + runtimeError = null; + + if (host == null) + { + runtimeError = "Target GameObject is null."; + return false; + } + + if (entryType == null || !typeof(MonoBehaviour).IsAssignableFrom(entryType)) + { + runtimeError = $"Type '{entryTypeName}' is not a MonoBehaviour."; + return false; + } + + try + { + // Check if component already exists + var existing = host.GetComponent(entryType); + if (existing != null) + { + Debug.LogWarning($"Component '{entryType.Name}' already exists on '{host.name}'. Removing old instance."); + if (Application.isPlaying) + Destroy(existing); + else + DestroyImmediate(existing); + } + + // Add the component + attachedComponent = host.AddComponent(entryType); + + if (attachedComponent == null) + { + runtimeError = "Failed to add component to GameObject."; + return false; + } + + Debug.Log($"Successfully attached '{entryType.Name}' to '{host.name}'"); + return true; + } + catch (Exception ex) + { + runtimeError = ex.ToString(); + Debug.LogError("Failed to attach MonoBehaviour: " + runtimeError); + return false; + } + } + + /// + /// Invokes a coroutine on the compiled type if it returns IEnumerator + /// + public bool InvokeCoroutine(MonoBehaviour host, out string runtimeError) + { + runtimeError = null; + + if (entryMethod == null) + { + runtimeError = "No entry method found."; + return false; + } + + if (!typeof(System.Collections.IEnumerator).IsAssignableFrom(entryMethod.ReturnType)) + { + runtimeError = $"Method '{entryMethodName}' does not return IEnumerator."; + return false; + } + + try + { + var parameters = entryMethod.GetParameters(); + object result = null; + + if (parameters.Length == 0) + { + result = entryMethod.Invoke(null, null); + } + else if (parameters.Length == 1) + { + var pType = parameters[0].ParameterType; + if (pType == typeof(GameObject)) + result = entryMethod.Invoke(null, new object[] { host.gameObject }); + else if (typeof(MonoBehaviour).IsAssignableFrom(pType)) + result = entryMethod.Invoke(null, new object[] { host }); + else + result = entryMethod.Invoke(null, new object[] { host }); + } + + if (result is System.Collections.IEnumerator coroutine) + { + host.StartCoroutine(coroutine); + Debug.Log($"Started coroutine '{entryMethodName}' on '{host.name}'"); + return true; + } + else + { + runtimeError = "Method did not return a valid IEnumerator."; + return false; + } + } + catch (Exception ex) + { + runtimeError = ex.ToString(); + Debug.LogError("Failed to start coroutine: " + runtimeError); + return false; + } + } + + /// + /// MCP-callable function: Compiles code and optionally attaches to a GameObject + /// + /// C# source code to compile + /// Type name to instantiate/invoke + /// Method name to invoke (for static methods) + /// Target GameObject (null = this.gameObject) + /// If true and type is MonoBehaviour, attach as component + /// Output error message if operation fails + /// True if successful, false otherwise + public bool CompileAndExecute( + string sourceCode, + string typeName, + string methodName, + GameObject targetObject, + bool shouldAttachComponent, + out string errorMessage) + { + errorMessage = null; + + // Validate inputs + if (string.IsNullOrWhiteSpace(sourceCode)) + { + errorMessage = "Source code cannot be empty."; + return false; + } + + if (string.IsNullOrWhiteSpace(typeName)) + { + errorMessage = "Type name cannot be empty."; + return false; + } + + // Set properties + code = sourceCode; + entryTypeName = typeName; + entryMethodName = string.IsNullOrWhiteSpace(methodName) ? "Run" : methodName; + attachAsComponent = shouldAttachComponent; + targetGameObject = targetObject; + + // Determine target GameObject first + GameObject target = targetGameObject != null ? targetGameObject : this.gameObject; + string targetName = target != null ? target.name : "null"; + + // Compile + if (!CompileInMemory(out string compileError)) + { + errorMessage = $"Compilation failed:\n{compileError}"; + AddHistoryEntry(sourceCode, typeName, entryMethodName, false, compileError, targetName); + return false; + } + + if (target == null) + { + errorMessage = "No target GameObject available."; + AddHistoryEntry(sourceCode, typeName, entryMethodName, false, "No target GameObject", "null"); + return false; + } + + // Execute based on type + try + { + // MonoBehaviour attachment + if (shouldAttachComponent && entryType != null && typeof(MonoBehaviour).IsAssignableFrom(entryType)) + { + if (!AttachMonoBehaviour(target, out string attachError)) + { + errorMessage = $"Failed to attach MonoBehaviour:\n{attachError}"; + AddHistoryEntry(sourceCode, typeName, entryMethodName, false, attachError, target.name); + return false; + } + + Debug.Log($"[MCP] MonoBehaviour '{typeName}' successfully attached to '{target.name}'"); + AddHistoryEntry(sourceCode, typeName, entryMethodName, true, "Component attached successfully", target.name); + return true; + } + + // Coroutine invocation + if (entryMethod != null && typeof(System.Collections.IEnumerator).IsAssignableFrom(entryMethod.ReturnType)) + { + var host = target.GetComponent() ?? this; + if (!InvokeCoroutine(host, out string coroutineError)) + { + errorMessage = $"Failed to start coroutine:\n{coroutineError}"; + AddHistoryEntry(sourceCode, typeName, entryMethodName, false, coroutineError, target.name); + return false; + } + + Debug.Log($"[MCP] Coroutine '{methodName}' started on '{target.name}'"); + AddHistoryEntry(sourceCode, typeName, entryMethodName, true, "Coroutine started successfully", target.name); + return true; + } + + // Static method invocation + if (!InvokeEntry(target, out string invokeError)) + { + errorMessage = $"Failed to invoke method:\n{invokeError}"; + AddHistoryEntry(sourceCode, typeName, entryMethodName, false, invokeError, target.name); + return false; + } + + Debug.Log($"[MCP] Method '{methodName}' executed successfully on '{target.name}'"); + AddHistoryEntry(sourceCode, typeName, entryMethodName, true, "Method executed successfully", target.name); + return true; + } + catch (Exception ex) + { + errorMessage = $"Execution error:\n{ex.Message}\n{ex.StackTrace}"; + return false; + } + } + + /// + /// Simplified MCP-callable function with default parameters + /// + public bool CompileAndExecute(string sourceCode, string typeName, GameObject targetObject, out string errorMessage) + { + // Auto-detect if it's a MonoBehaviour by checking the source + bool shouldAttach = sourceCode.Contains(": MonoBehaviour") || sourceCode.Contains(":MonoBehaviour"); + return CompileAndExecute(sourceCode, typeName, "Run", targetObject, shouldAttach, out errorMessage); + } + + /// + /// MCP-callable: Compile and attach to current GameObject + /// + public bool CompileAndAttachToSelf(string sourceCode, string typeName, out string errorMessage) + { + return CompileAndExecute(sourceCode, typeName, "Run", this.gameObject, true, out errorMessage); + } + + // helper: convenience method to compile + run on this.gameObject + public void CompileAndRunOnSelf() + { + if (CompileInMemory(out var diag)) + { + if (!Application.isPlaying) + Debug.LogWarning("Running compiled code in Edit Mode. Some UnityEngine APIs may not behave as expected."); + + GameObject target = targetGameObject != null ? targetGameObject : this.gameObject; + + // Check if we should attach as component + if (attachAsComponent && entryType != null && typeof(MonoBehaviour).IsAssignableFrom(entryType)) + { + if (AttachMonoBehaviour(target, out var attachErr)) + { + Debug.Log($"MonoBehaviour '{entryTypeName}' attached successfully to '{target.name}'."); + } + else + { + Debug.LogError("Failed to attach MonoBehaviour: " + attachErr); + } + } + // Check if it's a coroutine + else if (entryMethod != null && typeof(System.Collections.IEnumerator).IsAssignableFrom(entryMethod.ReturnType)) + { + var host = target.GetComponent() ?? this; + if (InvokeCoroutine(host, out var coroutineErr)) + { + Debug.Log("Coroutine started successfully."); + } + else + { + Debug.LogError("Failed to start coroutine: " + coroutineErr); + } + } + // Regular static method invocation + else if (InvokeEntry(target, out var runtimeErr)) + { + Debug.Log("Entry invoked successfully."); + } + else + { + Debug.LogError("Failed to invoke entry: " + runtimeErr); + } + } + else + { + Debug.LogError("Compile failed: " + lastCompileDiagnostics); + } + } + + /// + /// Adds an entry to the compilation history + /// + private void AddHistoryEntry(string sourceCode, string typeName, string methodName, bool success, string diagnostics, string target) + { + if (!enableHistory) return; + + var entry = new CompilationHistoryEntry + { + timestamp = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), + sourceCode = sourceCode, + typeName = typeName, + methodName = methodName, + success = success, + diagnostics = diagnostics, + executionTarget = target + }; + + _sharedHistory.Add(entry); + + // Trim if exceeded max + while (_sharedHistory.Count > _maxHistoryEntries) + { + _sharedHistory.RemoveAt(0); + } + } + + /// + /// Saves the compilation history to a JSON file outside Assets + /// + public bool SaveHistoryToFile(out string savedPath, out string error) + { + error = ""; + savedPath = ""; + + try + { + string projectRoot = Application.dataPath.Replace("/Assets", "").Replace("\\Assets", ""); + string historyDir = System.IO.Path.Combine(projectRoot, "RoslynHistory"); + + if (!System.IO.Directory.Exists(historyDir)) + { + System.IO.Directory.CreateDirectory(historyDir); + } + + string timestamp = System.DateTime.Now.ToString("yyyyMMdd_HHmmss"); + string filename = $"RoslynHistory_{timestamp}.json"; + savedPath = System.IO.Path.Combine(historyDir, filename); + + string json = JsonUtility.ToJson(new HistoryWrapper { entries = _sharedHistory }, true); + System.IO.File.WriteAllText(savedPath, json); + + Debug.Log($"[RuntimeRoslynDemo] Saved {_sharedHistory.Count} history entries to: {savedPath}"); + return true; + } + catch (System.Exception ex) + { + error = ex.Message; + Debug.LogError($"[RuntimeRoslynDemo] Failed to save history: {error}"); + return false; + } + } + + /// + /// Saves a specific history entry as a standalone .cs file outside Assets + /// + public bool SaveHistoryEntryAsScript(int index, out string savedPath, out string error) + { + error = ""; + savedPath = ""; + + if (index < 0 || index >= _sharedHistory.Count) + { + error = "Invalid history index"; + return false; + } + + try + { + var entry = _sharedHistory[index]; + string projectRoot = Application.dataPath.Replace("/Assets", "").Replace("\\Assets", ""); + string scriptsDir = System.IO.Path.Combine(projectRoot, "RoslynHistory", "Scripts"); + + if (!System.IO.Directory.Exists(scriptsDir)) + { + System.IO.Directory.CreateDirectory(scriptsDir); + } + + string timestamp = System.DateTime.Parse(entry.timestamp).ToString("yyyyMMdd_HHmmss"); + string filename = $"{entry.typeName}_{timestamp}.cs"; + savedPath = System.IO.Path.Combine(scriptsDir, filename); + + // Add header comment + string header = $"// Roslyn Runtime Compiled Script\n// Original Timestamp: {entry.timestamp}\n// Type: {entry.typeName}\n// Method: {entry.methodName}\n// Success: {entry.success}\n// Target: {entry.executionTarget}\n\n"; + + System.IO.File.WriteAllText(savedPath, header + entry.sourceCode); + + Debug.Log($"[RuntimeRoslynDemo] Saved script to: {savedPath}"); + return true; + } + catch (System.Exception ex) + { + error = ex.Message; + Debug.LogError($"[RuntimeRoslynDemo] Failed to save script: {error}"); + return false; + } + } + + /// + /// Clears the compilation history + /// + public void ClearHistory() + { + _sharedHistory.Clear(); + Debug.Log("[RuntimeRoslynDemo] Compilation history cleared"); + } + + [System.Serializable] + private class HistoryWrapper + { + public System.Collections.Generic.List entries; + } +} + +/// +/// Static helper class for MCP tools to compile and execute C# code at runtime +/// +public static class RoslynMCPHelper +{ + private static RoslynRuntimeCompiler _compiler; + + /// + /// Get or create the runtime compiler instance + /// + private static RoslynRuntimeCompiler GetOrCreateCompiler() + { + if (_compiler == null || _compiler.gameObject == null) + { + var existing = UnityEngine.Object.FindFirstObjectByType(); + if (existing != null) + { + _compiler = existing; + } + else + { + var go = new GameObject("MCPRoslynCompiler"); + _compiler = go.AddComponent(); + if (!Application.isPlaying) + { + go.hideFlags = HideFlags.HideAndDontSave; + } + } + } + return _compiler; + } + + /// + /// MCP Entry Point: Compile C# code and attach to a GameObject + /// + /// Complete C# source code + /// Name of the class to instantiate + /// Name of GameObject to attach to (null = create new) + /// Output result message + /// True if successful + public static bool CompileAndAttach(string sourceCode, string className, string targetGameObjectName, out string result) + { + try + { + var compiler = GetOrCreateCompiler(); + + // Find or create target GameObject + GameObject target = null; + if (!string.IsNullOrEmpty(targetGameObjectName)) + { + target = GameObject.Find(targetGameObjectName); + if (target == null) + { + result = $"GameObject '{targetGameObjectName}' not found."; + return false; + } + } + else + { + // Create a new GameObject for the script + target = new GameObject($"Generated_{className}"); + UnityEngine.Debug.Log($"[MCP] Created new GameObject: {target.name}"); + } + + // Compile and execute + bool success = compiler.CompileAndExecute(sourceCode, className, target, out string error); + + if (success) + { + result = $"Successfully compiled and attached '{className}' to '{target.name}'"; + UnityEngine.Debug.Log($"[MCP] {result}"); + return true; + } + else + { + result = $"Failed: {error}"; + UnityEngine.Debug.LogError($"[MCP] {result}"); + return false; + } + } + catch (Exception ex) + { + result = $"Exception: {ex.Message}"; + UnityEngine.Debug.LogError($"[MCP] {result}\n{ex.StackTrace}"); + return false; + } + } + + /// + /// MCP Entry Point: Compile and execute static method + /// + /// Complete C# source code + /// Name of the class containing the method + /// Name of the static method to invoke + /// GameObject to pass as parameter (optional) + /// Output result message + /// True if successful + public static bool CompileAndExecuteStatic(string sourceCode, string className, string methodName, string targetGameObjectName, out string result) + { + try + { + var compiler = GetOrCreateCompiler(); + + GameObject target = compiler.gameObject; + if (!string.IsNullOrEmpty(targetGameObjectName)) + { + var found = GameObject.Find(targetGameObjectName); + if (found != null) + { + target = found; + } + } + + bool success = compiler.CompileAndExecute(sourceCode, className, methodName, target, false, out string error); + + if (success) + { + result = $"Successfully compiled and executed '{className}.{methodName}'"; + UnityEngine.Debug.Log($"[MCP] {result}"); + return true; + } + else + { + result = $"Failed: {error}"; + UnityEngine.Debug.LogError($"[MCP] {result}"); + return false; + } + } + catch (Exception ex) + { + result = $"Exception: {ex.Message}"; + UnityEngine.Debug.LogError($"[MCP] {result}\n{ex.StackTrace}"); + return false; + } + } + + /// + /// MCP Entry Point: Quick compile and attach MonoBehaviour + /// + /// MonoBehaviour source code + /// MonoBehaviour class name + /// Target GameObject name (creates if null) + /// Success status message + public static string QuickAttachScript(string sourceCode, string className, string gameObjectName = null) + { + bool success = CompileAndAttach(sourceCode, className, gameObjectName, out string result); + return result; + } + + /// + /// MCP Entry Point: Execute code snippet with minimal parameters + /// + public static string ExecuteCode(string sourceCode, string className = "AIGenerated") + { + bool success = CompileAndExecuteStatic(sourceCode, className, "Run", null, out string result); + return result; + } +} + +#if UNITY_EDITOR +// Editor window +public class RoslynRuntimeCompilerWindow : EditorWindow +{ + private RoslynRuntimeCompiler helperInScene; + private Vector2 scrollPos; + private Vector2 diagScroll; + private Vector2 historyScroll; + private int selectedTab = 0; + private string[] tabNames = { "Compiler", "History" }; + private int selectedHistoryIndex = -1; + private Vector2 historyCodeScroll; + + // Editor UI state + private string codeText = string.Empty; + private string typeName = "AIGenerated"; + private string methodName = "Run"; + private bool attachAsComponent = false; + private GameObject targetGameObject = null; + + [MenuItem("Window/Roslyn Runtime Compiler")] + public static void ShowWindow() + { + var w = GetWindow("Roslyn Runtime Compiler"); + w.minSize = new Vector2(600, 400); + } + + void OnEnable() + { + // try to find an existing helper in scene + helperInScene = FindFirstObjectByType(FindObjectsInactive.Include); + if (helperInScene == null) + { + var go = new GameObject("RoslynRuntimeHelper"); + helperInScene = go.AddComponent(); + // Don't save this helper into scene assets + go.hideFlags = HideFlags.HideAndDontSave; + } + + if (helperInScene != null) + { + codeText = helperInScene.code; + typeName = helperInScene.entryTypeName; + methodName = helperInScene.entryMethodName; + attachAsComponent = helperInScene.attachAsComponent; + targetGameObject = helperInScene.targetGameObject; + } + } + + void OnDisable() + { + // keep editor text back to helper if it still exists + if (helperInScene != null && helperInScene.gameObject != null) + { + helperInScene.code = codeText; + helperInScene.entryTypeName = typeName; + helperInScene.entryMethodName = methodName; + helperInScene.attachAsComponent = attachAsComponent; + helperInScene.targetGameObject = targetGameObject; + } + } + + void OnDestroy() + { + // Clean up helper object when window is destroyed + if (helperInScene != null && helperInScene.gameObject != null) + { + DestroyImmediate(helperInScene.gameObject); + helperInScene = null; + } + } + + void OnGUI() + { + // Ensure helper exists before drawing GUI - recreate if needed + if (helperInScene == null || helperInScene.gameObject == null) + { + // Try to find existing helper first + helperInScene = FindFirstObjectByType(FindObjectsInactive.Include); + + // If still not found, create a new one + if (helperInScene == null) + { + var go = new GameObject("RoslynRuntimeHelper"); + helperInScene = go.AddComponent(); + go.hideFlags = HideFlags.HideAndDontSave; + + // Initialize with default values + helperInScene.code = codeText; + helperInScene.entryTypeName = typeName; + helperInScene.entryMethodName = methodName; + helperInScene.attachAsComponent = attachAsComponent; + helperInScene.targetGameObject = targetGameObject; + } + else + { + // Load state from found helper + codeText = helperInScene.code; + typeName = helperInScene.entryTypeName; + methodName = helperInScene.entryMethodName; + attachAsComponent = helperInScene.attachAsComponent; + targetGameObject = helperInScene.targetGameObject; + } + } + + EditorGUILayout.LabelField("Roslyn Runtime Compiler (Editor)", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + // Tab selector + selectedTab = GUILayout.Toolbar(selectedTab, tabNames); + EditorGUILayout.Space(); + + if (selectedTab == 0) + { + DrawCompilerTab(); + } + else if (selectedTab == 1) + { + DrawHistoryTab(); + } + } + + void DrawCompilerTab() + { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Entry Type:", GUILayout.Width(70)); + typeName = EditorGUILayout.TextField(typeName); + EditorGUILayout.LabelField("Method:", GUILayout.Width(50)); + methodName = EditorGUILayout.TextField(methodName, GUILayout.Width(120)); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.BeginHorizontal(); + attachAsComponent = EditorGUILayout.Toggle("Attach as Component", attachAsComponent, GUILayout.Width(200)); + if (attachAsComponent) + { + EditorGUILayout.LabelField("Target:", GUILayout.Width(45)); + targetGameObject = (GameObject)EditorGUILayout.ObjectField(targetGameObject, typeof(GameObject), true); + } + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + EditorGUILayout.LabelField("Code (paste LLM output here):"); + scrollPos = EditorGUILayout.BeginScrollView(scrollPos, GUILayout.Height(position.height * 0.55f)); + codeText = EditorGUILayout.TextArea(codeText, GUILayout.ExpandHeight(true)); + EditorGUILayout.EndScrollView(); + + EditorGUILayout.Space(); + + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Compile")) + { + ApplyToHelper(); + if (helperInScene != null) + { + var ok = helperInScene.CompileInMemory(out var diag); + Debug.Log(ok ? "Compile OK" : "Compile Failed\n" + diag); + } + } + + bool canRun = helperInScene != null && helperInScene.HasCompiledAssembly && + (helperInScene.HasEntryMethod || (helperInScene.HasEntryType && typeof(MonoBehaviour).IsAssignableFrom(helperInScene.EntryType))); + GUI.enabled = canRun; + if (GUILayout.Button("Run (invoke on selected)")) + { + ApplyToHelper(); + var sel = Selection.activeGameObject; + if (sel == null && helperInScene != null && helperInScene.gameObject != null) + sel = helperInScene.gameObject; + + if (sel != null && helperInScene != null) + { + if (helperInScene.InvokeEntry(sel, out var runtimeErr)) + Debug.Log("Invocation OK on: " + sel.name); + else + Debug.LogError("Invocation failed: " + runtimeErr); + } + } + + GUI.enabled = true; + if (GUILayout.Button("Compile & Run on helper")) + { + ApplyToHelper(); + if (helperInScene != null) + { + helperInScene.CompileAndRunOnSelf(); + } + } + + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Diagnostics:"); + diagScroll = EditorGUILayout.BeginScrollView(diagScroll, GUILayout.Height(120)); + string diagnosticsText = (helperInScene != null && helperInScene.lastCompileDiagnostics != null) + ? helperInScene.lastCompileDiagnostics + : "No diagnostics available."; + EditorGUILayout.HelpBox(diagnosticsText, MessageType.Info); + EditorGUILayout.EndScrollView(); + + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Notes:"); + EditorGUILayout.HelpBox("This compiles code in-memory using Roslyn. Do not write .cs files into Assets while running. Generated code runs with editor permissions.\n\n" + + "Supported patterns:\n" + + "1. Static method: public static void Run(GameObject host)\n" + + "2. MonoBehaviour: Enable 'Attach as Component' for classes inheriting MonoBehaviour\n" + + "3. Coroutine: public static IEnumerator RunCoroutine(MonoBehaviour host)\n" + + "4. Parameterless: public static void Run()", MessageType.None); + } + + void DrawHistoryTab() + { + if (helperInScene == null) return; + + var history = helperInScene.CompilationHistory; + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField($"Compilation History ({history.Count} entries)", EditorStyles.boldLabel); + + if (GUILayout.Button("Save History JSON", GUILayout.Width(140))) + { + if (helperInScene.SaveHistoryToFile(out string path, out string error)) + { + EditorUtility.DisplayDialog("Success", $"History saved to:\n{path}", "OK"); + } + else + { + EditorUtility.DisplayDialog("Error", $"Failed to save history:\n{error}", "OK"); + } + } + + if (GUILayout.Button("Clear History", GUILayout.Width(100))) + { + if (EditorUtility.DisplayDialog("Clear History", "Are you sure you want to clear all compilation history?", "Yes", "No")) + { + helperInScene.ClearHistory(); + selectedHistoryIndex = -1; + } + } + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + if (history.Count == 0) + { + EditorGUILayout.HelpBox("No compilation history yet. Compile and run scripts to see them here.", MessageType.Info); + return; + } + + EditorGUILayout.BeginHorizontal(); + + // Left panel - history list + EditorGUILayout.BeginVertical(GUILayout.Width(position.width * 0.4f)); + EditorGUILayout.LabelField("History Entries:", EditorStyles.boldLabel); + historyScroll = EditorGUILayout.BeginScrollView(historyScroll); + + for (int i = history.Count - 1; i >= 0; i--) // Reverse order (newest first) + { + var entry = history[i]; + GUIStyle entryStyle = new GUIStyle(GUI.skin.button); + entryStyle.alignment = TextAnchor.MiddleLeft; + entryStyle.normal.textColor = entry.success ? Color.green : Color.red; + + if (selectedHistoryIndex == i) + { + entryStyle.normal.background = Texture2D.grayTexture; + } + + string label = $"[{i}] {entry.timestamp} - {entry.typeName}.{entry.methodName}"; + if (entry.success) + label += " ✓"; + else + label += " ✗"; + + if (GUILayout.Button(label, entryStyle, GUILayout.Height(30))) + { + selectedHistoryIndex = i; + } + } + + EditorGUILayout.EndScrollView(); + EditorGUILayout.EndVertical(); + + // Right panel - selected entry details + EditorGUILayout.BeginVertical(); + + if (selectedHistoryIndex >= 0 && selectedHistoryIndex < history.Count) + { + var entry = history[selectedHistoryIndex]; + + EditorGUILayout.LabelField("Entry Details:", EditorStyles.boldLabel); + EditorGUILayout.LabelField("Timestamp:", entry.timestamp); + EditorGUILayout.LabelField("Type:", entry.typeName); + EditorGUILayout.LabelField("Method:", entry.methodName); + EditorGUILayout.LabelField("Target:", entry.executionTarget); + EditorGUILayout.LabelField("Success:", entry.success ? "Yes" : "No"); + + EditorGUILayout.Space(); + + if (!string.IsNullOrEmpty(entry.diagnostics)) + { + EditorGUILayout.LabelField("Diagnostics:"); + EditorGUILayout.HelpBox(entry.diagnostics, entry.success ? MessageType.Info : MessageType.Error); + } + + EditorGUILayout.Space(); + + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Load to Compiler", GUILayout.Height(25))) + { + codeText = entry.sourceCode; + typeName = entry.typeName; + methodName = entry.methodName; + selectedTab = 0; // Switch to compiler tab + } + + if (GUILayout.Button("Save as .cs File", GUILayout.Height(25))) + { + if (helperInScene.SaveHistoryEntryAsScript(selectedHistoryIndex, out string path, out string error)) + { + EditorUtility.DisplayDialog("Success", $"Script saved to:\n{path}", "OK"); + System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path.Replace("/", "\\")}\""); + } + else + { + EditorUtility.DisplayDialog("Error", $"Failed to save script:\n{error}", "OK"); + } + } + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + EditorGUILayout.LabelField("Source Code:"); + historyCodeScroll = EditorGUILayout.BeginScrollView(historyCodeScroll, GUILayout.ExpandHeight(true)); + EditorGUILayout.TextArea(entry.sourceCode, GUILayout.ExpandHeight(true)); + EditorGUILayout.EndScrollView(); + } + else + { + EditorGUILayout.HelpBox("Select a history entry to view details.", MessageType.Info); + } + + EditorGUILayout.EndVertical(); + + EditorGUILayout.EndHorizontal(); + } + + void ApplyToHelper() + { + if (helperInScene == null || helperInScene.gameObject == null) + { + Debug.LogError("Helper object is missing or destroyed. Cannot apply settings."); + return; + } + + helperInScene.code = codeText; + helperInScene.entryTypeName = typeName; + helperInScene.entryMethodName = methodName; + helperInScene.attachAsComponent = attachAsComponent; + helperInScene.targetGameObject = targetGameObject; + } +} +#endif diff --git a/CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs.meta b/CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs.meta new file mode 100644 index 000000000..066adb587 --- /dev/null +++ b/CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 97f1198c66ce56043a3c8a5e05ba0150 \ No newline at end of file diff --git a/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py b/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py new file mode 100644 index 000000000..488504145 --- /dev/null +++ b/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py @@ -0,0 +1,276 @@ +""" +Runtime compilation tool for MCP Unity. +Compiles and loads C# code at runtime without domain reload. +""" + +from typing import Annotated, Any +from fastmcp import Context +from registry import mcp_for_unity_tool +from unity_connection import send_command_with_retry + + +async def safe_info(ctx: Context, message: str) -> None: + """Safely send info messages when a request context is available.""" + try: + if ctx and hasattr(ctx, "info"): + await ctx.info(message) + except RuntimeError as ex: + # FastMCP raises this when called outside of an active request + if "outside of a request" not in str(ex): + raise + + +def handle_unity_command(command_name: str, params: dict) -> dict[str, Any]: + """ + Wrapper for Unity commands with better error handling. + """ + try: + response = send_command_with_retry(command_name, params) + return response if isinstance(response, dict) else {"success": False, "message": str(response)} + except Exception as e: + error_msg = str(e) + if "Context is not available" in error_msg or "not available outside of a request" in error_msg: + return { + "success": False, + "message": "Unity is not connected. Please ensure Unity Editor is running and MCP bridge is active.", + "error": "connection_error", + "details": "This tool requires an active connection to Unity. Make sure the Unity project is open and the MCP bridge is initialized." + } + return { + "success": False, + "message": f"Command failed: {error_msg}", + "error": "tool_error" + } + + +@mcp_for_unity_tool( + description="Compile and load C# code at runtime without domain reload. Creates dynamic assemblies that can be attached to GameObjects during Play Mode. Requires Roslyn (Microsoft.CodeAnalysis.CSharp) to be installed in Unity." +) +async def compile_runtime_code( + ctx: Context, + code: Annotated[str, "Complete C# code including using statements, namespace, and class definition"], + assembly_name: Annotated[str, "Unique name for the dynamic assembly. If not provided, a timestamp-based name will be generated."] | None = None, + attach_to_gameobject: Annotated[str, "Name or hierarchy path of GameObject to attach the compiled script to (e.g., 'Player' or 'Canvas/Panel')"] | None = None, + load_immediately: Annotated[bool, "Whether to load the assembly immediately after compilation. Default is true."] = True +) -> dict[str, Any]: + """ + Compile C# code at runtime and optionally attach it to a GameObject. Only enable it with Roslyn installed in Unity. + + REQUIREMENTS: + - Unity must be running and connected + - Roslyn (Microsoft.CodeAnalysis.CSharp) must be installed via NuGet + - USE_ROSLYN scripting define symbol must be set + + This tool allows you to: + - Compile new C# scripts without restarting Unity + - Load compiled assemblies into the running Unity instance + - Attach MonoBehaviour scripts to GameObjects dynamically + - Preserve game state during script additions + + Example code: + ```csharp + using UnityEngine; + + namespace DynamicScripts + { + public class MyDynamicBehavior : MonoBehaviour + { + void Start() + { + Debug.Log("Dynamic script loaded!"); + } + } + } + ``` + """ + #await safe_info(ctx, f"Compiling runtime code for assembly: {assembly_name or 'auto-generated'}") + await ctx.info(f"Compiling runtime code for assembly: {assembly_name or 'auto-generated'}") + + params = { + "action": "compile_and_load", + "code": code, + "assembly_name": assembly_name, + "attach_to": attach_to_gameobject, + "load_immediately": load_immediately, + } + params = {k: v for k, v in params.items() if v is not None} + + return handle_unity_command("runtime_compilation", params) + + +@mcp_for_unity_tool( + description="List all dynamically loaded assemblies in the current Unity session" +) +async def list_loaded_assemblies( + ctx: Context, +) -> dict[str, Any]: + """ + Get a list of all dynamically loaded assemblies created during this session. + + Returns information about: + - Assembly names + - Number of types in each assembly + - Load timestamps + - DLL file paths + """ + #await safe_info(ctx, "Retrieving loaded dynamic assemblies...") + await ctx.info("Retrieving loaded dynamic assemblies...") + + params = {"action": "list_loaded"} + return handle_unity_command("runtime_compilation", params) + + +@mcp_for_unity_tool( + description="Get all types (classes) from a dynamically loaded assembly" +) +async def get_assembly_types( + ctx: Context, + assembly_name: Annotated[str, "Name of the assembly to query"], +) -> dict[str, Any]: + """ + Retrieve all types defined in a specific dynamic assembly. + + This is useful for: + - Inspecting what was compiled + - Finding MonoBehaviour classes to attach + - Debugging compilation results + """ + #await safe_info(ctx, f"Getting types from assembly: {assembly_name}") + await ctx.info(f"Getting types from assembly: {assembly_name}") + + params = {"action": "get_types", "assembly_name": assembly_name} + return handle_unity_command("runtime_compilation", params) + + +@mcp_for_unity_tool( + description="Execute C# code using the RoslynRuntimeCompiler with full GUI tool features including history tracking, MonoBehaviour support, and coroutines" +) +async def execute_with_roslyn( + ctx: Context, + code: Annotated[str, "Complete C# source code to compile and execute"], + class_name: Annotated[str, "Name of the class to instantiate/invoke (default: AIGenerated)"] = "AIGenerated", + method_name: Annotated[str, "Name of the static method to call (default: Run)"] = "Run", + target_object: Annotated[str, "Name or path of target GameObject (optional)"] | None = None, + attach_as_component: Annotated[bool, "If true and type is MonoBehaviour, attach as component (default: false)"] = False, +) -> dict[str, Any]: + """ + Execute C# code using Unity's RoslynRuntimeCompiler tool with advanced features: + + - MonoBehaviour attachment: Set attach_as_component=true for classes inheriting MonoBehaviour + - Static method execution: Call public static methods (e.g., public static void Run(GameObject host)) + - Coroutine support: Methods returning IEnumerator will be started as coroutines + - History tracking: All compilations are tracked in history for later review + + Supported method signatures: + - public static void Run() + - public static void Run(GameObject host) + - public static void Run(MonoBehaviour host) + - public static IEnumerator RunCoroutine(MonoBehaviour host) + + Example MonoBehaviour: + ```csharp + using UnityEngine; + public class Rotator : MonoBehaviour { + void Update() { + transform.Rotate(Vector3.up * 30f * Time.deltaTime); + } + } + ``` + + Example Static Method: + ```csharp + using UnityEngine; + public class AIGenerated { + public static void Run(GameObject host) { + Debug.Log($"Hello from {host.name}!"); + } + } + ``` + """ + #await safe_info(ctx, f"Executing code with RoslynRuntimeCompiler: {class_name}.{method_name}") + await ctx.info(f"Executing code with RoslynRuntimeCompiler: {class_name}.{method_name}") + + params = { + "action": "execute_with_roslyn", + "code": code, + "class_name": class_name, + "method_name": method_name, + "target_object": target_object, + "attach_as_component": attach_as_component, + } + params = {k: v for k, v in params.items() if v is not None} + + return handle_unity_command("runtime_compilation", params) + + +@mcp_for_unity_tool( + description="Get the compilation history from RoslynRuntimeCompiler showing all previous compilations and executions" +) +async def get_compilation_history( + ctx: Context, +) -> dict[str, Any]: + """ + Retrieve the compilation history from the RoslynRuntimeCompiler. + + History includes: + - Timestamp of each compilation + - Class and method names + - Success/failure status + - Compilation diagnostics + - Target GameObject names + - Source code previews + + This is useful for: + - Reviewing what code has been compiled + - Debugging failed compilations + - Tracking execution flow + - Auditing dynamic code changes + """ + #await safe_info(ctx, "Retrieving compilation history...") + await ctx.info("Retrieving compilation history...") + + params = {"action": "get_history"} + return handle_unity_command("runtime_compilation", params) + + +@mcp_for_unity_tool( + description="Save the compilation history to a JSON file outside the Assets folder" +) +async def save_compilation_history( + ctx: Context, +) -> dict[str, Any]: + """ + Save all compilation history to a timestamped JSON file. + + The file is saved to: ProjectRoot/RoslynHistory/RoslynHistory_TIMESTAMP.json + + This allows you to: + - Keep a permanent record of dynamic compilations + - Review history after Unity restarts + - Share compilation sessions with team members + - Archive successful code patterns + """ + #await safe_info(ctx, "Saving compilation history to file...") + await ctx.info("Saving compilation history to file...") + + params = {"action": "save_history"} + return handle_unity_command("runtime_compilation", params) + + +@mcp_for_unity_tool( + description="Clear all compilation history from RoslynRuntimeCompiler" +) +async def clear_compilation_history( + ctx: Context, +) -> dict[str, Any]: + """ + Clear all compilation history entries. + + This removes all tracked compilations from memory but does not delete + saved history files. Use this to start fresh or reduce memory usage. + """ + #await safe_info(ctx, "Clearing compilation history...") + await ctx.info("Clearing compilation history...") + + params = {"action": "clear_history"} + return handle_unity_command("runtime_compilation", params) diff --git a/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py.meta b/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py.meta new file mode 100644 index 000000000..5b00cfbfa --- /dev/null +++ b/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 3934c3a018e9eb540a1b39056c193f71 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 11500000, guid: d68ef794590944f1ea7ee102c91887c7, type: 3} From 880593335d3bed1af5886ae1852f62ba4bb7bbd9 Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Mon, 10 Nov 2025 00:24:21 -0500 Subject: [PATCH 4/8] Fix based on CR --- .../ManageRuntimeCompilation.cs | 35 +++++++++++++++---- .../RoslynRuntimeCompiler.cs | 5 ++- .../runtime_compilation_tool.py | 21 ++++------- 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs b/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs index 401c7530d..c5b6daab2 100644 --- a/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs +++ b/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs @@ -84,7 +84,10 @@ private static object CompileAndLoad(JObject @params) try { string code = @params["code"]?.ToString(); - string assemblyName = @params["assembly_name"]?.ToString() ?? $"DynamicAssembly_{DateTime.Now.Ticks}"; + var assemblyToken = @params["assembly_name"]; + string assemblyName = assemblyToken == null || string.IsNullOrWhiteSpace(assemblyToken.ToString()) + ? $"DynamicAssembly_{DateTime.Now.Ticks}" + : assemblyToken.ToString().Trim(); string attachTo = @params["attach_to"]?.ToString(); bool loadImmediately = @params["load_immediately"]?.ToObject() ?? true; @@ -101,8 +104,21 @@ private static object CompileAndLoad(JObject @params) // Create output directory Directory.CreateDirectory(DynamicAssembliesPath); - string dllPath = Path.Combine(DynamicAssembliesPath, $"{assemblyName}.dll"); - + string basePath = Path.GetFullPath(DynamicAssembliesPath); + Directory.CreateDirectory(basePath); + string safeFileName = SanitizeAssemblyFileName(assemblyName); + string dllPath = Path.GetFullPath(Path.Combine(basePath, $"{safeFileName}.dll")); + + if (!dllPath.StartsWith(basePath, StringComparison.Ordinal)) + { + return Response.Error("Assembly name must resolve inside the dynamic assemblies directory."); + } + + if (File.Exists(dllPath)) + { + dllPath = Path.GetFullPath(Path.Combine(basePath, $"{safeFileName}_{DateTime.Now.Ticks}.dll")); + } + // Parse code var syntaxTree = CSharpSyntaxTree.ParseText(code); @@ -121,7 +137,7 @@ private static object CompileAndLoad(JObject @params) // Emit to file EmitResult emitResult; - using (var stream = new FileStream(dllPath, FileMode.Create)) + using (var stream = new FileStream(dllPath, FileMode.Create, FileAccess.Write, FileShare.None)) { emitResult = compilation.Emit(stream); } @@ -227,7 +243,7 @@ private static object CompileAndLoad(JObject @params) } #endif } - + private static object ListLoadedAssemblies() { var assemblies = LoadedAssemblies.Values.Select(info => new @@ -238,7 +254,7 @@ private static object ListLoadedAssemblies() type_count = info.TypeNames.Count, types = info.TypeNames }).ToList(); - + return Response.Success($"Found {assemblies.Count} loaded dynamic assemblies", new { count = assemblies.Count, @@ -246,6 +262,13 @@ private static object ListLoadedAssemblies() }); } + private static string SanitizeAssemblyFileName(string assemblyName) + { + var invalidChars = Path.GetInvalidFileNameChars(); + var sanitized = new string(assemblyName.Where(c => !invalidChars.Contains(c)).ToArray()); + return string.IsNullOrWhiteSpace(sanitized) ? $"DynamicAssembly_{DateTime.Now.Ticks}" : sanitized; + } + private static object GetAssemblyTypes(JObject @params) { string assemblyName = @params["assembly_name"]?.ToString(); diff --git a/CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs b/CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs index 79a26dcde..734e94892 100644 --- a/CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs +++ b/CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs @@ -93,7 +93,6 @@ public class CompilationHistoryEntry // Static shared history private static System.Collections.Generic.List _sharedHistory = new System.Collections.Generic.List(); - private static int _maxHistoryEntries = 50; public System.Collections.Generic.List CompilationHistory => _sharedHistory; @@ -584,7 +583,7 @@ private void AddHistoryEntry(string sourceCode, string typeName, string methodNa _sharedHistory.Add(entry); // Trim if exceeded max - while (_sharedHistory.Count > _maxHistoryEntries) + while (_sharedHistory.Count > maxHistoryEntries) { _sharedHistory.RemoveAt(0); } @@ -1167,7 +1166,7 @@ void DrawHistoryTab() if (helperInScene.SaveHistoryEntryAsScript(selectedHistoryIndex, out string path, out string error)) { EditorUtility.DisplayDialog("Success", $"Script saved to:\n{path}", "OK"); - System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path.Replace("/", "\\")}\""); + EditorUtility.RevealInFinder(path); } else { diff --git a/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py b/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py index 488504145..977b9f717 100644 --- a/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py +++ b/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py @@ -83,8 +83,7 @@ async def compile_runtime_code( } ``` """ - #await safe_info(ctx, f"Compiling runtime code for assembly: {assembly_name or 'auto-generated'}") - await ctx.info(f"Compiling runtime code for assembly: {assembly_name or 'auto-generated'}") + await safe_info(ctx, f"Compiling runtime code for assembly: {assembly_name or 'auto-generated'}") params = { "action": "compile_and_load", @@ -113,8 +112,7 @@ async def list_loaded_assemblies( - Load timestamps - DLL file paths """ - #await safe_info(ctx, "Retrieving loaded dynamic assemblies...") - await ctx.info("Retrieving loaded dynamic assemblies...") + await safe_info(ctx, "Retrieving loaded dynamic assemblies...") params = {"action": "list_loaded"} return handle_unity_command("runtime_compilation", params) @@ -135,8 +133,7 @@ async def get_assembly_types( - Finding MonoBehaviour classes to attach - Debugging compilation results """ - #await safe_info(ctx, f"Getting types from assembly: {assembly_name}") - await ctx.info(f"Getting types from assembly: {assembly_name}") + await safe_info(ctx, f"Getting types from assembly: {assembly_name}") params = {"action": "get_types", "assembly_name": assembly_name} return handle_unity_command("runtime_compilation", params) @@ -187,8 +184,7 @@ async def execute_with_roslyn( } ``` """ - #await safe_info(ctx, f"Executing code with RoslynRuntimeCompiler: {class_name}.{method_name}") - await ctx.info(f"Executing code with RoslynRuntimeCompiler: {class_name}.{method_name}") + await safe_info(ctx, f"Executing code with RoslynRuntimeCompiler: {class_name}.{method_name}") params = { "action": "execute_with_roslyn", @@ -226,8 +222,7 @@ async def get_compilation_history( - Tracking execution flow - Auditing dynamic code changes """ - #await safe_info(ctx, "Retrieving compilation history...") - await ctx.info("Retrieving compilation history...") + await safe_info(ctx, "Retrieving compilation history...") params = {"action": "get_history"} return handle_unity_command("runtime_compilation", params) @@ -250,8 +245,7 @@ async def save_compilation_history( - Share compilation sessions with team members - Archive successful code patterns """ - #await safe_info(ctx, "Saving compilation history to file...") - await ctx.info("Saving compilation history to file...") + await safe_info(ctx, "Saving compilation history to file...") params = {"action": "save_history"} return handle_unity_command("runtime_compilation", params) @@ -269,8 +263,7 @@ async def clear_compilation_history( This removes all tracked compilations from memory but does not delete saved history files. Use this to start fresh or reduce memory usage. """ - #await safe_info(ctx, "Clearing compilation history...") - await ctx.info("Clearing compilation history...") + await safe_info(ctx, "Clearing compilation history...") params = {"action": "clear_history"} return handle_unity_command("runtime_compilation", params) From 711e8bd4497fe09579295d84585217a9e44927bb Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Wed, 12 Nov 2025 21:43:06 -0500 Subject: [PATCH 5/8] Create claude_skill_unity.zip Upload the unity_claude_skill that can be uploaded to Claude for a combo of unity-mcp-skill. --- claude_skill_unity.zip | Bin 0 -> 75297 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 claude_skill_unity.zip diff --git a/claude_skill_unity.zip b/claude_skill_unity.zip new file mode 100644 index 0000000000000000000000000000000000000000..3584418b80aacf5b0181f7b547f38ad80f343e09 GIT binary patch literal 75297 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 G5bO8UVuFoRqv=V3!|54dcp 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 O8v_7%NeO@A1O5Z49$L}> literal 0 HcmV?d00001 From ae8cfe5e256c70ac4a16c79d50341a39cbac18ba Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Tue, 2 Dec 2025 00:32:15 -0500 Subject: [PATCH 6/8] Update for Custom_Tool Fix and Detection 1. Fix Original Roslyn Compilation Custom Tool to fit the V8 standard 2. Add a new panel in the GUI to see and toggle/untoggle the tools. The toggle feature will be implemented in the future, right now its implemented here to discuss with the team if this is a good feature to add; 3. Add few missing summary in certain tools --- .../ManageRuntimeCompilation.cs | 87 +++--- .../RoslynRuntimeCompilation/RoslynRuntime.md | 3 - .../runtime_compilation_tool.py | 269 ----------------- .../Editor/Constants/EditorPrefKeys.cs | 3 + .../Editor/MenuItems/MCPForUnityMenu.cs | 3 +- .../Editor/Services/IToolDiscoveryService.cs | 18 ++ .../Editor/Services/ToolDiscoveryService.cs | 224 ++++++++++++++- .../Transports/WebSocketTransportClient.cs | 3 +- MCPForUnity/Editor/Tools/ExecuteMenuItem.cs | 3 + .../Editor/Tools/Prefabs/ManagePrefabs.cs | 3 + .../Editor/Windows/Components/Common.uss | 85 ++++++ .../Editor/Windows/Components/Tools.meta | 6 +- .../Components/Tools/McpToolsSection.cs | 270 ++++++++++++++++++ .../Components/Tools/McpToolsSection.cs.meta | 11 + .../Components/Tools/McpToolsSection.uxml | 15 + .../Tools/McpToolsSection.uxml.meta | 4 +- .../Editor/Windows/MCPForUnityEditorWindow.cs | 145 +++++++++- .../Windows/MCPForUnityEditorWindow.uss | 27 +- .../Windows/MCPForUnityEditorWindow.uxml | 15 +- 19 files changed, 846 insertions(+), 348 deletions(-) delete mode 100644 CustomTools/RoslynRuntimeCompilation/RoslynRuntime.md delete mode 100644 CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py rename CustomTools/RoslynRuntimeCompilation/PythonTools.asset.meta => MCPForUnity/Editor/Windows/Components/Tools.meta (52%) create mode 100644 MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs create mode 100644 MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs.meta create mode 100644 MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.uxml rename CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py.meta => MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.uxml.meta (58%) diff --git a/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs b/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs index c5b6daab2..799341b27 100644 --- a/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs +++ b/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs @@ -19,9 +19,11 @@ namespace MCPForUnity.Editor.Tools { /// /// Runtime compilation tool for MCP Unity. - /// Compiles and loads C# code at runtime without triggering domain reload. + /// Compiles and loads C# code at runtime without triggering domain reload via Roslyn Runtime Compilation, where in traditional Unity workflow it would take seconds to reload assets and reset script states for each script change. /// - [McpForUnityTool("runtime_compilation")] + [McpForUnityTool( + name:"runtime_compilation", + Description = "Enable runtime compilation of C# code within Unity without domain reload via Roslyn.")] public static class ManageRuntimeCompilation { private static readonly Dictionary LoadedAssemblies = new Dictionary(); @@ -42,7 +44,7 @@ public static object HandleCommand(JObject @params) if (string.IsNullOrEmpty(action)) { - return Response.Error("Action parameter is required. Valid actions: compile_and_load, list_loaded, get_types, execute_with_roslyn, get_history, save_history, clear_history"); + return new ErrorResponse("Action parameter is required. Valid actions: compile_and_load, list_loaded, get_types, execute_with_roslyn, get_history, save_history, clear_history"); } switch (action) @@ -69,14 +71,14 @@ public static object HandleCommand(JObject @params) return ClearCompilationHistory(); default: - return Response.Error($"Unknown action '{action}'. Valid actions: compile_and_load, list_loaded, get_types, execute_with_roslyn, get_history, save_history, clear_history"); + return new ErrorResponse($"Unknown action '{action}'. Valid actions: compile_and_load, list_loaded, get_types, execute_with_roslyn, get_history, save_history, clear_history"); } } private static object CompileAndLoad(JObject @params) { #if !USE_ROSLYN - return Response.Error( + return new ErrorResponse( "Runtime compilation requires Roslyn. Please install Microsoft.CodeAnalysis.CSharp NuGet package and add USE_ROSLYN to Scripting Define Symbols. " + "See ManageScript.cs header for installation instructions." ); @@ -84,16 +86,13 @@ private static object CompileAndLoad(JObject @params) try { string code = @params["code"]?.ToString(); - var assemblyToken = @params["assembly_name"]; - string assemblyName = assemblyToken == null || string.IsNullOrWhiteSpace(assemblyToken.ToString()) - ? $"DynamicAssembly_{DateTime.Now.Ticks}" - : assemblyToken.ToString().Trim(); + string assemblyName = @params["assembly_name"]?.ToString() ?? $"DynamicAssembly_{DateTime.Now.Ticks}"; string attachTo = @params["attach_to"]?.ToString(); bool loadImmediately = @params["load_immediately"]?.ToObject() ?? true; if (string.IsNullOrEmpty(code)) { - return Response.Error("'code' parameter is required"); + return new ErrorResponse("'code' parameter is required"); } // Ensure unique assembly name @@ -104,21 +103,8 @@ private static object CompileAndLoad(JObject @params) // Create output directory Directory.CreateDirectory(DynamicAssembliesPath); - string basePath = Path.GetFullPath(DynamicAssembliesPath); - Directory.CreateDirectory(basePath); - string safeFileName = SanitizeAssemblyFileName(assemblyName); - string dllPath = Path.GetFullPath(Path.Combine(basePath, $"{safeFileName}.dll")); - - if (!dllPath.StartsWith(basePath, StringComparison.Ordinal)) - { - return Response.Error("Assembly name must resolve inside the dynamic assemblies directory."); - } - - if (File.Exists(dllPath)) - { - dllPath = Path.GetFullPath(Path.Combine(basePath, $"{safeFileName}_{DateTime.Now.Ticks}.dll")); - } - + string dllPath = Path.Combine(DynamicAssembliesPath, $"{assemblyName}.dll"); + // Parse code var syntaxTree = CSharpSyntaxTree.ParseText(code); @@ -137,7 +123,7 @@ private static object CompileAndLoad(JObject @params) // Emit to file EmitResult emitResult; - using (var stream = new FileStream(dllPath, FileMode.Create, FileAccess.Write, FileShare.None)) + using (var stream = new FileStream(dllPath, FileMode.Create)) { emitResult = compilation.Emit(stream); } @@ -156,7 +142,7 @@ private static object CompileAndLoad(JObject @params) }) .ToList(); - return Response.Error("Compilation failed", new + return new ErrorResponse("Compilation failed", new { errors = errors, error_count = errors.Count @@ -222,7 +208,7 @@ private static object CompileAndLoad(JObject @params) } } - return Response.Success("Runtime compilation completed successfully", new + return new SuccessResponse("Runtime compilation completed successfully", new { assembly_name = assemblyName, dll_path = dllPath, @@ -235,7 +221,7 @@ private static object CompileAndLoad(JObject @params) } catch (Exception ex) { - return Response.Error($"Runtime compilation failed: {ex.Message}", new + return new ErrorResponse($"Runtime compilation failed: {ex.Message}", new { exception = ex.GetType().Name, stack_trace = ex.StackTrace @@ -243,7 +229,7 @@ private static object CompileAndLoad(JObject @params) } #endif } - + private static object ListLoadedAssemblies() { var assemblies = LoadedAssemblies.Values.Select(info => new @@ -254,33 +240,26 @@ private static object ListLoadedAssemblies() type_count = info.TypeNames.Count, types = info.TypeNames }).ToList(); - - return Response.Success($"Found {assemblies.Count} loaded dynamic assemblies", new + + return new SuccessResponse($"Found {assemblies.Count} loaded dynamic assemblies", new { count = assemblies.Count, assemblies = assemblies }); } - private static string SanitizeAssemblyFileName(string assemblyName) - { - var invalidChars = Path.GetInvalidFileNameChars(); - var sanitized = new string(assemblyName.Where(c => !invalidChars.Contains(c)).ToArray()); - return string.IsNullOrWhiteSpace(sanitized) ? $"DynamicAssembly_{DateTime.Now.Ticks}" : sanitized; - } - private static object GetAssemblyTypes(JObject @params) { string assemblyName = @params["assembly_name"]?.ToString(); if (string.IsNullOrEmpty(assemblyName)) { - return Response.Error("'assembly_name' parameter is required"); + return new ErrorResponse("'assembly_name' parameter is required"); } if (!LoadedAssemblies.TryGetValue(assemblyName, out var info)) { - return Response.Error($"Assembly '{assemblyName}' not found in loaded assemblies"); + return new ErrorResponse($"Assembly '{assemblyName}' not found in loaded assemblies"); } var types = info.Assembly.GetTypes().Select(t => new @@ -294,7 +273,7 @@ private static object GetAssemblyTypes(JObject @params) base_type = t.BaseType?.FullName }).ToList(); - return Response.Success($"Retrieved {types.Count} types from {assemblyName}", new + return new SuccessResponse($"Retrieved {types.Count} types from {assemblyName}", new { assembly_name = assemblyName, type_count = types.Count, @@ -318,7 +297,7 @@ private static object ExecuteWithRoslyn(JObject @params) if (string.IsNullOrEmpty(code)) { - return Response.Error("'code' parameter is required"); + return new ErrorResponse("'code' parameter is required"); } // Get or create the RoslynRuntimeCompiler instance @@ -336,7 +315,7 @@ private static object ExecuteWithRoslyn(JObject @params) if (targetObject == null) { - return Response.Error($"Target GameObject '{targetObjectName}' not found"); + return new ErrorResponse($"Target GameObject '{targetObjectName}' not found"); } } @@ -352,7 +331,7 @@ out string errorMessage if (success) { - return Response.Success($"Code compiled and executed successfully", new + return new SuccessResponse($"Code compiled and executed successfully", new { class_name = className, method_name = methodName, @@ -363,7 +342,7 @@ out string errorMessage } else { - return Response.Error($"Execution failed: {errorMessage}", new + return new ErrorResponse($"Execution failed: {errorMessage}", new { diagnostics = compiler.lastCompileDiagnostics }); @@ -371,7 +350,7 @@ out string errorMessage } catch (Exception ex) { - return Response.Error($"Failed to execute with Roslyn: {ex.Message}", new + return new ErrorResponse($"Failed to execute with Roslyn: {ex.Message}", new { exception = ex.GetType().Name, stack_trace = ex.StackTrace @@ -402,7 +381,7 @@ private static object GetCompilationHistory() : entry.sourceCode }).ToList(); - return Response.Success($"Retrieved {historyData.Count} history entries", new + return new SuccessResponse($"Retrieved {historyData.Count} history entries", new { count = historyData.Count, history = historyData @@ -410,7 +389,7 @@ private static object GetCompilationHistory() } catch (Exception ex) { - return Response.Error($"Failed to get history: {ex.Message}"); + return new ErrorResponse($"Failed to get history: {ex.Message}"); } } @@ -425,7 +404,7 @@ private static object SaveCompilationHistory() if (compiler.SaveHistoryToFile(out string savedPath, out string error)) { - return Response.Success($"History saved successfully", new + return new SuccessResponse($"History saved successfully", new { path = savedPath, entry_count = compiler.CompilationHistory.Count @@ -433,12 +412,12 @@ private static object SaveCompilationHistory() } else { - return Response.Error($"Failed to save history: {error}"); + return new ErrorResponse($"Failed to save history: {error}"); } } catch (Exception ex) { - return Response.Error($"Failed to save history: {ex.Message}"); + return new ErrorResponse($"Failed to save history: {ex.Message}"); } } @@ -453,11 +432,11 @@ private static object ClearCompilationHistory() int count = compiler.CompilationHistory.Count; compiler.ClearHistory(); - return Response.Success($"Cleared {count} history entries"); + return new SuccessResponse($"Cleared {count} history entries"); } catch (Exception ex) { - return Response.Error($"Failed to clear history: {ex.Message}"); + return new ErrorResponse($"Failed to clear history: {ex.Message}"); } } diff --git a/CustomTools/RoslynRuntimeCompilation/RoslynRuntime.md b/CustomTools/RoslynRuntimeCompilation/RoslynRuntime.md deleted file mode 100644 index 91d05d5b8..000000000 --- a/CustomTools/RoslynRuntimeCompilation/RoslynRuntime.md +++ /dev/null @@ -1,3 +0,0 @@ -# Roslyn Runtime Compilation Tool - -This custom tool uses Roslyn Runtime Compilation to have users run script generation and compilation during Playmode in realtime, where in traditional Unity workflow it would take seconds to reload assets and reset script states for each script change. diff --git a/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py b/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py deleted file mode 100644 index 977b9f717..000000000 --- a/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py +++ /dev/null @@ -1,269 +0,0 @@ -""" -Runtime compilation tool for MCP Unity. -Compiles and loads C# code at runtime without domain reload. -""" - -from typing import Annotated, Any -from fastmcp import Context -from registry import mcp_for_unity_tool -from unity_connection import send_command_with_retry - - -async def safe_info(ctx: Context, message: str) -> None: - """Safely send info messages when a request context is available.""" - try: - if ctx and hasattr(ctx, "info"): - await ctx.info(message) - except RuntimeError as ex: - # FastMCP raises this when called outside of an active request - if "outside of a request" not in str(ex): - raise - - -def handle_unity_command(command_name: str, params: dict) -> dict[str, Any]: - """ - Wrapper for Unity commands with better error handling. - """ - try: - response = send_command_with_retry(command_name, params) - return response if isinstance(response, dict) else {"success": False, "message": str(response)} - except Exception as e: - error_msg = str(e) - if "Context is not available" in error_msg or "not available outside of a request" in error_msg: - return { - "success": False, - "message": "Unity is not connected. Please ensure Unity Editor is running and MCP bridge is active.", - "error": "connection_error", - "details": "This tool requires an active connection to Unity. Make sure the Unity project is open and the MCP bridge is initialized." - } - return { - "success": False, - "message": f"Command failed: {error_msg}", - "error": "tool_error" - } - - -@mcp_for_unity_tool( - description="Compile and load C# code at runtime without domain reload. Creates dynamic assemblies that can be attached to GameObjects during Play Mode. Requires Roslyn (Microsoft.CodeAnalysis.CSharp) to be installed in Unity." -) -async def compile_runtime_code( - ctx: Context, - code: Annotated[str, "Complete C# code including using statements, namespace, and class definition"], - assembly_name: Annotated[str, "Unique name for the dynamic assembly. If not provided, a timestamp-based name will be generated."] | None = None, - attach_to_gameobject: Annotated[str, "Name or hierarchy path of GameObject to attach the compiled script to (e.g., 'Player' or 'Canvas/Panel')"] | None = None, - load_immediately: Annotated[bool, "Whether to load the assembly immediately after compilation. Default is true."] = True -) -> dict[str, Any]: - """ - Compile C# code at runtime and optionally attach it to a GameObject. Only enable it with Roslyn installed in Unity. - - REQUIREMENTS: - - Unity must be running and connected - - Roslyn (Microsoft.CodeAnalysis.CSharp) must be installed via NuGet - - USE_ROSLYN scripting define symbol must be set - - This tool allows you to: - - Compile new C# scripts without restarting Unity - - Load compiled assemblies into the running Unity instance - - Attach MonoBehaviour scripts to GameObjects dynamically - - Preserve game state during script additions - - Example code: - ```csharp - using UnityEngine; - - namespace DynamicScripts - { - public class MyDynamicBehavior : MonoBehaviour - { - void Start() - { - Debug.Log("Dynamic script loaded!"); - } - } - } - ``` - """ - await safe_info(ctx, f"Compiling runtime code for assembly: {assembly_name or 'auto-generated'}") - - params = { - "action": "compile_and_load", - "code": code, - "assembly_name": assembly_name, - "attach_to": attach_to_gameobject, - "load_immediately": load_immediately, - } - params = {k: v for k, v in params.items() if v is not None} - - return handle_unity_command("runtime_compilation", params) - - -@mcp_for_unity_tool( - description="List all dynamically loaded assemblies in the current Unity session" -) -async def list_loaded_assemblies( - ctx: Context, -) -> dict[str, Any]: - """ - Get a list of all dynamically loaded assemblies created during this session. - - Returns information about: - - Assembly names - - Number of types in each assembly - - Load timestamps - - DLL file paths - """ - await safe_info(ctx, "Retrieving loaded dynamic assemblies...") - - params = {"action": "list_loaded"} - return handle_unity_command("runtime_compilation", params) - - -@mcp_for_unity_tool( - description="Get all types (classes) from a dynamically loaded assembly" -) -async def get_assembly_types( - ctx: Context, - assembly_name: Annotated[str, "Name of the assembly to query"], -) -> dict[str, Any]: - """ - Retrieve all types defined in a specific dynamic assembly. - - This is useful for: - - Inspecting what was compiled - - Finding MonoBehaviour classes to attach - - Debugging compilation results - """ - await safe_info(ctx, f"Getting types from assembly: {assembly_name}") - - params = {"action": "get_types", "assembly_name": assembly_name} - return handle_unity_command("runtime_compilation", params) - - -@mcp_for_unity_tool( - description="Execute C# code using the RoslynRuntimeCompiler with full GUI tool features including history tracking, MonoBehaviour support, and coroutines" -) -async def execute_with_roslyn( - ctx: Context, - code: Annotated[str, "Complete C# source code to compile and execute"], - class_name: Annotated[str, "Name of the class to instantiate/invoke (default: AIGenerated)"] = "AIGenerated", - method_name: Annotated[str, "Name of the static method to call (default: Run)"] = "Run", - target_object: Annotated[str, "Name or path of target GameObject (optional)"] | None = None, - attach_as_component: Annotated[bool, "If true and type is MonoBehaviour, attach as component (default: false)"] = False, -) -> dict[str, Any]: - """ - Execute C# code using Unity's RoslynRuntimeCompiler tool with advanced features: - - - MonoBehaviour attachment: Set attach_as_component=true for classes inheriting MonoBehaviour - - Static method execution: Call public static methods (e.g., public static void Run(GameObject host)) - - Coroutine support: Methods returning IEnumerator will be started as coroutines - - History tracking: All compilations are tracked in history for later review - - Supported method signatures: - - public static void Run() - - public static void Run(GameObject host) - - public static void Run(MonoBehaviour host) - - public static IEnumerator RunCoroutine(MonoBehaviour host) - - Example MonoBehaviour: - ```csharp - using UnityEngine; - public class Rotator : MonoBehaviour { - void Update() { - transform.Rotate(Vector3.up * 30f * Time.deltaTime); - } - } - ``` - - Example Static Method: - ```csharp - using UnityEngine; - public class AIGenerated { - public static void Run(GameObject host) { - Debug.Log($"Hello from {host.name}!"); - } - } - ``` - """ - await safe_info(ctx, f"Executing code with RoslynRuntimeCompiler: {class_name}.{method_name}") - - params = { - "action": "execute_with_roslyn", - "code": code, - "class_name": class_name, - "method_name": method_name, - "target_object": target_object, - "attach_as_component": attach_as_component, - } - params = {k: v for k, v in params.items() if v is not None} - - return handle_unity_command("runtime_compilation", params) - - -@mcp_for_unity_tool( - description="Get the compilation history from RoslynRuntimeCompiler showing all previous compilations and executions" -) -async def get_compilation_history( - ctx: Context, -) -> dict[str, Any]: - """ - Retrieve the compilation history from the RoslynRuntimeCompiler. - - History includes: - - Timestamp of each compilation - - Class and method names - - Success/failure status - - Compilation diagnostics - - Target GameObject names - - Source code previews - - This is useful for: - - Reviewing what code has been compiled - - Debugging failed compilations - - Tracking execution flow - - Auditing dynamic code changes - """ - await safe_info(ctx, "Retrieving compilation history...") - - params = {"action": "get_history"} - return handle_unity_command("runtime_compilation", params) - - -@mcp_for_unity_tool( - description="Save the compilation history to a JSON file outside the Assets folder" -) -async def save_compilation_history( - ctx: Context, -) -> dict[str, Any]: - """ - Save all compilation history to a timestamped JSON file. - - The file is saved to: ProjectRoot/RoslynHistory/RoslynHistory_TIMESTAMP.json - - This allows you to: - - Keep a permanent record of dynamic compilations - - Review history after Unity restarts - - Share compilation sessions with team members - - Archive successful code patterns - """ - await safe_info(ctx, "Saving compilation history to file...") - - params = {"action": "save_history"} - return handle_unity_command("runtime_compilation", params) - - -@mcp_for_unity_tool( - description="Clear all compilation history from RoslynRuntimeCompiler" -) -async def clear_compilation_history( - ctx: Context, -) -> dict[str, Any]: - """ - Clear all compilation history entries. - - This removes all tracked compilations from memory but does not delete - saved history files. Use this to start fresh or reduce memory usage. - """ - await safe_info(ctx, "Clearing compilation history...") - - params = {"action": "clear_history"} - return handle_unity_command("runtime_compilation", params) diff --git a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs index 30dcd2bb3..570e9f9dd 100644 --- a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs +++ b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs @@ -25,6 +25,9 @@ internal static class EditorPrefKeys internal const string UseEmbeddedServer = "MCPForUnity.UseEmbeddedServer"; internal const string LockCursorConfig = "MCPForUnity.LockCursorConfig"; internal const string AutoRegisterEnabled = "MCPForUnity.AutoRegisterEnabled"; + internal const string ToolEnabledPrefix = "MCPForUnity.ToolEnabled."; + internal const string ToolFoldoutStatePrefix = "MCPForUnity.ToolFoldout."; + internal const string EditorWindowActivePanel = "MCPForUnity.EditorWindow.ActivePanel"; internal const string SetupCompleted = "MCPForUnity.SetupCompleted"; internal const string SetupDismissed = "MCPForUnity.SetupDismissed"; diff --git a/MCPForUnity/Editor/MenuItems/MCPForUnityMenu.cs b/MCPForUnity/Editor/MenuItems/MCPForUnityMenu.cs index 32fde7203..07e7a096e 100644 --- a/MCPForUnity/Editor/MenuItems/MCPForUnityMenu.cs +++ b/MCPForUnity/Editor/MenuItems/MCPForUnityMenu.cs @@ -31,7 +31,8 @@ public static void ToggleMCPWindow() { if (EditorWindow.HasOpenInstances()) { - foreach (var window in UnityEngine.Resources.FindObjectsOfTypeAll()) + var window = EditorWindow.GetWindow(); + if (window != null) { window.Close(); } diff --git a/MCPForUnity/Editor/Services/IToolDiscoveryService.cs b/MCPForUnity/Editor/Services/IToolDiscoveryService.cs index 01ceb2b63..300f00e05 100644 --- a/MCPForUnity/Editor/Services/IToolDiscoveryService.cs +++ b/MCPForUnity/Editor/Services/IToolDiscoveryService.cs @@ -13,9 +13,12 @@ public class ToolMetadata public List Parameters { get; set; } public string ClassName { get; set; } public string Namespace { get; set; } + public string AssemblyName { get; set; } + public string AssetPath { get; set; } public bool AutoRegister { get; set; } = true; public bool RequiresPolling { get; set; } = false; public string PollAction { get; set; } = "status"; + public bool IsBuiltIn { get; set; } } /// @@ -45,6 +48,21 @@ public interface IToolDiscoveryService /// ToolMetadata GetToolMetadata(string toolName); + /// + /// Returns only the tools currently enabled for registration + /// + List GetEnabledTools(); + + /// + /// Checks whether a tool is currently enabled for registration + /// + bool IsToolEnabled(string toolName); + + /// + /// Updates the enabled state for a tool + /// + void SetToolEnabled(string toolName, bool enabled); + /// /// Invalidates the tool discovery cache /// diff --git a/MCPForUnity/Editor/Services/ToolDiscoveryService.cs b/MCPForUnity/Editor/Services/ToolDiscoveryService.cs index 0f3406a1e..0dcfc79be 100644 --- a/MCPForUnity/Editor/Services/ToolDiscoveryService.cs +++ b/MCPForUnity/Editor/Services/ToolDiscoveryService.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; +using MCPForUnity.Editor.Constants; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Tools; using UnityEditor; @@ -11,6 +13,8 @@ namespace MCPForUnity.Editor.Services public class ToolDiscoveryService : IToolDiscoveryService { private Dictionary _cachedTools; + private readonly Dictionary _scriptPathCache = new(); + private readonly Dictionary _summaryCache = new(); public List DiscoverAllTools() { @@ -40,6 +44,7 @@ public List DiscoverAllTools() if (metadata != null) { _cachedTools[metadata.Name] = metadata; + EnsurePreferenceInitialized(metadata); } } } @@ -64,6 +69,41 @@ public ToolMetadata GetToolMetadata(string toolName) return _cachedTools.TryGetValue(toolName, out var metadata) ? metadata : null; } + public List GetEnabledTools() + { + return DiscoverAllTools() + .Where(tool => IsToolEnabled(tool.Name)) + .ToList(); + } + + public bool IsToolEnabled(string toolName) + { + if (string.IsNullOrEmpty(toolName)) + { + return false; + } + + string key = GetToolPreferenceKey(toolName); + if (EditorPrefs.HasKey(key)) + { + return EditorPrefs.GetBool(key, true); + } + + var metadata = GetToolMetadata(toolName); + return metadata?.AutoRegister ?? false; + } + + public void SetToolEnabled(string toolName, bool enabled) + { + if (string.IsNullOrEmpty(toolName)) + { + return; + } + + string key = GetToolPreferenceKey(toolName); + EditorPrefs.SetBool(key, enabled); + } + private ToolMetadata ExtractToolMetadata(Type type, McpForUnityToolAttribute toolAttr) { try @@ -82,7 +122,7 @@ private ToolMetadata ExtractToolMetadata(Type type, McpForUnityToolAttribute too // Extract parameters var parameters = ExtractParameters(type); - return new ToolMetadata + var metadata = new ToolMetadata { Name = toolName, Description = description, @@ -90,10 +130,24 @@ private ToolMetadata ExtractToolMetadata(Type type, McpForUnityToolAttribute too Parameters = parameters, ClassName = type.Name, Namespace = type.Namespace ?? "", + AssemblyName = type.Assembly.GetName().Name, + AssetPath = ResolveScriptAssetPath(type), AutoRegister = toolAttr.AutoRegister, RequiresPolling = toolAttr.RequiresPolling, PollAction = string.IsNullOrEmpty(toolAttr.PollAction) ? "status" : toolAttr.PollAction }; + + metadata.IsBuiltIn = DetermineIsBuiltIn(type, metadata); + if (metadata.IsBuiltIn) + { + string summaryDescription = ExtractSummaryDescription(type, metadata); + if (!string.IsNullOrWhiteSpace(summaryDescription)) + { + metadata.Description = summaryDescription; + } + } + return metadata; + } catch (Exception ex) { @@ -180,5 +234,173 @@ public void InvalidateCache() { _cachedTools = null; } + + private void EnsurePreferenceInitialized(ToolMetadata metadata) + { + if (metadata == null || string.IsNullOrEmpty(metadata.Name)) + { + return; + } + + string key = GetToolPreferenceKey(metadata.Name); + if (!EditorPrefs.HasKey(key)) + { + bool defaultValue = metadata.AutoRegister || metadata.IsBuiltIn; + EditorPrefs.SetBool(key, defaultValue); + return; + } + + if (metadata.IsBuiltIn && !metadata.AutoRegister) + { + bool currentValue = EditorPrefs.GetBool(key, metadata.AutoRegister); + if (currentValue == metadata.AutoRegister) + { + EditorPrefs.SetBool(key, true); + } + } + } + + private static string GetToolPreferenceKey(string toolName) + { + return EditorPrefKeys.ToolEnabledPrefix + toolName; + } + + private string ResolveScriptAssetPath(Type type) + { + if (type == null) + { + return null; + } + + if (_scriptPathCache.TryGetValue(type, out var cachedPath)) + { + return cachedPath; + } + + string resolvedPath = null; + + try + { + string filter = string.IsNullOrEmpty(type.Name) ? "t:MonoScript" : $"{type.Name} t:MonoScript"; + var guids = AssetDatabase.FindAssets(filter); + + foreach (var guid in guids) + { + string assetPath = AssetDatabase.GUIDToAssetPath(guid); + if (string.IsNullOrEmpty(assetPath)) + { + continue; + } + + var script = AssetDatabase.LoadAssetAtPath(assetPath); + if (script == null) + { + continue; + } + + var scriptClass = script.GetClass(); + if (scriptClass == type) + { + resolvedPath = assetPath.Replace('\\', '/'); + break; + } + } + } + catch (Exception ex) + { + McpLog.Warn($"Failed to resolve asset path for {type.FullName}: {ex.Message}"); + } + + _scriptPathCache[type] = resolvedPath; + return resolvedPath; + } + + private bool DetermineIsBuiltIn(Type type, ToolMetadata metadata) + { + if (metadata == null) + { + return false; + } + + if (!string.IsNullOrEmpty(metadata.AssetPath)) + { + string normalizedPath = metadata.AssetPath.Replace("\\", "/"); + string packageRoot = AssetPathUtility.GetMcpPackageRootPath(); + + if (!string.IsNullOrEmpty(packageRoot)) + { + string normalizedRoot = packageRoot.Replace("\\", "/"); + if (!normalizedRoot.EndsWith("/", StringComparison.Ordinal)) + { + normalizedRoot += "/"; + } + + string builtInRoot = normalizedRoot + "Editor/Tools/"; + if (normalizedPath.StartsWith(builtInRoot, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + + if (!string.IsNullOrEmpty(metadata.AssemblyName) && metadata.AssemblyName.Equals("MCPForUnity.Editor", StringComparison.Ordinal)) + { + return true; + } + + return false; + } + + private string ExtractSummaryDescription(Type type, ToolMetadata metadata) + { + if (metadata == null || string.IsNullOrEmpty(metadata.AssetPath)) + { + return null; + } + + if (_summaryCache.TryGetValue(metadata.AssetPath, out var cachedSummary)) + { + return cachedSummary; + } + + string summary = null; + + try + { + var monoScript = AssetDatabase.LoadAssetAtPath(metadata.AssetPath); + string scriptText = monoScript?.text; + if (string.IsNullOrEmpty(scriptText)) + { + _summaryCache[metadata.AssetPath] = null; + return null; + } + + string classPattern = $@"///\s*\s*(?[\s\S]*?)\s*\s*(?:\[[^\]]*\]\s*)*(?:public\s+)?(?:static\s+)?class\s+{Regex.Escape(type.Name)}"; + var match = Regex.Match(scriptText, classPattern); + + if (!match.Success) + { + match = Regex.Match(scriptText, @"///\s*\s*(?[\s\S]*?)\s*"); + } + + if (!match.Success) + { + _summaryCache[metadata.AssetPath] = null; + return null; + } + + summary = match.Groups["content"].Value; + summary = Regex.Replace(summary, @"^\s*///\s?", string.Empty, RegexOptions.Multiline); + summary = Regex.Replace(summary, @"<[^>]+>", string.Empty); + summary = Regex.Replace(summary, @"\s+", " ").Trim(); + } + catch (System.Exception ex) + { + McpLog.Warn($"Failed to extract summary description for {type?.FullName}: {ex.Message}"); + } + + _summaryCache[metadata.AssetPath] = summary; + return summary; + } } } diff --git a/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs b/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs index 35011a80d..0648193e7 100644 --- a/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs +++ b/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs @@ -421,7 +421,8 @@ private async Task SendRegisterToolsAsync(CancellationToken token) { if (_toolDiscoveryService == null) return; - var tools = _toolDiscoveryService.DiscoverAllTools(); + var tools = _toolDiscoveryService.GetEnabledTools(); + McpLog.Info($"[WebSocket] Preparing to register {tools.Count} tool(s) with the bridge."); var toolsArray = new JArray(); foreach (var tool in tools) diff --git a/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs b/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs index 2ce1c8442..f606f774f 100644 --- a/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs +++ b/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs @@ -7,6 +7,9 @@ namespace MCPForUnity.Editor.Tools { [McpForUnityTool("execute_menu_item", AutoRegister = false)] + /// + /// Tool to execute a Unity Editor menu item by its path. + /// public static class ExecuteMenuItem { // Basic blacklist to prevent execution of disruptive menu items. diff --git a/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs b/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs index d30053edd..18b4ae0c0 100644 --- a/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs +++ b/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs @@ -10,6 +10,9 @@ namespace MCPForUnity.Editor.Tools.Prefabs { [McpForUnityTool("manage_prefabs", AutoRegister = false)] + /// + /// Tool to manage Unity Prefab stages and create prefabs from GameObjects. + /// public static class ManagePrefabs { private const string SupportedActions = "open_stage, close_stage, save_open_stage, create_from_gameobject"; diff --git a/MCPForUnity/Editor/Windows/Components/Common.uss b/MCPForUnity/Editor/Windows/Components/Common.uss index 4c2f05fe4..e89e0bec7 100644 --- a/MCPForUnity/Editor/Windows/Components/Common.uss +++ b/MCPForUnity/Editor/Windows/Components/Common.uss @@ -293,6 +293,75 @@ margin-top: 4px; } +/* Tools Section */ +.tool-actions { + flex-direction: row; + flex-wrap: wrap; + margin-top: 8px; + margin-bottom: 8px; +} + +.tool-action-button { + flex-grow: 1; + min-width: 0; + height: 26px; + margin-bottom: 4px; +} + +.tool-category-container { + flex-direction: column; + margin-top: 8px; +} + +.tool-item { + flex-direction: column; + padding: 8px; + margin-bottom: 8px; + background-color: rgba(0, 0, 0, 0.04); + border-radius: 4px; + border-width: 1px; + border-color: rgba(0, 0, 0, 0.12); +} + +.tool-item-header { + flex-direction: row; + align-items: center; +} + +.tool-item-toggle { + flex-shrink: 0; + min-width: 0; +} + +.tool-tags { + flex-direction: row; + flex-wrap: wrap; + margin-left: auto; + padding-left: 8px; +} + +.tool-tag { + font-size: 10px; + padding: 2px 6px; + margin-left: 4px; + margin-top: 2px; + background-color: rgba(100, 100, 100, 0.25); + border-radius: 3px; + color: rgba(40, 40, 40, 1); +} + +.tool-item-description, +.tool-parameters { + font-size: 11px; + color: rgba(120, 120, 120, 1); + white-space: normal; + margin-top: 4px; +} + +.tool-parameters { + font-style: italic; +} + /* Advanced Settings */ .advanced-settings-foldout { margin-top: 16px; @@ -400,6 +469,22 @@ border-color: rgba(0, 0, 0, 0.15); } +.unity-theme-dark .tool-tag { + color: rgba(220, 220, 220, 1); + background-color: rgba(80, 80, 80, 0.6); +} + +.unity-theme-dark .tool-item { + background-color: rgba(255, 255, 255, 0.04); + border-color: rgba(255, 255, 255, 0.08); +} + +.unity-theme-dark .tool-item-description, +.unity-theme-dark .tool-parameters { + color: rgba(200, 200, 200, 0.8); +} + + .unity-theme-light .validation-description { background-color: rgba(100, 150, 200, 0.1); } diff --git a/CustomTools/RoslynRuntimeCompilation/PythonTools.asset.meta b/MCPForUnity/Editor/Windows/Components/Tools.meta similarity index 52% rename from CustomTools/RoslynRuntimeCompilation/PythonTools.asset.meta rename to MCPForUnity/Editor/Windows/Components/Tools.meta index 58dd3a6f9..a1165e9c1 100644 --- a/CustomTools/RoslynRuntimeCompilation/PythonTools.asset.meta +++ b/MCPForUnity/Editor/Windows/Components/Tools.meta @@ -1,8 +1,8 @@ fileFormatVersion: 2 -guid: a3b463767742cdf43b366f68a656e42e -NativeFormatImporter: +guid: c2f853b1b3974f829a2cc09d52d3d7ad +folderAsset: yes +DefaultImporter: externalObjects: {} - mainObjectFileID: 11400000 userData: assetBundleName: assetBundleVariant: diff --git a/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs b/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs new file mode 100644 index 000000000..b6fc2b4a9 --- /dev/null +++ b/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MCPForUnity.Editor.Constants; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Services; +using UnityEditor; +using UnityEngine.UIElements; + +namespace MCPForUnity.Editor.Windows.Components.Tools +{ + /// + /// Controller for the Tools section inside the MCP For Unity editor window. + /// Provides discovery, filtering, and per-tool enablement toggles. + /// + public class McpToolsSection + { + private readonly Dictionary toolToggleMap = new(); + private Label summaryLabel; + private Label noteLabel; + private Button enableAllButton; + private Button disableAllButton; + private Button rescanButton; + private VisualElement categoryContainer; + private List allTools = new(); + + public VisualElement Root { get; } + + public McpToolsSection(VisualElement root) + { + Root = root; + CacheUIElements(); + RegisterCallbacks(); + } + + private void CacheUIElements() + { + summaryLabel = Root.Q