Skip to main content
Glama

Union Unity MCP Server

by nurture-tech
ScriptService.cs13.6 kB
#if !NO_MCP using System; using System.CodeDom.Compiler; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using ModelContextProtocol; using ModelContextProtocol.Server; using UnityEditor; using UnityEngine; using Nurture.MCP.Editor; namespace Nurture.MCP.Editor.Services { [McpServerToolType] public static class ScriptService { public struct ScriptInfo { public string Guid { get; set; } public string Path { get; set; } public string Code { get; set; } } public struct MCPParameterInfo { public string Name { get; set; } public string Type { get; set; } public bool IsOptional { get; set; } public object DefaultValue { get; set; } } public struct MCPMethodInfo { public string Name { get; set; } public string ReturnType { get; set; } public List<MCPParameterInfo> Parameters { get; set; } } public struct MCPFieldInfo { public string Name { get; set; } public string Type { get; set; } public bool ReadOnly { get; set; } } public class TypeInfo { public List<MCPMethodInfo> Methods { get; set; } public List<MCPFieldInfo> Fields { get; set; } } [McpServerTool( Destructive = true, Idempotent = true, OpenWorld = true, ReadOnly = false, Title = "Create Unity Script", Name = "create_script" )] // FIXME: We instruct the agent to stop after running this tool to allow for the Domain Reload which will restart the MCP server. [Description( @"Create or replace a C# code file at the given path. This also checks to make sure the script compiles. Use this tool instead of generic file creation tools when working with Unity C# code." )] internal static Task<ScriptInfo> CreateScript( SynchronizationContext context, string code, [Description("The names of .NET assemblies to reference when compiling the script.")] List<string> assemblies, string path, CancellationToken cancellationToken, IProgress<ProgressNotificationValue> progress, [Description("Whether the script is an editor script or a play mode script.")] bool editor = false ) { return context.Run( async () => { Directory.CreateDirectory(Path.GetDirectoryName(path)); progress.Report( new ProgressNotificationValue() { Progress = 0f, Message = "Compiling script...", Total = 1.0f, } ); CompileCode(code, assemblies, editor); await File.WriteAllTextAsync(path, code); progress.Report( new ProgressNotificationValue() { Progress = 0.5f, Message = "Refreshing asset database...", Total = 1.0f, } ); AssetDatabase.Refresh(); return new ScriptInfo() { Guid = AssetDatabase.AssetPathToGUID(path), Path = path, Code = code, }; }, cancellationToken ); } [McpServerTool( Destructive = true, Idempotent = false, OpenWorld = true, ReadOnly = false, Title = "Unity Execute Code", Name = "execute_code" )] [Description(@"Execute code inside the Unity editor. Instructions: - Call the appropriate `Undo` class methods to allow undoing any modifications. - Use `PhysicsMasterial` instead of `PhysicMaterial`. - When creating a prefab, use `PrefabUtility.SaveAsPrefabAssetAndConnect` instead of `PrefabUtility.SaveAsPrefab` so that the gameobject in the current scene/prefab is connected to the new prefab.")] public static Task<UnityLoggerExtensions.WithLogResult<string>> ExecuteCode( SynchronizationContext context, [Description( @"The C# class to compile and execute which follows the format: using UnityEngine; using UnityEditor; {{ using }} public class CodeExecutor { public static string Execute() { {{ body }} return {{ result }}; } } " )] string code, [Description("The names of .NET assemblies to reference when compiling the class.")] List<string> assemblies, CancellationToken cancellationToken, IProgress<ProgressNotificationValue> progress ) { return context.Run( async () => { await EditorExtensions.EnsureNotPlaying(progress, cancellationToken, 0.1f); try { EditorApplication.LockReloadAssemblies(); // Create a method that wraps the code progress.Report( new ProgressNotificationValue() { Progress = 0.5f, Message = "Compiling script...", Total = 1.0f, } ); var assembly = CompileCode(code, assemblies, true); var type = assembly.GetType("CodeExecutor"); var method = type.GetMethod("Execute"); progress.Report( new ProgressNotificationValue() { Progress = 0.75f, Message = "Executing script...", Total = 1.0f, } ); try { await EditorExtensions.FocusSceneView(cancellationToken); var result = await UnityLoggerExtensions.WithLogs( () => Task.FromResult( method.Invoke(null, new object[] { }) as string ), // Don't log stack traces since they'll be useless (no backing file) false ); await EditorExtensions.FocusSceneView(cancellationToken); return result; } catch (Exception e) { throw new McpException( $"Script execution failed: {e.InnerException?.Message}\n{e.InnerException?.StackTrace}", e ); } } finally { EditorApplication.UnlockReloadAssemblies(); } }, cancellationToken ); } private static Assembly CompileCode( string wrappedCode, List<string> assemblies, bool editor ) { // Use Mono's built-in compiler var options = new CompilerParameters { GenerateInMemory = true }; if (editor) { options.CompilerOptions = "-define:UNITY_EDITOR"; } if (Settings.Instance.AlwaysIncludedAssemblies != null) { assemblies.AddRange(Settings.Instance.AlwaysIncludedAssemblies); } assemblies.AddRange( new List<string> { "netstandard", "System.Core", "UnityEngine", "UnityEngine.CoreModule", "UnityEngine.UIModule", "UnityEngine.PhysicsModule", "UnityEngine.AnimationModule", "UnityEngine.AudioModule", "UnityEngine.DirectorModule", "UnityEngine.ParticleSystemModule", "Assembly-CSharp", } ); if (editor) { assemblies.AddRange( new List<string> { "UnityEditor", "UnityEditor.CoreModule", "ModelContextProtocol", } ); } foreach (var assemblyName in assemblies) { var assembly = TypeExtensions.FindAssembly(assemblyName); if (assembly == null) { Debug.LogWarning($"[MCP] Assembly {assemblyName} not found"); continue; } if (!options.ReferencedAssemblies.Contains(assembly.Location)) { options.ReferencedAssemblies.Add(assembly.Location); } } // Compile and execute using var provider = new Microsoft.CSharp.CSharpCodeProvider(); var results = provider.CompileAssemblyFromSource(options, wrappedCode); if (results.Errors.HasErrors) { var errors = string.Join( "\n", results .Errors.Cast<CompilerError>() .Where(e => !e.IsWarning) .Select(e => $"Line {e.Line}: [{e.ErrorNumber}] {e.ErrorText}") ); throw new McpException($"Compilation failed:\n{errors}"); } return results.CompiledAssembly; } [McpServerTool( Destructive = false, Idempotent = true, OpenWorld = false, ReadOnly = true, Title = "Unity Get Type Info", Name = "get_type_info" )] [Description( "Get public fields and methods on a Unity fully qualified type name, including the assembly. This is primarily useful for correcting errors when calling the `execute_code` tool." )] public static async Task<TypeInfo> GetTypeInfo( SynchronizationContext context, string typeName, string assemblyName, CancellationToken cancellationToken ) { return await context.Run( () => { var assembly = TypeExtensions.FindAssembly(assemblyName) ?? throw new McpException($"Assembly {assemblyName} not found"); var type = assembly.GetType(typeName) ?? throw new McpException($"Type {typeName} not found"); return new TypeInfo() { Methods = type.GetMethods() .Select(m => new MCPMethodInfo() { Name = m.Name, ReturnType = m.ReturnType.AssemblyQualifiedName, Parameters = m.GetParameters() .Select(p => new MCPParameterInfo() { Name = p.Name, Type = p.ParameterType.AssemblyQualifiedName, IsOptional = p.IsOptional, DefaultValue = p.DefaultValue, }) .ToList(), }) .ToList(), Fields = type.GetFields() .Select(f => new MCPFieldInfo() { Name = f.Name, Type = f.FieldType.AssemblyQualifiedName, ReadOnly = f.IsInitOnly, }) .ToList(), }; }, cancellationToken ); } } } #endif

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/nurture-tech/unity-mcp-server'

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