embedded-scripts.ts•60.5 kB
import * as fs from 'fs/promises';
import * as path from 'path';
export interface EmbeddedScript {
  fileName: string;
  content: string;
  version: string;
}
/**
 * Static embedded scripts provider
 * Generated at build time from Unity source files
 */
export class EmbeddedScriptsProvider {
  private scripts: Map<string, EmbeddedScript> = new Map();
  constructor() {
    this.initializeScripts();
  }
  private initializeScripts() {
    // UnityHttpServer.cs content
    this.scripts.set('UnityHttpServer.cs', {
      fileName: 'UnityHttpServer.cs',
      version: '1.1.0',
      content: `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();
        }
    }
}`
    });
    // UnityMCPServerWindow.cs content
    this.scripts.set('UnityMCPServerWindow.cs', {
      fileName: 'UnityMCPServerWindow.cs',
      version: '1.0.0',
      content: `using System;
using UnityEngine;
using UnityEditor;
namespace UnityMCP
{
    /// <summary>
    /// Unity MCP Server control window
    /// </summary>
    public class UnityMCPServerWindow : EditorWindow
    {
        // Version information (should match UnityHttpServer)
        private const string SCRIPT_VERSION = "1.1.0";
        
        private int serverPort = 23457;
        private bool isServerRunning = false;
        private string serverStatus = "Stopped";
        private string lastError = "";
        
        [MenuItem("Window/Unity MCP Server")]
        public static void ShowWindow()
        {
            GetWindow<UnityMCPServerWindow>("Unity MCP Server");
        }
        
        void OnEnable()
        {
            // Load saved settings
            serverPort = EditorPrefs.GetInt("UnityMCP.ServerPort", 23457);
            UpdateStatus();
        }
        
        void OnDisable()
        {
            // Save settings
            EditorPrefs.SetInt("UnityMCP.ServerPort", serverPort);
        }
        
        void OnGUI()
        {
            GUILayout.Label("Unity MCP Server Control", EditorStyles.boldLabel);
            GUILayout.Label($"Version: {SCRIPT_VERSION}", EditorStyles.miniLabel);
            
            EditorGUILayout.Space();
            
            // Server Status
            EditorGUILayout.BeginHorizontal();
            GUILayout.Label("Status:", GUILayout.Width(60));
            var statusColor = isServerRunning ? Color.green : Color.red;
            var originalColor = GUI.color;
            GUI.color = statusColor;
            GUILayout.Label(serverStatus, EditorStyles.boldLabel);
            GUI.color = originalColor;
            EditorGUILayout.EndHorizontal();
            
            EditorGUILayout.Space();
            
            // Port Configuration
            EditorGUILayout.BeginHorizontal();
            GUILayout.Label("Port:", GUILayout.Width(60));
            var newPort = EditorGUILayout.IntField(serverPort);
            if (newPort != serverPort && newPort > 0 && newPort <= 65535)
            {
                serverPort = newPort;
                EditorPrefs.SetInt("UnityMCP.ServerPort", serverPort);
            }
            EditorGUILayout.EndHorizontal();
            
            // Port validation
            if (serverPort < 1024)
            {
                EditorGUILayout.HelpBox("Warning: Ports below 1024 may require administrator privileges.", MessageType.Warning);
            }
            
            EditorGUILayout.Space();
            
            // Control Buttons
            EditorGUILayout.BeginHorizontal();
            
            GUI.enabled = !isServerRunning;
            if (GUILayout.Button("Start Server", GUILayout.Height(30)))
            {
                StartServer();
            }
            
            GUI.enabled = isServerRunning;
            if (GUILayout.Button("Stop Server", GUILayout.Height(30)))
            {
                StopServer();
            }
            
            GUI.enabled = true;
            EditorGUILayout.EndHorizontal();
            
            EditorGUILayout.Space();
            
            // Connection Info
            if (isServerRunning)
            {
                EditorGUILayout.BeginVertical(EditorStyles.helpBox);
                GUILayout.Label("Connection Information", EditorStyles.boldLabel);
                EditorGUILayout.SelectableLabel($"http://localhost:{serverPort}/");
                EditorGUILayout.EndVertical();
            }
            
            // Error Display
            if (!string.IsNullOrEmpty(lastError))
            {
                EditorGUILayout.Space();
                EditorGUILayout.HelpBox(lastError, MessageType.Error);
                if (GUILayout.Button("Clear Error"))
                {
                    lastError = "";
                }
            }
            
            EditorGUILayout.Space();
            
            // Instructions
            EditorGUILayout.BeginVertical(EditorStyles.helpBox);
            GUILayout.Label("Instructions", EditorStyles.boldLabel);
            GUILayout.Label("1. Configure the port (default: 23457)");
            GUILayout.Label("2. Click 'Start Server' to begin");
            GUILayout.Label("3. Use the MCP client to connect");
            GUILayout.Label("4. Click 'Stop Server' when done");
            EditorGUILayout.EndVertical();
        }
        
        void StartServer()
        {
            try
            {
                UnityHttpServer.Start(serverPort);
                UpdateStatus();
                lastError = "";
                Debug.Log($"[UnityMCP] Server started on port {serverPort}");
            }
            catch (Exception e)
            {
                lastError = $"Failed to start server: {e.Message}";
                Debug.LogError($"[UnityMCP] {lastError}");
            }
        }
        
        void StopServer()
        {
            try
            {
                UnityHttpServer.Shutdown();
                UpdateStatus();
                lastError = "";
                Debug.Log("[UnityMCP] Server stopped");
            }
            catch (Exception e)
            {
                lastError = $"Failed to stop server: {e.Message}";
                Debug.LogError($"[UnityMCP] {lastError}");
            }
        }
        
        void UpdateStatus()
        {
            isServerRunning = UnityHttpServer.IsRunning;
            serverStatus = isServerRunning ? $"Running on port {UnityHttpServer.CurrentPort}" : "Stopped";
            Repaint();
        }
        
        void Update()
        {
            // Update status periodically
            UpdateStatus();
        }
    }
}`
    });
  }
  /**
   * Get script by filename
   */
  async getScript(fileName: string): Promise<EmbeddedScript | null> {
    return this.scripts.get(fileName) || null;
  }
  /**
   * Get script synchronously
   */
  getScriptSync(fileName: string): EmbeddedScript | null {
    return this.scripts.get(fileName) || null;
  }
  /**
   * Write script to file with proper UTF-8 BOM for Unity compatibility
   */
  async writeScriptToFile(fileName: string, targetPath: string): Promise<void> {
    const script = await this.getScript(fileName);
    if (!script) {
      throw new Error(`Script not found: ${fileName}`);
    }
    // Ensure target directory exists
    await fs.mkdir(path.dirname(targetPath), { recursive: true });
    
    // Write with UTF-8 BOM for Unity compatibility
    const utf8BOM = Buffer.from([0xEF, 0xBB, 0xBF]);
    const contentBuffer = Buffer.from(script.content, 'utf8');
    const finalBuffer = Buffer.concat([utf8BOM, contentBuffer]);
    
    await fs.writeFile(targetPath, finalBuffer);
  }
  /**
   * Get all available script names
   */
  getAvailableScripts(): string[] {
    return Array.from(this.scripts.keys());
  }
  /**
   * Get script version
   */
  getScriptVersion(fileName: string): string | null {
    const script = this.scripts.get(fileName);
    return script?.version || null;
  }
}