Skip to main content
Glama

MCP Server for Unity

by zabaglione
UnityHttpServer.cs52.6 kB
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Threading; using UnityEngine; using UnityEditor; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace UnityMCP { [InitializeOnLoad] public static class UnityMCPInstaller { static UnityMCPInstaller() { CheckAndUpdateScripts(); } static void CheckAndUpdateScripts() { var installedVersion = EditorPrefs.GetString(UnityHttpServer.VERSION_META_KEY, "0.0.0"); if (installedVersion != UnityHttpServer.SCRIPT_VERSION) { Debug.Log($"[UnityMCP] Updating Unity MCP scripts from version {installedVersion} to {UnityHttpServer.SCRIPT_VERSION}"); // Version update logic will be handled by the MCP server EditorPrefs.SetString(UnityHttpServer.VERSION_META_KEY, UnityHttpServer.SCRIPT_VERSION); } } } /// <summary> /// Simple HTTP server for Unity MCP integration /// </summary> public static class UnityHttpServer { // Version information for auto-update public const string SCRIPT_VERSION = "1.1.0"; public const string VERSION_META_KEY = "UnityMCP.InstalledVersion"; // Configuration constants private const int DEFAULT_PORT = 23457; private const int REQUEST_TIMEOUT_MS = 120000; // 2 minutes private const int THREAD_JOIN_TIMEOUT_MS = 1000; // 1 second private const int ASSET_REFRESH_DELAY_MS = 500; // Wait after asset operations public const string SERVER_LOG_PREFIX = "[UnityMCP]"; private const string PREFS_PORT_KEY = "UnityMCP.ServerPort"; private const string PREFS_PORT_BEFORE_PLAY_KEY = "UnityMCP.ServerPortBeforePlay"; // File path constants private const string ASSETS_PREFIX = "Assets/"; private const int ASSETS_PREFIX_LENGTH = 7; private const string DEFAULT_SCRIPTS_FOLDER = "Assets/Scripts"; private const string DEFAULT_SHADERS_FOLDER = "Assets/Shaders"; private const string CS_EXTENSION = ".cs"; private const string SHADER_EXTENSION = ".shader"; private static HttpListener httpListener; private static Thread listenerThread; private static bool isRunning = false; // Request queue for serialization private static readonly Queue<Action> requestQueue = new Queue<Action>(); private static bool isProcessingRequest = false; private static int currentPort = DEFAULT_PORT; /// <summary> /// Gets whether the server is currently running /// </summary> public static bool IsRunning => isRunning; /// <summary> /// Gets the current port the server is running on /// </summary> public static int CurrentPort => currentPort; [InitializeOnLoad] static class AutoShutdown { static AutoShutdown() { EditorApplication.playModeStateChanged += OnPlayModeChanged; EditorApplication.quitting += Shutdown; // Handle script recompilation UnityEditor.Compilation.CompilationPipeline.compilationStarted += OnCompilationStarted; UnityEditor.Compilation.CompilationPipeline.compilationFinished += OnCompilationFinished; // Auto-start server on Unity startup EditorApplication.delayCall += () => { if (!isRunning) { var savedPort = EditorPrefs.GetInt(PREFS_PORT_KEY, DEFAULT_PORT); Debug.Log($"{SERVER_LOG_PREFIX} Auto-starting server on port {savedPort}"); Start(savedPort); } }; } static void OnCompilationStarted(object obj) { Debug.Log($"{SERVER_LOG_PREFIX} Compilation started - stopping server"); if (isRunning) { Shutdown(); } } static void OnCompilationFinished(object obj) { Debug.Log($"{SERVER_LOG_PREFIX} Compilation finished - auto-restarting server"); // Always auto-restart after compilation var savedPort = EditorPrefs.GetInt(PREFS_PORT_KEY, DEFAULT_PORT); EditorApplication.delayCall += () => Start(savedPort); } } /// <summary> /// Start the HTTP server on the specified port /// </summary> /// <param name="port">Port to listen on</param> public static void Start(int port = DEFAULT_PORT) { if (isRunning) { Debug.LogWarning($"{SERVER_LOG_PREFIX} Server is already running. Stop it first."); return; } currentPort = port; try { httpListener = new HttpListener(); httpListener.Prefixes.Add($"http://localhost:{currentPort}/"); httpListener.Start(); isRunning = true; listenerThread = new Thread(ListenLoop) { IsBackground = true, Name = "UnityMCPHttpListener" }; listenerThread.Start(); Debug.Log($"{SERVER_LOG_PREFIX} HTTP Server started on port {currentPort}"); } catch (Exception e) { isRunning = false; Debug.LogError($"{SERVER_LOG_PREFIX} Failed to start HTTP server: {e.Message}"); throw; } } /// <summary> /// Stop the HTTP server /// </summary> public static void Shutdown() { if (!isRunning) { Debug.LogWarning($"{SERVER_LOG_PREFIX} Server is not running."); return; } isRunning = false; try { httpListener?.Stop(); httpListener?.Close(); listenerThread?.Join(THREAD_JOIN_TIMEOUT_MS); Debug.Log($"{SERVER_LOG_PREFIX} HTTP Server stopped"); } catch (Exception e) { Debug.LogError($"{SERVER_LOG_PREFIX} Error during shutdown: {e.Message}"); } finally { httpListener = null; listenerThread = null; } } static void OnPlayModeChanged(PlayModeStateChange state) { // Stop server when entering play mode to avoid conflicts if (state == PlayModeStateChange.ExitingEditMode) { if (isRunning) { Debug.Log($"{SERVER_LOG_PREFIX} Stopping server due to play mode change"); EditorPrefs.SetInt(PREFS_PORT_BEFORE_PLAY_KEY, currentPort); Shutdown(); } } // Restart server when returning to edit mode else if (state == PlayModeStateChange.EnteredEditMode) { var savedPort = EditorPrefs.GetInt(PREFS_PORT_BEFORE_PLAY_KEY, DEFAULT_PORT); Debug.Log($"{SERVER_LOG_PREFIX} Restarting server after play mode on port {savedPort}"); EditorApplication.delayCall += () => Start(savedPort); } } static void ListenLoop() { while (isRunning) { try { var context = httpListener.GetContext(); ThreadPool.QueueUserWorkItem(_ => HandleRequest(context)); } catch (Exception e) { if (isRunning) Debug.LogError($"{SERVER_LOG_PREFIX} Listen error: {e.Message}"); } } } static void HandleRequest(HttpListenerContext context) { var request = context.Request; var response = context.Response; response.Headers.Add("Access-Control-Allow-Origin", "*"); try { if (request.HttpMethod != "POST") { SendResponse(response, 405, false, null, "Method not allowed"); return; } string requestBody; // Force UTF-8 encoding for request body using (var reader = new StreamReader(request.InputStream, Encoding.UTF8)) { requestBody = reader.ReadToEnd(); } var requestData = JObject.Parse(requestBody); var method = requestData["method"]?.ToString(); if (string.IsNullOrEmpty(method)) { SendResponse(response, 400, false, null, "Method is required"); return; } Debug.Log($"{SERVER_LOG_PREFIX} Processing request: {method}"); // Check if this request requires main thread bool requiresMainThread = RequiresMainThread(method); if (!requiresMainThread) { // Process directly on worker thread try { var result = ProcessRequestOnWorkerThread(method, requestData); SendResponse(response, 200, true, result, null); } catch (Exception e) { var statusCode = e is ArgumentException ? 400 : 500; SendResponse(response, statusCode, false, null, e.Message); } } else { // Execute on main thread for Unity API calls object result = null; Exception error = null; var resetEvent = new ManualResetEvent(false); EditorApplication.delayCall += () => { try { Debug.Log($"{SERVER_LOG_PREFIX} Processing on main thread: {method}"); result = ProcessRequest(method, requestData); Debug.Log($"{SERVER_LOG_PREFIX} Completed processing: {method}"); } catch (Exception e) { error = e; Debug.LogError($"{SERVER_LOG_PREFIX} Error processing {method}: {e.Message}"); } finally { resetEvent.Set(); } }; if (!resetEvent.WaitOne(REQUEST_TIMEOUT_MS)) { SendResponse(response, 504, false, null, "Request timeout - Unity may be busy or unfocused"); return; } if (error != null) { var statusCode = error is ArgumentException ? 400 : 500; SendResponse(response, statusCode, false, null, error.Message); return; } SendResponse(response, 200, true, result, null); } } catch (Exception e) { SendResponse(response, 400, false, null, $"Bad request: {e.Message}"); } } static bool RequiresMainThread(string method) { // These methods can run on worker thread switch (method) { case "ping": case "script/read": case "shader/read": return false; // project/info now requires Unity API for render pipeline detection // Creating, deleting files require Unity API (AssetDatabase) default: return true; } } static object ProcessRequestOnWorkerThread(string method, JObject request) { switch (method) { case "ping": return new { status = "ok", time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") }; case "project/info": // project/info requires Unity API for render pipeline detection throw new NotImplementedException("project/info requires main thread for render pipeline detection"); case "script/read": return ReadScriptOnWorkerThread(request); case "shader/read": return ReadShaderOnWorkerThread(request); // Folder operations (can run on worker thread) case "folder/create": return CreateFolderOnWorkerThread(request); case "folder/rename": return RenameFolderOnWorkerThread(request); case "folder/move": return MoveFolderOnWorkerThread(request); case "folder/delete": return DeleteFolderOnWorkerThread(request); case "folder/list": return ListFolderOnWorkerThread(request); default: throw new NotImplementedException($"Method not implemented for worker thread: {method}"); } } static object ReadScriptOnWorkerThread(JObject request) { var path = request["path"]?.ToString(); if (string.IsNullOrEmpty(path)) throw new ArgumentException("path is required"); var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); if (!File.Exists(fullPath)) throw new FileNotFoundException($"File not found: {path}"); return new { path = path, content = File.ReadAllText(fullPath, new UTF8Encoding(true)), guid = "" // GUID requires AssetDatabase, skip in worker thread }; } static object ReadShaderOnWorkerThread(JObject request) { var path = request["path"]?.ToString(); if (string.IsNullOrEmpty(path)) throw new ArgumentException("path is required"); var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); if (!File.Exists(fullPath)) throw new FileNotFoundException($"File not found: {path}"); return new { path = path, content = File.ReadAllText(fullPath, new UTF8Encoding(true)), guid = "" // GUID requires AssetDatabase, skip in worker thread }; } static object ProcessRequest(string method, JObject request) { switch (method) { case "ping": return new { status = "ok", time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") }; // Script operations case "script/create": return CreateScript(request); case "script/read": return ReadScript(request); case "script/delete": return DeleteScript(request); case "script/applyDiff": return ApplyDiff(request); // Shader operations case "shader/create": return CreateShader(request); case "shader/read": return ReadShader(request); case "shader/delete": return DeleteShader(request); // Project operations case "project/info": return GetProjectInfo(); // Folder operations case "folder/create": return CreateFolder(request); case "folder/rename": return RenameFolder(request); case "folder/move": return MoveFolder(request); case "folder/delete": return DeleteFolder(request); case "folder/list": return ListFolder(request); default: throw new NotImplementedException($"Method not found: {method}"); } } static object CreateScript(JObject request) { var fileName = request["fileName"]?.ToString(); if (string.IsNullOrEmpty(fileName)) throw new ArgumentException("fileName is required"); if (!fileName.EndsWith(CS_EXTENSION)) fileName += CS_EXTENSION; var content = request["content"]?.ToString(); var folder = request["folder"]?.ToString() ?? DEFAULT_SCRIPTS_FOLDER; var path = Path.Combine(folder, fileName); var directory = Path.GetDirectoryName(path); // Create directory if needed if (!AssetDatabase.IsValidFolder(directory)) { CreateFolderRecursive(directory); } // Use Unity-safe file creation approach var scriptContent = content ?? GetDefaultScriptContent(fileName); // First, ensure the asset doesn't already exist if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path) != null) { throw new InvalidOperationException($"Asset already exists: {path}"); } // Write file using UTF-8 with BOM (Unity standard) var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); var utf8WithBom = new UTF8Encoding(true); File.WriteAllText(fullPath, scriptContent, utf8WithBom); // Import the asset immediately and wait for completion AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate); // Verify the asset was imported successfully var attempts = 0; const int maxAttempts = 10; while (AssetDatabase.AssetPathToGUID(path) == "" && attempts < maxAttempts) { System.Threading.Thread.Sleep(100); AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); attempts++; } if (AssetDatabase.AssetPathToGUID(path) == "") { throw new InvalidOperationException($"Failed to import asset: {path}"); } return new { path = path, guid = AssetDatabase.AssetPathToGUID(path) }; } static object ReadScript(JObject request) { var path = request["path"]?.ToString(); if (string.IsNullOrEmpty(path)) throw new ArgumentException("path is required"); var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); if (!File.Exists(fullPath)) throw new FileNotFoundException($"File not found: {path}"); return new { path = path, content = File.ReadAllText(fullPath, new UTF8Encoding(true)), guid = AssetDatabase.AssetPathToGUID(path) }; } static object DeleteScript(JObject request) { var path = request["path"]?.ToString(); if (string.IsNullOrEmpty(path)) throw new ArgumentException("path is required"); // Verify file exists before deletion var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); if (!File.Exists(fullPath)) throw new FileNotFoundException($"File not found: {path}"); // Delete using AssetDatabase if (!AssetDatabase.DeleteAsset(path)) throw new InvalidOperationException($"Failed to delete: {path}"); // Force immediate refresh AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); // Wait for asset database to process deletion System.Threading.Thread.Sleep(ASSET_REFRESH_DELAY_MS); return new { message = "Script deleted successfully" }; } static object ApplyDiff(JObject request) { var path = request["path"]?.ToString(); var diff = request["diff"]?.ToString(); var options = request["options"] as JObject; if (string.IsNullOrEmpty(path)) throw new ArgumentException("path is required"); if (string.IsNullOrEmpty(diff)) throw new ArgumentException("diff is required"); var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); if (!File.Exists(fullPath)) throw new FileNotFoundException($"File not found: {path}"); var dryRun = options?["dryRun"]?.Value<bool>() ?? false; // Read current content using UTF-8 with BOM (Unity standard) var utf8WithBom = new UTF8Encoding(true); var originalContent = File.ReadAllText(fullPath, utf8WithBom); var lines = originalContent.Split('\n').ToList(); // Parse and apply unified diff var diffLines = diff.Split('\n'); var linesAdded = 0; var linesRemoved = 0; var currentLine = 0; for (int i = 0; i < diffLines.Length; i++) { var line = diffLines[i]; if (line.StartsWith("@@")) { // Parse hunk header: @@ -l,s +l,s @@ var match = System.Text.RegularExpressions.Regex.Match(line, @"@@ -(\d+),?\d* \+(\d+),?\d* @@"); if (match.Success) { currentLine = int.Parse(match.Groups[1].Value) - 1; } } else if (line.StartsWith("-") && !line.StartsWith("---")) { // Remove line if (currentLine < lines.Count) { lines.RemoveAt(currentLine); linesRemoved++; } } else if (line.StartsWith("+") && !line.StartsWith("+++")) { // Add line lines.Insert(currentLine, line.Substring(1)); currentLine++; linesAdded++; } else if (line.StartsWith(" ")) { // Context line currentLine++; } } // Write result if not dry run if (!dryRun) { var updatedContent = string.Join("\n", lines); // Write with UTF-8 with BOM (Unity standard) File.WriteAllText(fullPath, updatedContent, utf8WithBom); AssetDatabase.Refresh(); // Wait for asset database to process System.Threading.Thread.Sleep(ASSET_REFRESH_DELAY_MS); } return new { path = path, linesAdded = linesAdded, linesRemoved = linesRemoved, dryRun = dryRun, guid = AssetDatabase.AssetPathToGUID(path) }; } static object CreateShader(JObject request) { var name = request["name"]?.ToString(); if (string.IsNullOrEmpty(name)) throw new ArgumentException("name is required"); if (!name.EndsWith(SHADER_EXTENSION)) name += SHADER_EXTENSION; var content = request["content"]?.ToString(); var folder = request["folder"]?.ToString() ?? DEFAULT_SHADERS_FOLDER; var path = Path.Combine(folder, name); var directory = Path.GetDirectoryName(path); if (!AssetDatabase.IsValidFolder(directory)) { CreateFolderRecursive(directory); } // Use Unity-safe file creation approach var shaderContent = content ?? GetDefaultShaderContent(name); // First, ensure the asset doesn't already exist if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path) != null) { throw new InvalidOperationException($"Asset already exists: {path}"); } // Write file using UTF-8 with BOM (Unity standard) var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); var utf8WithBom = new UTF8Encoding(true); File.WriteAllText(fullPath, shaderContent, utf8WithBom); // Import the asset immediately and wait for completion AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate); // Verify the asset was imported successfully var attempts = 0; const int maxAttempts = 10; while (AssetDatabase.AssetPathToGUID(path) == "" && attempts < maxAttempts) { System.Threading.Thread.Sleep(100); AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); attempts++; } if (AssetDatabase.AssetPathToGUID(path) == "") { throw new InvalidOperationException($"Failed to import asset: {path}"); } return new { path = path, guid = AssetDatabase.AssetPathToGUID(path) }; } static object ReadShader(JObject request) { var path = request["path"]?.ToString(); if (string.IsNullOrEmpty(path)) throw new ArgumentException("path is required"); var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); if (!File.Exists(fullPath)) throw new FileNotFoundException($"File not found: {path}"); return new { path = path, content = File.ReadAllText(fullPath, new UTF8Encoding(true)), guid = AssetDatabase.AssetPathToGUID(path) }; } static object DeleteShader(JObject request) { var path = request["path"]?.ToString(); if (string.IsNullOrEmpty(path)) throw new ArgumentException("path is required"); if (!AssetDatabase.DeleteAsset(path)) throw new InvalidOperationException($"Failed to delete: {path}"); // Wait for asset database to process deletion System.Threading.Thread.Sleep(ASSET_REFRESH_DELAY_MS); return new { message = "Shader deleted successfully" }; } static object GetProjectInfo() { // Detect render pipeline with multiple methods string renderPipeline = "Built-in"; string renderPipelineVersion = "N/A"; string detectionMethod = "Default"; try { // Method 1: Check GraphicsSettings.renderPipelineAsset var renderPipelineAsset = UnityEngine.Rendering.GraphicsSettings.renderPipelineAsset; Debug.Log($"{SERVER_LOG_PREFIX} RenderPipelineAsset: {(renderPipelineAsset != null ? renderPipelineAsset.GetType().FullName : "null")}"); if (renderPipelineAsset != null) { var assetType = renderPipelineAsset.GetType(); var typeName = assetType.Name; var fullTypeName = assetType.FullName; Debug.Log($"{SERVER_LOG_PREFIX} Asset type: {typeName}, Full type: {fullTypeName}"); if (fullTypeName.Contains("Universal") || typeName.Contains("Universal") || fullTypeName.Contains("URP") || typeName.Contains("URP")) { renderPipeline = "URP"; detectionMethod = "GraphicsSettings.renderPipelineAsset"; } else if (fullTypeName.Contains("HighDefinition") || typeName.Contains("HighDefinition") || fullTypeName.Contains("HDRP") || typeName.Contains("HDRP")) { renderPipeline = "HDRP"; detectionMethod = "GraphicsSettings.renderPipelineAsset"; } else { renderPipeline = $"Custom ({typeName})"; detectionMethod = "GraphicsSettings.renderPipelineAsset"; } } else { // Method 2: Check for installed packages if no render pipeline asset Debug.Log($"{SERVER_LOG_PREFIX} No render pipeline asset found, checking packages..."); try { var urpPackage = UnityEditor.PackageManager.PackageInfo.FindForPackageName("com.unity.render-pipelines.universal"); var hdrpPackage = UnityEditor.PackageManager.PackageInfo.FindForPackageName("com.unity.render-pipelines.high-definition"); if (urpPackage != null) { renderPipeline = "URP (Package Available)"; renderPipelineVersion = urpPackage.version; detectionMethod = "Package Detection"; } else if (hdrpPackage != null) { renderPipeline = "HDRP (Package Available)"; renderPipelineVersion = hdrpPackage.version; detectionMethod = "Package Detection"; } else { renderPipeline = "Built-in"; detectionMethod = "No SRP packages found"; } } catch (System.Exception ex) { Debug.LogWarning($"{SERVER_LOG_PREFIX} Package detection failed: {ex.Message}"); renderPipeline = "Built-in (Package detection failed)"; detectionMethod = "Package detection error"; } } // Try to get version info if not already obtained if (renderPipelineVersion == "N/A" && renderPipeline.StartsWith("URP")) { try { var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForPackageName("com.unity.render-pipelines.universal"); if (packageInfo != null) { renderPipelineVersion = packageInfo.version; } } catch (System.Exception ex) { Debug.LogWarning($"{SERVER_LOG_PREFIX} URP version detection failed: {ex.Message}"); renderPipelineVersion = "Version unknown"; } } else if (renderPipelineVersion == "N/A" && renderPipeline.StartsWith("HDRP")) { try { var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForPackageName("com.unity.render-pipelines.high-definition"); if (packageInfo != null) { renderPipelineVersion = packageInfo.version; } } catch (System.Exception ex) { Debug.LogWarning($"{SERVER_LOG_PREFIX} HDRP version detection failed: {ex.Message}"); renderPipelineVersion = "Version unknown"; } } Debug.Log($"{SERVER_LOG_PREFIX} Detected render pipeline: {renderPipeline} (v{renderPipelineVersion}) via {detectionMethod}"); } catch (System.Exception ex) { Debug.LogError($"{SERVER_LOG_PREFIX} Render pipeline detection failed: {ex.Message}"); renderPipeline = "Detection Failed"; detectionMethod = "Exception occurred"; } return new { projectPath = Application.dataPath.Replace("/Assets", ""), projectName = Application.productName, unityVersion = Application.unityVersion, platform = Application.platform.ToString(), isPlaying = Application.isPlaying, renderPipeline = renderPipeline, renderPipelineVersion = renderPipelineVersion, detectionMethod = detectionMethod }; } static void CreateFolderRecursive(string path) { var folders = path.Split('/'); var currentPath = folders[0]; for (int i = 1; i < folders.Length; i++) { var newPath = currentPath + "/" + folders[i]; if (!AssetDatabase.IsValidFolder(newPath)) { AssetDatabase.CreateFolder(currentPath, folders[i]); } currentPath = newPath; } } static string GetDefaultScriptContent(string fileName) { var className = Path.GetFileNameWithoutExtension(fileName); return "using UnityEngine;\n\n" + $"public class {className} : MonoBehaviour\n" + "{\n" + " void Start()\n" + " {\n" + " \n" + " }\n" + " \n" + " void Update()\n" + " {\n" + " \n" + " }\n" + "}"; } static string GetDefaultShaderContent(string fileName) { var shaderName = Path.GetFileNameWithoutExtension(fileName); return $"Shader \"Custom/{shaderName}\"\n" + "{\n" + " Properties\n" + " {\n" + " _MainTex (\"Texture\", 2D) = \"white\" {}\n" + " }\n" + " SubShader\n" + " {\n" + " Tags { \"RenderType\"=\"Opaque\" }\n" + " LOD 200\n" + "\n" + " CGPROGRAM\n" + " #pragma surface surf Standard fullforwardshadows\n" + "\n" + " sampler2D _MainTex;\n" + "\n" + " struct Input\n" + " {\n" + " float2 uv_MainTex;\n" + " };\n" + "\n" + " void surf (Input IN, inout SurfaceOutputStandard o)\n" + " {\n" + " fixed4 c = tex2D (_MainTex, IN.uv_MainTex);\n" + " o.Albedo = c.rgb;\n" + " o.Alpha = c.a;\n" + " }\n" + " ENDCG\n" + " }\n" + " FallBack \"Diffuse\"\n" + "}"; } // Folder operations static object CreateFolder(JObject request) { var path = request["path"]?.ToString(); if (string.IsNullOrEmpty(path)) throw new ArgumentException("path is required"); if (!path.StartsWith(ASSETS_PREFIX)) path = Path.Combine(DEFAULT_SCRIPTS_FOLDER, path); // Use Unity-safe folder creation if (AssetDatabase.IsValidFolder(path)) { throw new InvalidOperationException($"Folder already exists: {path}"); } // Create directory structure properly var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); Directory.CreateDirectory(fullPath); // Import the folder immediately AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport); // Verify the folder was imported successfully var attempts = 0; const int maxAttempts = 10; while (!AssetDatabase.IsValidFolder(path) && attempts < maxAttempts) { System.Threading.Thread.Sleep(100); AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); attempts++; } if (!AssetDatabase.IsValidFolder(path)) { throw new InvalidOperationException($"Failed to import folder: {path}"); } return new { path = path, guid = AssetDatabase.AssetPathToGUID(path) }; } static object CreateFolderOnWorkerThread(JObject request) { var path = request["path"]?.ToString(); if (string.IsNullOrEmpty(path)) throw new ArgumentException("path is required"); if (!path.StartsWith(ASSETS_PREFIX)) path = Path.Combine(DEFAULT_SCRIPTS_FOLDER, path); var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); Directory.CreateDirectory(fullPath); return new { path = path, guid = "" // GUID requires AssetDatabase }; } static object RenameFolder(JObject request) { var oldPath = request["oldPath"]?.ToString(); var newName = request["newName"]?.ToString(); if (string.IsNullOrEmpty(oldPath)) throw new ArgumentException("oldPath is required"); if (string.IsNullOrEmpty(newName)) throw new ArgumentException("newName is required"); var error = AssetDatabase.RenameAsset(oldPath, newName); if (!string.IsNullOrEmpty(error)) throw new InvalidOperationException(error); // Wait for asset database to process System.Threading.Thread.Sleep(ASSET_REFRESH_DELAY_MS); var newPath = Path.Combine(Path.GetDirectoryName(oldPath), newName); return new { oldPath = oldPath, newPath = newPath, guid = AssetDatabase.AssetPathToGUID(newPath) }; } static object RenameFolderOnWorkerThread(JObject request) { var oldPath = request["oldPath"]?.ToString(); var newName = request["newName"]?.ToString(); if (string.IsNullOrEmpty(oldPath)) throw new ArgumentException("oldPath is required"); if (string.IsNullOrEmpty(newName)) throw new ArgumentException("newName is required"); var oldFullPath = Path.Combine(Application.dataPath, oldPath.Substring(ASSETS_PREFIX_LENGTH)); var parentDir = Path.GetDirectoryName(oldFullPath); var newFullPath = Path.Combine(parentDir, newName); if (!Directory.Exists(oldFullPath)) throw new DirectoryNotFoundException($"Directory not found: {oldPath}"); Directory.Move(oldFullPath, newFullPath); var newPath = Path.Combine(Path.GetDirectoryName(oldPath), newName); return new { oldPath = oldPath, newPath = newPath, guid = "" // GUID requires AssetDatabase }; } static object MoveFolder(JObject request) { var sourcePath = request["sourcePath"]?.ToString(); var targetPath = request["targetPath"]?.ToString(); if (string.IsNullOrEmpty(sourcePath)) throw new ArgumentException("sourcePath is required"); if (string.IsNullOrEmpty(targetPath)) throw new ArgumentException("targetPath is required"); var error = AssetDatabase.MoveAsset(sourcePath, targetPath); if (!string.IsNullOrEmpty(error)) throw new InvalidOperationException(error); // Wait for asset database to process System.Threading.Thread.Sleep(ASSET_REFRESH_DELAY_MS); return new { sourcePath = sourcePath, targetPath = targetPath, guid = AssetDatabase.AssetPathToGUID(targetPath) }; } static object MoveFolderOnWorkerThread(JObject request) { var sourcePath = request["sourcePath"]?.ToString(); var targetPath = request["targetPath"]?.ToString(); if (string.IsNullOrEmpty(sourcePath)) throw new ArgumentException("sourcePath is required"); if (string.IsNullOrEmpty(targetPath)) throw new ArgumentException("targetPath is required"); var sourceFullPath = Path.Combine(Application.dataPath, sourcePath.Substring(ASSETS_PREFIX_LENGTH)); var targetFullPath = Path.Combine(Application.dataPath, targetPath.Substring(ASSETS_PREFIX_LENGTH)); if (!Directory.Exists(sourceFullPath)) throw new DirectoryNotFoundException($"Directory not found: {sourcePath}"); // Ensure target parent directory exists var targetParent = Path.GetDirectoryName(targetFullPath); if (!Directory.Exists(targetParent)) Directory.CreateDirectory(targetParent); Directory.Move(sourceFullPath, targetFullPath); return new { sourcePath = sourcePath, targetPath = targetPath, guid = "" // GUID requires AssetDatabase }; } static object DeleteFolder(JObject request) { var path = request["path"]?.ToString(); var recursive = request["recursive"]?.Value<bool>() ?? true; if (string.IsNullOrEmpty(path)) throw new ArgumentException("path is required"); var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); if (!Directory.Exists(fullPath)) throw new DirectoryNotFoundException($"Directory not found: {path}"); if (!AssetDatabase.DeleteAsset(path)) throw new InvalidOperationException($"Failed to delete folder: {path}"); // Wait for asset database to process deletion System.Threading.Thread.Sleep(ASSET_REFRESH_DELAY_MS); return new { path = path }; } static object DeleteFolderOnWorkerThread(JObject request) { var path = request["path"]?.ToString(); var recursive = request["recursive"]?.Value<bool>() ?? true; if (string.IsNullOrEmpty(path)) throw new ArgumentException("path is required"); var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); if (!Directory.Exists(fullPath)) throw new DirectoryNotFoundException($"Directory not found: {path}"); Directory.Delete(fullPath, recursive); // Also delete .meta file var metaPath = fullPath + ".meta"; if (File.Exists(metaPath)) File.Delete(metaPath); return new { path = path }; } static object ListFolder(JObject request) { var path = request["path"]?.ToString() ?? ASSETS_PREFIX; var recursive = request["recursive"]?.Value<bool>() ?? false; var fullPath = Path.Combine(Application.dataPath, path.StartsWith(ASSETS_PREFIX) ? path.Substring(ASSETS_PREFIX_LENGTH) : path); if (!Directory.Exists(fullPath)) throw new DirectoryNotFoundException($"Directory not found: {path}"); var entries = new List<object>(); // Get directories var dirs = Directory.GetDirectories(fullPath, "*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); foreach (var dir in dirs) { var relativePath = ASSETS_PREFIX + GetRelativePath(Application.dataPath, dir); entries.Add(new { path = relativePath, name = Path.GetFileName(dir), type = "folder", guid = AssetDatabase.AssetPathToGUID(relativePath) }); } // Get files var files = Directory.GetFiles(fullPath, "*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) .Where(f => !f.EndsWith(".meta")); foreach (var file in files) { var relativePath = ASSETS_PREFIX + GetRelativePath(Application.dataPath, file); entries.Add(new { path = relativePath, name = Path.GetFileName(file), type = "file", extension = Path.GetExtension(file), guid = AssetDatabase.AssetPathToGUID(relativePath) }); } return new { path = path, entries = entries }; } static object ListFolderOnWorkerThread(JObject request) { var path = request["path"]?.ToString() ?? ASSETS_PREFIX; var recursive = request["recursive"]?.Value<bool>() ?? false; var fullPath = Path.Combine(Application.dataPath, path.StartsWith(ASSETS_PREFIX) ? path.Substring(ASSETS_PREFIX_LENGTH) : path); if (!Directory.Exists(fullPath)) throw new DirectoryNotFoundException($"Directory not found: {path}"); var entries = new List<object>(); // Get directories var dirs = Directory.GetDirectories(fullPath, "*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); foreach (var dir in dirs) { var relativePath = ASSETS_PREFIX + GetRelativePath(Application.dataPath, dir); entries.Add(new { path = relativePath, name = Path.GetFileName(dir), type = "folder", guid = "" // GUID requires AssetDatabase }); } // Get files var files = Directory.GetFiles(fullPath, "*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) .Where(f => !f.EndsWith(".meta")); foreach (var file in files) { var relativePath = ASSETS_PREFIX + GetRelativePath(Application.dataPath, file); entries.Add(new { path = relativePath, name = Path.GetFileName(file), type = "file", extension = Path.GetExtension(file), guid = "" // GUID requires AssetDatabase }); } return new { path = path, entries = entries }; } static string GetRelativePath(string basePath, string fullPath) { if (!fullPath.StartsWith(basePath)) return fullPath; var relativePath = fullPath.Substring(basePath.Length); if (relativePath.StartsWith(Path.DirectorySeparatorChar.ToString())) relativePath = relativePath.Substring(1); return relativePath.Replace(Path.DirectorySeparatorChar, '/'); } static void SendResponse(HttpListenerResponse response, int statusCode, bool success, object result, string error) { response.StatusCode = statusCode; response.ContentType = "application/json; charset=utf-8"; response.ContentEncoding = Encoding.UTF8; var responseData = new Dictionary<string, object> { ["success"] = success }; if (result != null) responseData["result"] = result; if (!string.IsNullOrEmpty(error)) responseData["error"] = error; var json = JsonConvert.SerializeObject(responseData); var buffer = Encoding.UTF8.GetBytes(json); response.ContentLength64 = buffer.Length; response.OutputStream.Write(buffer, 0, buffer.Length); response.Close(); } } }

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

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