Skip to main content
Glama
ManageRuntimeCompilation.cs21.7 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. /// </summary> [McpForUnityTool("runtime_compilation")] 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 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(); 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<bool>() ?? 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 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); // 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, FileAccess.Write, FileShare.None)) { 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<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 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 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"); } 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 }); } /// <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 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 }); } } /// <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 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}"); } } /// <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 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}"); } } /// <summary> /// Clear compilation history /// </summary> 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<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