using System; using System.CodeDom.Compiler; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using MCPForUnity.Editor.Helpers; using MCPForUnity.Runtime.Helpers; using Microsoft.CSharp; using Newtonsoft.Json.Linq; using UnityEngine; namespace MCPForUnity.Editor.Tools { [McpForUnityTool("execute_code", AutoRegister = false, Group = "scripting_ext")] public static class ExecuteCode { private const int MaxCodeLength = 50000; private const int MaxHistoryEntries = 50; private const int MaxHistoryCodePreview = 500; internal const int WrapperLineOffset = 10; private const string WrapperClassName = "MCPDynamicCode"; private const string WrapperMethodName = "Execute"; private const string ActionExecute = "execute"; private const string ActionGetHistory = "get_history"; private const string ActionClearHistory = "clear_history"; private const string ActionReplay = "replay"; private static readonly List _history = new List(); private static string[] _cachedAssemblyPaths; [UnityEditor.InitializeOnLoadMethod] private static void OnDomainReload() { _cachedAssemblyPaths = null; RoslynCompiler.ResetCache(); } private static readonly HashSet _blockedPatterns = new HashSet(StringComparer.OrdinalIgnoreCase) { "System.IO.File.Delete", "System.IO.Directory.Delete", "FileUtil.DeleteFileOrDirectory", "AssetDatabase.DeleteAsset", "AssetDatabase.MoveAssetToTrash", "EditorApplication.Exit", "Process.Start", "Process.Kill", "while(true)", "while (true)", "for(;;)", "for (;;)", }; public static object HandleCommand(JObject @params) { if (@params == null) return new ErrorResponse("Parameters cannot be null."); var p = new ToolParams(@params); var actionResult = p.GetRequired("action"); if (!actionResult.IsSuccess) return new ErrorResponse(actionResult.ErrorMessage); string action = actionResult.Value.ToLowerInvariant(); switch (action) { case ActionExecute: return HandleExecute(@params); case ActionGetHistory: return HandleGetHistory(@params); case ActionClearHistory: return HandleClearHistory(); case ActionReplay: return HandleReplay(@params); default: return new ErrorResponse( $"Unknown action: '{action}'. Valid actions: {ActionExecute}, {ActionGetHistory}, {ActionClearHistory}, {ActionReplay}"); } } private static object HandleExecute(JObject @params) { string code = @params["code"]?.ToString(); if (string.IsNullOrWhiteSpace(code)) return new ErrorResponse("Required parameter 'code' is missing or empty."); if (code.Length > MaxCodeLength) return new ErrorResponse($"Code exceeds maximum length of {MaxCodeLength} characters."); bool safetyChecks = @params["safety_checks"]?.Value() ?? true; string compiler = @params["compiler"]?.ToString()?.ToLowerInvariant() ?? "auto"; if (safetyChecks) { var violation = CheckBlockedPatterns(code); if (violation != null) return new ErrorResponse($"Blocked pattern detected: {violation}"); } try { var startTime = DateTime.UtcNow; var result = CompileAndExecute(code, compiler); var elapsed = (DateTime.UtcNow - startTime).TotalMilliseconds; AddToHistory(code, result, elapsed, safetyChecks, compiler); return result; } catch (Exception e) { McpLog.Error($"[ExecuteCode] Execution failed: {e}"); var errorResult = new ErrorResponse($"Execution failed: {e.Message}"); AddToHistory(code, errorResult, 0, safetyChecks, compiler); return errorResult; } } private static object HandleGetHistory(JObject @params) { int limit = @params["limit"]?.Value() ?? 10; limit = Math.Clamp(limit, 1, MaxHistoryEntries); if (_history.Count == 0) return new SuccessResponse("No execution history.", new { total = 0, entries = new object[0] }); var entries = _history.Skip(Math.Max(0, _history.Count - limit)).ToList(); return new SuccessResponse($"Returning {entries.Count} of {_history.Count} history entries.", new { total = _history.Count, entries = entries.Select((e, i) => new { index = _history.Count - entries.Count + i, codePreview = e.code.Length > MaxHistoryCodePreview ? e.code.Substring(0, MaxHistoryCodePreview) + "..." : e.code, e.success, e.resultPreview, e.elapsedMs, e.timestamp, e.safetyChecksEnabled, e.compiler, }).ToList(), }); } private static object HandleClearHistory() { int count = _history.Count; _history.Clear(); return new SuccessResponse($"Cleared {count} history entries."); } private static object HandleReplay(JObject @params) { if (_history.Count == 0) return new ErrorResponse("No execution history to replay."); int? index = @params["index"]?.Value(); if (index == null || index < 0 || index >= _history.Count) return new ErrorResponse($"Invalid history index. Valid range: 0-{_history.Count - 1}"); var entry = _history[index.Value]; var replayParams = JObject.FromObject(new { action = ActionExecute, code = entry.code, safety_checks = entry.safetyChecksEnabled, compiler = entry.compiler ?? "auto", }); return HandleExecute(replayParams); } // ──────────────────── Compilation ──────────────────── private static object CompileAndExecute(string code, string compiler) { string wrappedSource = WrapUserCode(code); string[] assemblyPaths = GetAssemblyPaths(); Assembly compiled; string usedCompiler; switch (compiler) { case "roslyn": if (!RoslynCompiler.IsAvailable) return new ErrorResponse("Roslyn (Microsoft.CodeAnalysis) is not available. Install it via NuGet or use compiler='codedom'."); compiled = RoslynCompiler.Compile(wrappedSource, assemblyPaths, out var roslynErrors); if (compiled == null) return new ErrorResponse("Compilation failed", new { errors = OffsetErrors(roslynErrors), compiler = "roslyn" }); usedCompiler = "roslyn"; break; case "codedom": compiled = CodeDomCompile(wrappedSource, assemblyPaths, out var codedomErrors); if (compiled == null) return new ErrorResponse("Compilation failed", new { errors = OffsetErrors(codedomErrors), compiler = "codedom" }); usedCompiler = "codedom"; break; default: // "auto" if (RoslynCompiler.IsAvailable) { compiled = RoslynCompiler.Compile(wrappedSource, assemblyPaths, out var autoErrors); if (compiled == null) return new ErrorResponse("Compilation failed", new { errors = OffsetErrors(autoErrors), compiler = "roslyn" }); usedCompiler = "roslyn"; } else { compiled = CodeDomCompile(wrappedSource, assemblyPaths, out var autoFallbackErrors); if (compiled == null) return new ErrorResponse("Compilation failed", new { errors = OffsetErrors(autoFallbackErrors), compiler = "codedom" }); usedCompiler = "codedom"; } break; } return InvokeCompiled(compiled, usedCompiler); } private static object InvokeCompiled(Assembly assembly, string compilerUsed) { var type = assembly.GetType(WrapperClassName); if (type == null) return new ErrorResponse("Internal error: failed to find compiled type."); var method = type.GetMethod(WrapperMethodName, BindingFlags.Public | BindingFlags.Static); if (method == null) return new ErrorResponse("Internal error: failed to find Execute method."); object result = null; Exception executionError = null; try { result = method.Invoke(null, null); } catch (TargetInvocationException tie) { executionError = tie.InnerException ?? tie; } catch (Exception e) { executionError = e; } if (executionError != null) return new ErrorResponse($"Runtime error: {executionError.Message}", new { exceptionType = executionError.GetType().Name, stackTrace = executionError.StackTrace, compiler = compilerUsed }); if (result != null) return new SuccessResponse("Code executed successfully.", new { result = SerializeResult(result), compiler = compilerUsed }); return new SuccessResponse("Code executed successfully.", new { compiler = compilerUsed }); } private static List OffsetErrors(List errors) { // Errors already have line numbers adjusted by the compiler-specific code return errors; } // ──────────────────── CodeDom compiler ──────────────────── private static Assembly CodeDomCompile(string source, string[] assemblyPaths, out List errors) { errors = new List(); // CodeDom needs the netstandard-aware filtered paths var filtered = FilterAssemblyPathsForCodeDom(assemblyPaths); // CSharpCodeProvider turns every ReferencedAssemblies entry into a literal /r:"..." flag // on the csc command line. Projects with ~100+ asmdefs blow past Windows' 32 KB // CreateProcess argument limit and fail with "The filename or extension is too long." // Route references through a response file (@responsefile is supported by both mcs and // Roslyn csc) so we pass exactly one short argument regardless of reference count. string responseFilePath = Path.Combine(Path.GetTempPath(), $"mcp-codedom-{Guid.NewGuid():N}.rsp"); try { using (var writer = new StreamWriter(responseFilePath, append: false, Encoding.UTF8)) { foreach (var path in filtered) { writer.Write("/r:\""); writer.Write(path); writer.WriteLine("\""); } } using (var provider = new CSharpCodeProvider()) { var parameters = new CompilerParameters { GenerateInMemory = true, GenerateExecutable = false, TreatWarningsAsErrors = false, CompilerOptions = "@\"" + responseFilePath + "\"", }; var results = provider.CompileAssemblyFromSource(parameters, source); if (results.Errors.HasErrors) { foreach (CompilerError error in results.Errors) { if (!error.IsWarning) { int userLine = Math.Max(1, error.Line - WrapperLineOffset); errors.Add($"Line {userLine}: {error.ErrorText}"); } } return null; } return results.CompiledAssembly; } } finally { try { if (File.Exists(responseFilePath)) File.Delete(responseFilePath); } catch { /* best effort */ } } } // CSharpCodeProvider can't resolve type-forwarding, so when netstandard.dll is loaded // alongside mscorlib/System.Runtime/System.Collections, types like List appear in // multiple assemblies causing "type defined multiple times" errors. private static readonly HashSet _codedomDuplicateAssemblies = new HashSet(StringComparer.OrdinalIgnoreCase) { "mscorlib", "System.Runtime", "System.Private.CoreLib", "System.Collections", }; private static string[] FilterAssemblyPathsForCodeDom(string[] allPaths) { bool hasNetstandard = allPaths.Any(p => string.Equals(Path.GetFileNameWithoutExtension(p), "netstandard", StringComparison.OrdinalIgnoreCase)); if (!hasNetstandard) return allPaths; return allPaths.Where(p => !_codedomDuplicateAssemblies.Contains(Path.GetFileNameWithoutExtension(p))).ToArray(); } // ──────────────────── Shared helpers ──────────────────── private static string WrapUserCode(string code) { var sb = new StringBuilder(); sb.AppendLine("using System;"); sb.AppendLine("using System.Collections.Generic;"); sb.AppendLine("using System.Linq;"); sb.AppendLine("using System.Reflection;"); sb.AppendLine("using UnityEngine;"); sb.AppendLine("using UnityEditor;"); sb.AppendLine($"public static class {WrapperClassName}"); sb.AppendLine("{"); sb.AppendLine($" public static object {WrapperMethodName}()"); sb.AppendLine(" {"); sb.AppendLine(code); sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } private static string[] GetAssemblyPaths() { if (_cachedAssemblyPaths == null) _cachedAssemblyPaths = ResolveAssemblyPaths(); return _cachedAssemblyPaths; } private static string[] ResolveAssemblyPaths() { var paths = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var assembly in UnityAssembliesCompat.GetLoadedAssemblies()) { try { if (assembly.IsDynamic) continue; var location = assembly.Location; if (string.IsNullOrEmpty(location)) continue; if (!File.Exists(location)) continue; paths.Add(location); } catch (NotSupportedException) { // Some assemblies don't support Location property } } var result = new string[paths.Count]; paths.CopyTo(result); return result; } private static string CheckBlockedPatterns(string code) { foreach (var pattern in _blockedPatterns) { if (code.IndexOf(pattern, StringComparison.OrdinalIgnoreCase) >= 0) return $"Code contains blocked pattern: '{pattern}'. Disable safety checks with safety_checks=false if this is intentional."; } return null; } private static void AddToHistory(string code, object result, double elapsedMs, bool safetyChecks, string compiler = "auto") { string preview; if (result is SuccessResponse sr) preview = sr.Data?.ToString() ?? sr.Message; else if (result is ErrorResponse er) preview = er.Error; else preview = result?.ToString() ?? "null"; if (preview != null && preview.Length > 200) preview = preview.Substring(0, 200) + "..."; _history.Add(new HistoryEntry { code = code, success = result is SuccessResponse, resultPreview = preview, elapsedMs = Math.Round(elapsedMs, 1), timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"), safetyChecksEnabled = safetyChecks, compiler = compiler, }); while (_history.Count > MaxHistoryEntries) _history.RemoveAt(0); } private static object SerializeResult(object result) { if (result == null) return null; var type = result.GetType(); if (type.IsPrimitive || result is string || result is decimal) return result; try { return JToken.FromObject(result); } catch { return result.ToString(); } } private class HistoryEntry { public string code; public bool success; public string resultPreview; public double elapsedMs; public string timestamp; public bool safetyChecksEnabled; public string compiler; } } /// /// Roslyn compiler backend accessed entirely via reflection. /// No compile-time dependency on Microsoft.CodeAnalysis — works only if the package is installed. /// internal static class RoslynCompiler { private static bool? _isAvailable; private static Type _syntaxTreeType; private static Type _compilationType; private static Type _compilationOptionsType; private static Type _parseOptionsType; private static Type _metadataReferenceType; private static Type _outputKindEnum; private static Type _languageVersionEnum; private static MethodInfo _parseText; private static MethodInfo _createCompilation; private static MethodInfo _createFromFile; private static MethodInfo _emit; private static object _parseOptions; private static object _compilationOptions; public static bool IsAvailable { get { if (_isAvailable == null) _isAvailable = Initialize(); return _isAvailable.Value; } } public static void ResetCache() { _isAvailable = null; } private static bool Initialize() { try { _syntaxTreeType = Type.GetType("Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree, Microsoft.CodeAnalysis.CSharp"); _compilationType = Type.GetType("Microsoft.CodeAnalysis.CSharp.CSharpCompilation, Microsoft.CodeAnalysis.CSharp"); _compilationOptionsType = Type.GetType("Microsoft.CodeAnalysis.CSharp.CSharpCompilationOptions, Microsoft.CodeAnalysis.CSharp"); _parseOptionsType = Type.GetType("Microsoft.CodeAnalysis.CSharp.CSharpParseOptions, Microsoft.CodeAnalysis.CSharp"); _metadataReferenceType = Type.GetType("Microsoft.CodeAnalysis.MetadataReference, Microsoft.CodeAnalysis"); _outputKindEnum = Type.GetType("Microsoft.CodeAnalysis.OutputKind, Microsoft.CodeAnalysis"); _languageVersionEnum = Type.GetType("Microsoft.CodeAnalysis.CSharp.LanguageVersion, Microsoft.CodeAnalysis.CSharp"); if (_syntaxTreeType == null || _compilationType == null || _compilationOptionsType == null || _parseOptionsType == null || _metadataReferenceType == null || _outputKindEnum == null || _languageVersionEnum == null) return false; // CSharpSyntaxTree.ParseText(string, CSharpParseOptions, string, Encoding, CancellationToken) var syntaxTreeBase = Type.GetType("Microsoft.CodeAnalysis.SyntaxTree, Microsoft.CodeAnalysis"); _parseText = _syntaxTreeType.GetMethod("ParseText", new[] { typeof(string), _parseOptionsType, typeof(string), typeof(Encoding), typeof(System.Threading.CancellationToken) }); if (_parseText == null) return false; // CSharpCompilation.Create(string, IEnumerable, IEnumerable, CSharpCompilationOptions) var metadataRefBase = _metadataReferenceType; var syntaxTreeEnumerable = typeof(IEnumerable<>).MakeGenericType(syntaxTreeBase); var metadataRefEnumerable = typeof(IEnumerable<>).MakeGenericType(metadataRefBase); _createCompilation = _compilationType.GetMethod("Create", new[] { typeof(string), syntaxTreeEnumerable, metadataRefEnumerable, _compilationOptionsType }); if (_createCompilation == null) return false; // MetadataReference.CreateFromFile(string, MetadataReferenceProperties, DocumentationProvider) _createFromFile = _metadataReferenceType.GetMethods(BindingFlags.Public | BindingFlags.Static) .FirstOrDefault(m => m.Name == "CreateFromFile"); if (_createFromFile == null) return false; // Emit has no single-param overload; the simplest is // Emit(Stream, Stream, Stream, Stream, IEnumerable, EmitOptions, CancellationToken) var compilationBase = Type.GetType("Microsoft.CodeAnalysis.Compilation, Microsoft.CodeAnalysis"); if (compilationBase == null) return false; _emit = compilationBase.GetMethods(BindingFlags.Public | BindingFlags.Instance) .Where(m => m.Name == "Emit") .OrderBy(m => m.GetParameters().Length) .FirstOrDefault(); if (_emit == null) return false; // Build CSharpParseOptions — constructor has optional params, use reflection var latestValue = Enum.Parse(_languageVersionEnum, "Latest"); var parseOptionsCtor = _parseOptionsType.GetConstructors(BindingFlags.Public | BindingFlags.Instance)[0]; var parseCtorParams = parseOptionsCtor.GetParameters(); var parseArgs = new object[parseCtorParams.Length]; for (int i = 0; i < parseCtorParams.Length; i++) { if (parseCtorParams[i].Name == "languageVersion") parseArgs[i] = latestValue; else if (parseCtorParams[i].HasDefaultValue) parseArgs[i] = parseCtorParams[i].DefaultValue; else parseArgs[i] = null; } _parseOptions = parseOptionsCtor.Invoke(parseArgs); // Build CSharpCompilationOptions — use the first constructor (has most defaults) var dllKind = Enum.Parse(_outputKindEnum, "DynamicallyLinkedLibrary"); var compOptionsCtor = _compilationOptionsType.GetConstructors(BindingFlags.Public | BindingFlags.Instance)[0]; var compCtorParams = compOptionsCtor.GetParameters(); var compArgs = new object[compCtorParams.Length]; for (int i = 0; i < compCtorParams.Length; i++) { if (compCtorParams[i].Name == "outputKind") compArgs[i] = dllKind; else if (compCtorParams[i].HasDefaultValue) compArgs[i] = compCtorParams[i].DefaultValue; else compArgs[i] = null; } _compilationOptions = compOptionsCtor.Invoke(compArgs); return true; } catch (Exception e) { McpLog.Warn($"[ExecuteCode] Roslyn initialization failed: {e.Message}"); return false; } } public static Assembly Compile(string source, string[] assemblyPaths, out List errors) { errors = new List(); try { // Parse source var syntaxTree = _parseText.Invoke(null, new object[] { source, _parseOptions, null, null, default(System.Threading.CancellationToken) }); // Build metadata references var metadataRefBase = _metadataReferenceType; var listType = typeof(List<>).MakeGenericType(metadataRefBase); var refs = (System.Collections.IList)Activator.CreateInstance(listType); foreach (var path in assemblyPaths) { try { var cfParams = _createFromFile.GetParameters(); var cfArgs = new object[cfParams.Length]; cfArgs[0] = path; // string path for (int i = 1; i < cfParams.Length; i++) cfArgs[i] = cfParams[i].HasDefaultValue ? cfParams[i].DefaultValue : null; var metaRef = _createFromFile.Invoke(null, cfArgs); refs.Add(metaRef); } catch { // Skip assemblies that can't be loaded as metadata } } // Build syntax tree array var syntaxTreeBase = Type.GetType("Microsoft.CodeAnalysis.SyntaxTree, Microsoft.CodeAnalysis"); var treeArray = Array.CreateInstance(syntaxTreeBase, 1); treeArray.SetValue(syntaxTree, 0); // Create compilation var compilation = _createCompilation.Invoke(null, new object[] { "MCPDynamic", treeArray, refs, _compilationOptions }); // Emit to memory using (var ms = new MemoryStream()) { // Build args for the Emit overload (fill non-stream params with defaults) var emitParams = _emit.GetParameters(); var emitArgs = new object[emitParams.Length]; emitArgs[0] = ms; // peStream for (int i = 1; i < emitParams.Length; i++) { if (emitParams[i].HasDefaultValue) emitArgs[i] = emitParams[i].DefaultValue; else emitArgs[i] = null; } var emitResult = _emit.Invoke(compilation, emitArgs); // Check emitResult.Success var successProp = emitResult.GetType().GetProperty("Success"); bool success = (bool)successProp.GetValue(emitResult); if (!success) { // Read emitResult.Diagnostics var diagProp = emitResult.GetType().GetProperty("Diagnostics"); var diagnostics = (System.Collections.IEnumerable)diagProp.GetValue(emitResult); var severityError = Enum.Parse(Type.GetType("Microsoft.CodeAnalysis.DiagnosticSeverity, Microsoft.CodeAnalysis"), "Error"); foreach (var diag in diagnostics) { var sevProp = diag.GetType().GetProperty("Severity"); var severity = sevProp.GetValue(diag); if (!severity.Equals(severityError)) continue; var locProp = diag.GetType().GetProperty("Location"); var loc = locProp.GetValue(diag); var spanProp = loc.GetType().GetMethod("GetLineSpan"); var lineSpan = spanProp.Invoke(loc, null); var startProp = lineSpan.GetType().GetProperty("StartLinePosition"); var startPos = startProp.GetValue(lineSpan); var lineProp = startPos.GetType().GetProperty("Line"); int line = (int)lineProp.GetValue(startPos); var msgProp = diag.GetType().GetMethod("GetMessage", new[] { typeof(System.Globalization.CultureInfo) }); string msg = (string)msgProp.Invoke(diag, new object[] { null }); int userLine = Math.Max(1, line + 1 - ExecuteCode.WrapperLineOffset); errors.Add($"Line {userLine}: {msg}"); } return null; } ms.Seek(0, SeekOrigin.Begin); return Assembly.Load(ms.ToArray()); } } catch (Exception e) { // Walk to the deepest cause: TargetInvocationException (and friends) wrap the real // failure inside .InnerException, and reporting only e.Message hides everything // useful (e.g. a missing transitive dep manifests as the generic "Exception has been // thrown by the target of an invocation."). Exception root = e; while (root.InnerException != null) root = root.InnerException; errors.Add($"Roslyn compilation error: {root.GetType().Name}: {root.Message}"); return null; } } } }