Skip to main content
Glama
ManageRuntimeCompilation.cs20.8 kB
#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 { /// <summary> /// Runtime compilation tool for MCP Unity. /// 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. /// </summary> [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<string, LoadedAssemblyInfo> LoadedAssemblies = new Dictionary<string, LoadedAssemblyInfo>(); 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<string> TypeNames; } public static object HandleCommand(JObject @params) { string action = @params["action"]?.ToString()?.ToLower(); if (string.IsNullOrEmpty(action)) { 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) { 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 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 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." ); #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<bool>() ?? true; if (string.IsNullOrEmpty(code)) { return new ErrorResponse("'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 new ErrorResponse("Compilation failed", new { errors = errors, error_count = errors.Count }); } // Load assembly if requested Assembly loadedAssembly = null; List<string> typeNames = new List<string>(); 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 new SuccessResponse("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 new ErrorResponse($"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 new SuccessResponse($"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 new ErrorResponse("'assembly_name' parameter is required"); } if (!LoadedAssemblies.TryGetValue(assemblyName, out var info)) { return new ErrorResponse($"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 new SuccessResponse($"Retrieved {types.Count} types from {assemblyName}", new { assembly_name = assemblyName, type_count = types.Count, types = types }); } /// <summary> /// Execute code using RoslynRuntimeCompiler with full GUI tool integration /// Supports MonoBehaviours, static methods, and coroutines /// </summary> 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<bool>() ?? false; if (string.IsNullOrEmpty(code)) { return new ErrorResponse("'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 new ErrorResponse($"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 new SuccessResponse($"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 new ErrorResponse($"Execution failed: {errorMessage}", new { diagnostics = compiler.lastCompileDiagnostics }); } } catch (Exception ex) { return new ErrorResponse($"Failed to execute with Roslyn: {ex.Message}", new { exception = ex.GetType().Name, stack_trace = ex.StackTrace }); } } /// <summary> /// Get compilation history from RoslynRuntimeCompiler /// </summary> 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 new SuccessResponse($"Retrieved {historyData.Count} history entries", new { count = historyData.Count, history = historyData }); } catch (Exception ex) { return new ErrorResponse($"Failed to get history: {ex.Message}"); } } /// <summary> /// Save compilation history to JSON file /// </summary> private static object SaveCompilationHistory() { try { var compiler = GetOrCreateRoslynCompiler(); if (compiler.SaveHistoryToFile(out string savedPath, out string error)) { return new SuccessResponse($"History saved successfully", new { path = savedPath, entry_count = compiler.CompilationHistory.Count }); } else { return new ErrorResponse($"Failed to save history: {error}"); } } catch (Exception ex) { return new ErrorResponse($"Failed to save history: {ex.Message}"); } } /// <summary> /// Clear compilation history /// </summary> private static object ClearCompilationHistory() { try { var compiler = GetOrCreateRoslynCompiler(); int count = compiler.CompilationHistory.Count; compiler.ClearHistory(); return new SuccessResponse($"Cleared {count} history entries"); } catch (Exception ex) { return new ErrorResponse($"Failed to clear history: {ex.Message}"); } } #if USE_ROSLYN private static List<MetadataReference> GetDefaultReferences() { var references = new List<MetadataReference>(); // 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; } /// <summary> /// Get or create a RoslynRuntimeCompiler instance for GUI integration /// This allows MCP commands to leverage the existing GUI tool /// </summary> private static RoslynRuntimeCompiler GetOrCreateRoslynCompiler() { var existing = UnityEngine.Object.FindFirstObjectByType<RoslynRuntimeCompiler>(); if (existing != null) { return existing; } var go = new GameObject("MCPRoslynCompiler"); var compiler = go.AddComponent<RoslynRuntimeCompiler>(); compiler.enableHistory = true; // Enable history tracking for MCP operations if (!Application.isPlaying) { go.hideFlags = HideFlags.HideAndDontSave; } return compiler; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/CoplayDev/unity-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server