#
tokens: 15502/50000 1/29 files (page 2/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 3. Use http://codebase.md/zabaglione/mcp-server-unity?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .claude
│   └── settings.local.json
├── .gitignore
├── build-bundle.js
├── build-final-dxt.sh
├── BUILD.md
├── CHANGELOG.md
├── CLAUDE.md
├── CONTRIBUTING.md
├── create-bundled-dxt.sh
├── docs
│   ├── API.md
│   └── ARCHITECTURE.md
├── generate-embedded-scripts.cjs
├── LICENSE
├── manifest.json
├── package-lock.json
├── package.json
├── README-ja.md
├── README.md
├── src
│   ├── adapters
│   │   └── unity-http-adapter.ts
│   ├── embedded-scripts.ts
│   ├── services
│   │   └── unity-bridge-deploy-service.ts
│   ├── simple-index.ts
│   ├── tools
│   │   └── unity-mcp-tools.ts
│   └── unity-scripts
│       ├── UnityHttpServer.cs
│       └── UnityMCPServerWindow.cs
├── TECHNICAL.md
├── tests
│   ├── integration
│   │   └── simple-integration.test.ts
│   ├── unit
│   │   ├── adapters
│   │   │   └── unity-http-adapter.test.ts
│   │   ├── templates
│   │   │   └── shaders
│   │   │       └── shader-templates.test.ts
│   │   └── tools
│   │       └── unity-mcp-tools.test.ts
│   └── unity
│       └── UnityHttpServerTests.cs
├── tsconfig.json
├── tsconfig.test.json
├── unity-mcp-server.bundle.js
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/src/embedded-scripts.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import * as fs from 'fs/promises';
   2 | import * as path from 'path';
   3 | 
   4 | export interface EmbeddedScript {
   5 |   fileName: string;
   6 |   content: string;
   7 |   version: string;
   8 | }
   9 | 
  10 | /**
  11 |  * Static embedded scripts provider
  12 |  * Generated at build time from Unity source files
  13 |  */
  14 | export class EmbeddedScriptsProvider {
  15 |   private scripts: Map<string, EmbeddedScript> = new Map();
  16 | 
  17 |   constructor() {
  18 |     this.initializeScripts();
  19 |   }
  20 | 
  21 |   private initializeScripts() {
  22 |     // UnityHttpServer.cs content
  23 |     this.scripts.set('UnityHttpServer.cs', {
  24 |       fileName: 'UnityHttpServer.cs',
  25 |       version: '1.1.0',
  26 |       content: `using System;
  27 | using System.Collections.Generic;
  28 | using System.IO;
  29 | using System.Linq;
  30 | using System.Net;
  31 | using System.Text;
  32 | using System.Threading;
  33 | using UnityEngine;
  34 | using UnityEditor;
  35 | using Newtonsoft.Json;
  36 | using Newtonsoft.Json.Linq;
  37 | 
  38 | namespace UnityMCP
  39 | {
  40 |     [InitializeOnLoad]
  41 |     public static class UnityMCPInstaller
  42 |     {
  43 |         static UnityMCPInstaller()
  44 |         {
  45 |             CheckAndUpdateScripts();
  46 |         }
  47 |         
  48 |         static void CheckAndUpdateScripts()
  49 |         {
  50 |             var installedVersion = EditorPrefs.GetString(UnityHttpServer.VERSION_META_KEY, "0.0.0");
  51 |             if (installedVersion != UnityHttpServer.SCRIPT_VERSION)
  52 |             {
  53 |                 Debug.Log($"[UnityMCP] Updating Unity MCP scripts from version {installedVersion} to {UnityHttpServer.SCRIPT_VERSION}");
  54 |                 // Version update logic will be handled by the MCP server
  55 |                 EditorPrefs.SetString(UnityHttpServer.VERSION_META_KEY, UnityHttpServer.SCRIPT_VERSION);
  56 |             }
  57 |         }
  58 |     }
  59 |     /// <summary>
  60 |     /// Simple HTTP server for Unity MCP integration
  61 |     /// </summary>
  62 |     public static class UnityHttpServer
  63 |     {
  64 |         // Version information for auto-update
  65 |         public const string SCRIPT_VERSION = "1.1.0";
  66 |         public const string VERSION_META_KEY = "UnityMCP.InstalledVersion";
  67 |         
  68 |         // Configuration constants
  69 |         private const int DEFAULT_PORT = 23457;
  70 |         private const int REQUEST_TIMEOUT_MS = 120000; // 2 minutes
  71 |         private const int THREAD_JOIN_TIMEOUT_MS = 1000; // 1 second
  72 |         private const int ASSET_REFRESH_DELAY_MS = 500; // Wait after asset operations
  73 |         public const string SERVER_LOG_PREFIX = "[UnityMCP]";
  74 |         private const string PREFS_PORT_KEY = "UnityMCP.ServerPort";
  75 |         private const string PREFS_PORT_BEFORE_PLAY_KEY = "UnityMCP.ServerPortBeforePlay";
  76 |         
  77 |         // File path constants
  78 |         private const string ASSETS_PREFIX = "Assets/";
  79 |         private const int ASSETS_PREFIX_LENGTH = 7;
  80 |         private const string DEFAULT_SCRIPTS_FOLDER = "Assets/Scripts";
  81 |         private const string DEFAULT_SHADERS_FOLDER = "Assets/Shaders";
  82 |         private const string CS_EXTENSION = ".cs";
  83 |         private const string SHADER_EXTENSION = ".shader";
  84 |         
  85 |         private static HttpListener httpListener;
  86 |         private static Thread listenerThread;
  87 |         private static bool isRunning = false;
  88 |         
  89 |         // Request queue for serialization
  90 |         private static readonly Queue<Action> requestQueue = new Queue<Action>();
  91 |         private static bool isProcessingRequest = false;
  92 |         private static int currentPort = DEFAULT_PORT;
  93 |         
  94 |         /// <summary>
  95 |         /// Gets whether the server is currently running
  96 |         /// </summary>
  97 |         public static bool IsRunning => isRunning;
  98 |         
  99 |         /// <summary>
 100 |         /// Gets the current port the server is running on
 101 |         /// </summary>
 102 |         public static int CurrentPort => currentPort;
 103 |         
 104 |         [InitializeOnLoad]
 105 |         static class AutoShutdown
 106 |         {
 107 |             static AutoShutdown()
 108 |             {
 109 |                 EditorApplication.playModeStateChanged += OnPlayModeChanged;
 110 |                 EditorApplication.quitting += Shutdown;
 111 |                 
 112 |                 // Handle script recompilation
 113 |                 UnityEditor.Compilation.CompilationPipeline.compilationStarted += OnCompilationStarted;
 114 |                 UnityEditor.Compilation.CompilationPipeline.compilationFinished += OnCompilationFinished;
 115 |                 
 116 |                 // Auto-start server on Unity startup
 117 |                 EditorApplication.delayCall += () => {
 118 |                     if (!isRunning)
 119 |                     {
 120 |                         var savedPort = EditorPrefs.GetInt(PREFS_PORT_KEY, DEFAULT_PORT);
 121 |                         Debug.Log($"{SERVER_LOG_PREFIX} Auto-starting server on port {savedPort}");
 122 |                         Start(savedPort);
 123 |                     }
 124 |                 };
 125 |             }
 126 |             
 127 |             static void OnCompilationStarted(object obj)
 128 |             {
 129 |                 Debug.Log($"{SERVER_LOG_PREFIX} Compilation started - stopping server");
 130 |                 if (isRunning)
 131 |                 {
 132 |                     Shutdown();
 133 |                 }
 134 |             }
 135 |             
 136 |             static void OnCompilationFinished(object obj)
 137 |             {
 138 |                 Debug.Log($"{SERVER_LOG_PREFIX} Compilation finished - auto-restarting server");
 139 |                 // Always auto-restart after compilation
 140 |                 var savedPort = EditorPrefs.GetInt(PREFS_PORT_KEY, DEFAULT_PORT);
 141 |                 EditorApplication.delayCall += () => Start(savedPort);
 142 |             }
 143 |         }
 144 |         
 145 |         /// <summary>
 146 |         /// Start the HTTP server on the specified port
 147 |         /// </summary>
 148 |         /// <param name="port">Port to listen on</param>
 149 |         public static void Start(int port = DEFAULT_PORT)
 150 |         {
 151 |             if (isRunning) 
 152 |             {
 153 |                 Debug.LogWarning($"{SERVER_LOG_PREFIX} Server is already running. Stop it first.");
 154 |                 return;
 155 |             }
 156 |             
 157 |             currentPort = port;
 158 |             
 159 |             try
 160 |             {
 161 |                 httpListener = new HttpListener();
 162 |                 httpListener.Prefixes.Add($"http://localhost:{currentPort}/");
 163 |                 httpListener.Start();
 164 |                 isRunning = true;
 165 |                 
 166 |                 listenerThread = new Thread(ListenLoop) 
 167 |                 { 
 168 |                     IsBackground = true,
 169 |                     Name = "UnityMCPHttpListener"
 170 |                 };
 171 |                 listenerThread.Start();
 172 |                 
 173 |                 Debug.Log($"{SERVER_LOG_PREFIX} HTTP Server started on port {currentPort}");
 174 |             }
 175 |             catch (Exception e)
 176 |             {
 177 |                 isRunning = false;
 178 |                 Debug.LogError($"{SERVER_LOG_PREFIX} Failed to start HTTP server: {e.Message}");
 179 |                 throw;
 180 |             }
 181 |         }
 182 |         
 183 |         /// <summary>
 184 |         /// Stop the HTTP server
 185 |         /// </summary>
 186 |         public static void Shutdown()
 187 |         {
 188 |             if (!isRunning)
 189 |             {
 190 |                 Debug.LogWarning($"{SERVER_LOG_PREFIX} Server is not running.");
 191 |                 return;
 192 |             }
 193 |             
 194 |             isRunning = false;
 195 |             
 196 |             try
 197 |             {
 198 |                 httpListener?.Stop();
 199 |                 httpListener?.Close();
 200 |                 listenerThread?.Join(THREAD_JOIN_TIMEOUT_MS);
 201 |                 Debug.Log($"{SERVER_LOG_PREFIX} HTTP Server stopped");
 202 |             }
 203 |             catch (Exception e)
 204 |             {
 205 |                 Debug.LogError($"{SERVER_LOG_PREFIX} Error during shutdown: {e.Message}");
 206 |             }
 207 |             finally
 208 |             {
 209 |                 httpListener = null;
 210 |                 listenerThread = null;
 211 |             }
 212 |         }
 213 |         
 214 |         static void OnPlayModeChanged(PlayModeStateChange state)
 215 |         {
 216 |             // Stop server when entering play mode to avoid conflicts
 217 |             if (state == PlayModeStateChange.ExitingEditMode)
 218 |             {
 219 |                 if (isRunning)
 220 |                 {
 221 |                     Debug.Log($"{SERVER_LOG_PREFIX} Stopping server due to play mode change");
 222 |                     EditorPrefs.SetInt(PREFS_PORT_BEFORE_PLAY_KEY, currentPort);
 223 |                     Shutdown();
 224 |                 }
 225 |             }
 226 |             // Restart server when returning to edit mode
 227 |             else if (state == PlayModeStateChange.EnteredEditMode)
 228 |             {
 229 |                 var savedPort = EditorPrefs.GetInt(PREFS_PORT_BEFORE_PLAY_KEY, DEFAULT_PORT);
 230 |                 Debug.Log($"{SERVER_LOG_PREFIX} Restarting server after play mode on port {savedPort}");
 231 |                 EditorApplication.delayCall += () => Start(savedPort);
 232 |             }
 233 |         }
 234 |         
 235 |         static void ListenLoop()
 236 |         {
 237 |             while (isRunning)
 238 |             {
 239 |                 try
 240 |                 {
 241 |                     var context = httpListener.GetContext();
 242 |                     ThreadPool.QueueUserWorkItem(_ => HandleRequest(context));
 243 |                 }
 244 |                 catch (Exception e)
 245 |                 {
 246 |                     if (isRunning)
 247 |                         Debug.LogError($"{SERVER_LOG_PREFIX} Listen error: {e.Message}");
 248 |                 }
 249 |             }
 250 |         }
 251 |         
 252 |         static void HandleRequest(HttpListenerContext context)
 253 |         {
 254 |             var request = context.Request;
 255 |             var response = context.Response;
 256 |             response.Headers.Add("Access-Control-Allow-Origin", "*");
 257 |             
 258 |             try
 259 |             {
 260 |                 if (request.HttpMethod != "POST")
 261 |                 {
 262 |                     SendResponse(response, 405, false, null, "Method not allowed");
 263 |                     return;
 264 |                 }
 265 |                 
 266 |                 string requestBody;
 267 |                 // Force UTF-8 encoding for request body
 268 |                 using (var reader = new StreamReader(request.InputStream, Encoding.UTF8))
 269 |                 {
 270 |                     requestBody = reader.ReadToEnd();
 271 |                 }
 272 |                 
 273 |                 var requestData = JObject.Parse(requestBody);
 274 |                 var method = requestData["method"]?.ToString();
 275 |                 
 276 |                 if (string.IsNullOrEmpty(method))
 277 |                 {
 278 |                     SendResponse(response, 400, false, null, "Method is required");
 279 |                     return;
 280 |                 }
 281 |                 
 282 |                 Debug.Log($"{SERVER_LOG_PREFIX} Processing request: {method}");
 283 |                 
 284 |                 // Check if this request requires main thread
 285 |                 bool requiresMainThread = RequiresMainThread(method);
 286 |                 
 287 |                 if (!requiresMainThread)
 288 |                 {
 289 |                     // Process directly on worker thread
 290 |                     try
 291 |                     {
 292 |                         var result = ProcessRequestOnWorkerThread(method, requestData);
 293 |                         SendResponse(response, 200, true, result, null);
 294 |                     }
 295 |                     catch (Exception e)
 296 |                     {
 297 |                         var statusCode = e is ArgumentException ? 400 : 500;
 298 |                         SendResponse(response, statusCode, false, null, e.Message);
 299 |                     }
 300 |                 }
 301 |                 else
 302 |                 {
 303 |                     // Execute on main thread for Unity API calls
 304 |                     object result = null;
 305 |                     Exception error = null;
 306 |                     var resetEvent = new ManualResetEvent(false);
 307 |                     
 308 |                     EditorApplication.delayCall += () =>
 309 |                     {
 310 |                         try
 311 |                         {
 312 |                             Debug.Log($"{SERVER_LOG_PREFIX} Processing on main thread: {method}");
 313 |                             result = ProcessRequest(method, requestData);
 314 |                             Debug.Log($"{SERVER_LOG_PREFIX} Completed processing: {method}");
 315 |                         }
 316 |                         catch (Exception e)
 317 |                         {
 318 |                             error = e;
 319 |                             Debug.LogError($"{SERVER_LOG_PREFIX} Error processing {method}: {e.Message}");
 320 |                         }
 321 |                         finally
 322 |                         {
 323 |                             resetEvent.Set();
 324 |                         }
 325 |                     };
 326 |                     
 327 |                     if (!resetEvent.WaitOne(REQUEST_TIMEOUT_MS))
 328 |                     {
 329 |                         SendResponse(response, 504, false, null, "Request timeout - Unity may be busy or unfocused");
 330 |                         return;
 331 |                     }
 332 |                     
 333 |                     if (error != null)
 334 |                     {
 335 |                         var statusCode = error is ArgumentException ? 400 : 500;
 336 |                         SendResponse(response, statusCode, false, null, error.Message);
 337 |                         return;
 338 |                     }
 339 |                     
 340 |                     SendResponse(response, 200, true, result, null);
 341 |                 }
 342 |             }
 343 |             catch (Exception e)
 344 |             {
 345 |                 SendResponse(response, 400, false, null, $"Bad request: {e.Message}");
 346 |             }
 347 |         }
 348 |         
 349 |         static bool RequiresMainThread(string method)
 350 |         {
 351 |             // These methods can run on worker thread
 352 |             switch (method)
 353 |             {
 354 |                 case "ping":
 355 |                 case "script/read":
 356 |                 case "shader/read":
 357 |                     return false;
 358 |                     
 359 |                 // project/info now requires Unity API for render pipeline detection
 360 |                 // Creating, deleting files require Unity API (AssetDatabase)
 361 |                 default:
 362 |                     return true;
 363 |             }
 364 |         }
 365 |         
 366 |         static object ProcessRequestOnWorkerThread(string method, JObject request)
 367 |         {
 368 |             switch (method)
 369 |             {
 370 |                 case "ping":
 371 |                     return new { status = "ok", time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") };
 372 |                     
 373 |                 case "project/info":
 374 |                     // project/info requires Unity API for render pipeline detection
 375 |                     throw new NotImplementedException("project/info requires main thread for render pipeline detection");
 376 |                     
 377 |                 case "script/read":
 378 |                     return ReadScriptOnWorkerThread(request);
 379 |                     
 380 |                 case "shader/read":
 381 |                     return ReadShaderOnWorkerThread(request);
 382 |                     
 383 |                 // Folder operations (can run on worker thread)
 384 |                 case "folder/create":
 385 |                     return CreateFolderOnWorkerThread(request);
 386 |                 case "folder/rename":
 387 |                     return RenameFolderOnWorkerThread(request);
 388 |                 case "folder/move":
 389 |                     return MoveFolderOnWorkerThread(request);
 390 |                 case "folder/delete":
 391 |                     return DeleteFolderOnWorkerThread(request);
 392 |                 case "folder/list":
 393 |                     return ListFolderOnWorkerThread(request);
 394 |                     
 395 |                 default:
 396 |                     throw new NotImplementedException($"Method not implemented for worker thread: {method}");
 397 |             }
 398 |         }
 399 |         
 400 |         static object ReadScriptOnWorkerThread(JObject request)
 401 |         {
 402 |             var path = request["path"]?.ToString();
 403 |             if (string.IsNullOrEmpty(path))
 404 |                 throw new ArgumentException("path is required");
 405 |             
 406 |             var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH));
 407 |             if (!File.Exists(fullPath))
 408 |                 throw new FileNotFoundException($"File not found: {path}");
 409 |             
 410 |             return new
 411 |             {
 412 |                 path = path,
 413 |                 content = File.ReadAllText(fullPath, new UTF8Encoding(true)),
 414 |                 guid = "" // GUID requires AssetDatabase, skip in worker thread
 415 |             };
 416 |         }
 417 |         
 418 |         static object ReadShaderOnWorkerThread(JObject request)
 419 |         {
 420 |             var path = request["path"]?.ToString();
 421 |             if (string.IsNullOrEmpty(path))
 422 |                 throw new ArgumentException("path is required");
 423 |             
 424 |             var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH));
 425 |             if (!File.Exists(fullPath))
 426 |                 throw new FileNotFoundException($"File not found: {path}");
 427 |             
 428 |             return new
 429 |             {
 430 |                 path = path,
 431 |                 content = File.ReadAllText(fullPath, new UTF8Encoding(true)),
 432 |                 guid = "" // GUID requires AssetDatabase, skip in worker thread
 433 |             };
 434 |         }
 435 |         
 436 |         static object ProcessRequest(string method, JObject request)
 437 |         {
 438 |             switch (method)
 439 |             {
 440 |                 case "ping":
 441 |                     return new { status = "ok", time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") };
 442 |                 
 443 |                 // Script operations
 444 |                 case "script/create":
 445 |                     return CreateScript(request);
 446 |                 case "script/read":
 447 |                     return ReadScript(request);
 448 |                 case "script/delete":
 449 |                     return DeleteScript(request);
 450 |                 case "script/applyDiff":
 451 |                     return ApplyDiff(request);
 452 |                 
 453 |                 // Shader operations
 454 |                 case "shader/create":
 455 |                     return CreateShader(request);
 456 |                 case "shader/read":
 457 |                     return ReadShader(request);
 458 |                 case "shader/delete":
 459 |                     return DeleteShader(request);
 460 |                 
 461 |                 // Project operations
 462 |                 case "project/info":
 463 |                     return GetProjectInfo();
 464 |                 
 465 |                 // Folder operations
 466 |                 case "folder/create":
 467 |                     return CreateFolder(request);
 468 |                 case "folder/rename":
 469 |                     return RenameFolder(request);
 470 |                 case "folder/move":
 471 |                     return MoveFolder(request);
 472 |                 case "folder/delete":
 473 |                     return DeleteFolder(request);
 474 |                 case "folder/list":
 475 |                     return ListFolder(request);
 476 |                 
 477 |                 default:
 478 |                     throw new NotImplementedException($"Method not found: {method}");
 479 |             }
 480 |         }
 481 |         
 482 |         static object CreateScript(JObject request)
 483 |         {
 484 |             var fileName = request["fileName"]?.ToString();
 485 |             if (string.IsNullOrEmpty(fileName))
 486 |                 throw new ArgumentException("fileName is required");
 487 |             
 488 |             if (!fileName.EndsWith(CS_EXTENSION))
 489 |                 fileName += CS_EXTENSION;
 490 |             
 491 |             var content = request["content"]?.ToString();
 492 |             var folder = request["folder"]?.ToString() ?? DEFAULT_SCRIPTS_FOLDER;
 493 |             
 494 |             var path = Path.Combine(folder, fileName);
 495 |             var directory = Path.GetDirectoryName(path);
 496 |             
 497 |             // Create directory if needed
 498 |             if (!AssetDatabase.IsValidFolder(directory))
 499 |             {
 500 |                 CreateFolderRecursive(directory);
 501 |             }
 502 |             
 503 |             // Use Unity-safe file creation approach
 504 |             var scriptContent = content ?? GetDefaultScriptContent(fileName);
 505 |             
 506 |             // First, ensure the asset doesn't already exist
 507 |             if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path) != null)
 508 |             {
 509 |                 throw new InvalidOperationException($"Asset already exists: {path}");
 510 |             }
 511 |             
 512 |             // Write file using UTF-8 with BOM (Unity standard)
 513 |             var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH));
 514 |             var utf8WithBom = new UTF8Encoding(true);
 515 |             File.WriteAllText(fullPath, scriptContent, utf8WithBom);
 516 |             
 517 |             // Import the asset immediately and wait for completion
 518 |             AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate);
 519 |             
 520 |             // Verify the asset was imported successfully
 521 |             var attempts = 0;
 522 |             const int maxAttempts = 10;
 523 |             while (AssetDatabase.AssetPathToGUID(path) == "" && attempts < maxAttempts)
 524 |             {
 525 |                 System.Threading.Thread.Sleep(100);
 526 |                 AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
 527 |                 attempts++;
 528 |             }
 529 |             
 530 |             if (AssetDatabase.AssetPathToGUID(path) == "")
 531 |             {
 532 |                 throw new InvalidOperationException($"Failed to import asset: {path}");
 533 |             }
 534 |             
 535 |             return new
 536 |             {
 537 |                 path = path,
 538 |                 guid = AssetDatabase.AssetPathToGUID(path)
 539 |             };
 540 |         }
 541 |         
 542 |         static object ReadScript(JObject request)
 543 |         {
 544 |             var path = request["path"]?.ToString();
 545 |             if (string.IsNullOrEmpty(path))
 546 |                 throw new ArgumentException("path is required");
 547 |             
 548 |             var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH));
 549 |             if (!File.Exists(fullPath))
 550 |                 throw new FileNotFoundException($"File not found: {path}");
 551 |             
 552 |             return new
 553 |             {
 554 |                 path = path,
 555 |                 content = File.ReadAllText(fullPath, new UTF8Encoding(true)),
 556 |                 guid = AssetDatabase.AssetPathToGUID(path)
 557 |             };
 558 |         }
 559 |         
 560 |         static object DeleteScript(JObject request)
 561 |         {
 562 |             var path = request["path"]?.ToString();
 563 |             if (string.IsNullOrEmpty(path))
 564 |                 throw new ArgumentException("path is required");
 565 |             
 566 |             // Verify file exists before deletion
 567 |             var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH));
 568 |             if (!File.Exists(fullPath))
 569 |                 throw new FileNotFoundException($"File not found: {path}");
 570 |             
 571 |             // Delete using AssetDatabase
 572 |             if (!AssetDatabase.DeleteAsset(path))
 573 |                 throw new InvalidOperationException($"Failed to delete: {path}");
 574 |             
 575 |             // Force immediate refresh
 576 |             AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
 577 |             
 578 |             // Wait for asset database to process deletion
 579 |             System.Threading.Thread.Sleep(ASSET_REFRESH_DELAY_MS);
 580 |             
 581 |             return new { message = "Script deleted successfully" };
 582 |         }
 583 |         
 584 |         static object ApplyDiff(JObject request)
 585 |         {
 586 |             var path = request["path"]?.ToString();
 587 |             var diff = request["diff"]?.ToString();
 588 |             var options = request["options"] as JObject;
 589 |             
 590 |             if (string.IsNullOrEmpty(path))
 591 |                 throw new ArgumentException("path is required");
 592 |             if (string.IsNullOrEmpty(diff))
 593 |                 throw new ArgumentException("diff is required");
 594 |             
 595 |             var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH));
 596 |             if (!File.Exists(fullPath))
 597 |                 throw new FileNotFoundException($"File not found: {path}");
 598 |             
 599 |             var dryRun = options?["dryRun"]?.Value<bool>() ?? false;
 600 |             
 601 |             // Read current content using UTF-8 with BOM (Unity standard)
 602 |             var utf8WithBom = new UTF8Encoding(true);
 603 |             var originalContent = File.ReadAllText(fullPath, utf8WithBom);
 604 |             var lines = originalContent.Split('\\n').ToList();
 605 |             
 606 |             // Parse and apply unified diff
 607 |             var diffLines = diff.Split('\\n');
 608 |             var linesAdded = 0;
 609 |             var linesRemoved = 0;
 610 |             var currentLine = 0;
 611 |             
 612 |             for (int i = 0; i < diffLines.Length; i++)
 613 |             {
 614 |                 var line = diffLines[i];
 615 |                 if (line.StartsWith("@@"))
 616 |                 {
 617 |                     // Parse hunk header: @@ -l,s +l,s @@
 618 |                     var match = System.Text.RegularExpressions.Regex.Match(line, @"@@ -(\\d+),?\\d* \\+(\\d+),?\\d* @@");
 619 |                     if (match.Success)
 620 |                     {
 621 |                         currentLine = int.Parse(match.Groups[1].Value) - 1;
 622 |                     }
 623 |                 }
 624 |                 else if (line.StartsWith("-") && !line.StartsWith("---"))
 625 |                 {
 626 |                     // Remove line
 627 |                     if (currentLine < lines.Count)
 628 |                     {
 629 |                         lines.RemoveAt(currentLine);
 630 |                         linesRemoved++;
 631 |                     }
 632 |                 }
 633 |                 else if (line.StartsWith("+") && !line.StartsWith("+++"))
 634 |                 {
 635 |                     // Add line
 636 |                     lines.Insert(currentLine, line.Substring(1));
 637 |                     currentLine++;
 638 |                     linesAdded++;
 639 |                 }
 640 |                 else if (line.StartsWith(" "))
 641 |                 {
 642 |                     // Context line
 643 |                     currentLine++;
 644 |                 }
 645 |             }
 646 |             
 647 |             // Write result if not dry run
 648 |             if (!dryRun)
 649 |             {
 650 |                 var updatedContent = string.Join("\\n", lines);
 651 |                 // Write with UTF-8 with BOM (Unity standard)
 652 |                 File.WriteAllText(fullPath, updatedContent, utf8WithBom);
 653 |                 AssetDatabase.Refresh();
 654 |                 
 655 |                 // Wait for asset database to process
 656 |                 System.Threading.Thread.Sleep(ASSET_REFRESH_DELAY_MS);
 657 |             }
 658 |             
 659 |             return new
 660 |             {
 661 |                 path = path,
 662 |                 linesAdded = linesAdded,
 663 |                 linesRemoved = linesRemoved,
 664 |                 dryRun = dryRun,
 665 |                 guid = AssetDatabase.AssetPathToGUID(path)
 666 |             };
 667 |         }
 668 |         
 669 |         static object CreateShader(JObject request)
 670 |         {
 671 |             var name = request["name"]?.ToString();
 672 |             if (string.IsNullOrEmpty(name))
 673 |                 throw new ArgumentException("name is required");
 674 |             
 675 |             if (!name.EndsWith(SHADER_EXTENSION))
 676 |                 name += SHADER_EXTENSION;
 677 |             
 678 |             var content = request["content"]?.ToString();
 679 |             var folder = request["folder"]?.ToString() ?? DEFAULT_SHADERS_FOLDER;
 680 |             
 681 |             var path = Path.Combine(folder, name);
 682 |             var directory = Path.GetDirectoryName(path);
 683 |             
 684 |             if (!AssetDatabase.IsValidFolder(directory))
 685 |             {
 686 |                 CreateFolderRecursive(directory);
 687 |             }
 688 |             
 689 |             // Use Unity-safe file creation approach
 690 |             var shaderContent = content ?? GetDefaultShaderContent(name);
 691 |             
 692 |             // First, ensure the asset doesn't already exist
 693 |             if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path) != null)
 694 |             {
 695 |                 throw new InvalidOperationException($"Asset already exists: {path}");
 696 |             }
 697 |             
 698 |             // Write file using UTF-8 with BOM (Unity standard)
 699 |             var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH));
 700 |             var utf8WithBom = new UTF8Encoding(true);
 701 |             File.WriteAllText(fullPath, shaderContent, utf8WithBom);
 702 |             
 703 |             // Import the asset immediately and wait for completion
 704 |             AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate);
 705 |             
 706 |             // Verify the asset was imported successfully
 707 |             var attempts = 0;
 708 |             const int maxAttempts = 10;
 709 |             while (AssetDatabase.AssetPathToGUID(path) == "" && attempts < maxAttempts)
 710 |             {
 711 |                 System.Threading.Thread.Sleep(100);
 712 |                 AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
 713 |                 attempts++;
 714 |             }
 715 |             
 716 |             if (AssetDatabase.AssetPathToGUID(path) == "")
 717 |             {
 718 |                 throw new InvalidOperationException($"Failed to import asset: {path}");
 719 |             }
 720 |             
 721 |             return new
 722 |             {
 723 |                 path = path,
 724 |                 guid = AssetDatabase.AssetPathToGUID(path)
 725 |             };
 726 |         }
 727 |         
 728 |         static object ReadShader(JObject request)
 729 |         {
 730 |             var path = request["path"]?.ToString();
 731 |             if (string.IsNullOrEmpty(path))
 732 |                 throw new ArgumentException("path is required");
 733 |             
 734 |             var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH));
 735 |             if (!File.Exists(fullPath))
 736 |                 throw new FileNotFoundException($"File not found: {path}");
 737 |             
 738 |             return new
 739 |             {
 740 |                 path = path,
 741 |                 content = File.ReadAllText(fullPath, new UTF8Encoding(true)),
 742 |                 guid = AssetDatabase.AssetPathToGUID(path)
 743 |             };
 744 |         }
 745 |         
 746 |         static object DeleteShader(JObject request)
 747 |         {
 748 |             var path = request["path"]?.ToString();
 749 |             if (string.IsNullOrEmpty(path))
 750 |                 throw new ArgumentException("path is required");
 751 |             
 752 |             if (!AssetDatabase.DeleteAsset(path))
 753 |                 throw new InvalidOperationException($"Failed to delete: {path}");
 754 |             
 755 |             // Wait for asset database to process deletion
 756 |             System.Threading.Thread.Sleep(ASSET_REFRESH_DELAY_MS);
 757 |             
 758 |             return new { message = "Shader deleted successfully" };
 759 |         }
 760 |         
 761 |         static object GetProjectInfo()
 762 |         {
 763 |             // Detect render pipeline with multiple methods
 764 |             string renderPipeline = "Built-in";
 765 |             string renderPipelineVersion = "N/A";
 766 |             string detectionMethod = "Default";
 767 |             
 768 |             try
 769 |             {
 770 |                 // Method 1: Check GraphicsSettings.renderPipelineAsset
 771 |                 var renderPipelineAsset = UnityEngine.Rendering.GraphicsSettings.renderPipelineAsset;
 772 |                 Debug.Log($"{SERVER_LOG_PREFIX} RenderPipelineAsset: {(renderPipelineAsset != null ? renderPipelineAsset.GetType().FullName : "null")}");
 773 |                 
 774 |                 if (renderPipelineAsset != null)
 775 |                 {
 776 |                     var assetType = renderPipelineAsset.GetType();
 777 |                     var typeName = assetType.Name;
 778 |                     var fullTypeName = assetType.FullName;
 779 |                     
 780 |                     Debug.Log($"{SERVER_LOG_PREFIX} Asset type: {typeName}, Full type: {fullTypeName}");
 781 |                     
 782 |                     if (fullTypeName.Contains("Universal") || typeName.Contains("Universal") || 
 783 |                         fullTypeName.Contains("URP") || typeName.Contains("URP"))
 784 |                     {
 785 |                         renderPipeline = "URP";
 786 |                         detectionMethod = "GraphicsSettings.renderPipelineAsset";
 787 |                     }
 788 |                     else if (fullTypeName.Contains("HighDefinition") || typeName.Contains("HighDefinition") || 
 789 |                              fullTypeName.Contains("HDRP") || typeName.Contains("HDRP"))
 790 |                     {
 791 |                         renderPipeline = "HDRP";
 792 |                         detectionMethod = "GraphicsSettings.renderPipelineAsset";
 793 |                     }
 794 |                     else
 795 |                     {
 796 |                         renderPipeline = $"Custom ({typeName})";
 797 |                         detectionMethod = "GraphicsSettings.renderPipelineAsset";
 798 |                     }
 799 |                 }
 800 |                 else
 801 |                 {
 802 |                     // Method 2: Check for installed packages if no render pipeline asset
 803 |                     Debug.Log($"{SERVER_LOG_PREFIX} No render pipeline asset found, checking packages...");
 804 |                     
 805 |                     try
 806 |                     {
 807 |                         var urpPackage = UnityEditor.PackageManager.PackageInfo.FindForPackageName("com.unity.render-pipelines.universal");
 808 |                         var hdrpPackage = UnityEditor.PackageManager.PackageInfo.FindForPackageName("com.unity.render-pipelines.high-definition");
 809 |                         
 810 |                         if (urpPackage != null)
 811 |                         {
 812 |                             renderPipeline = "URP (Package Available)";
 813 |                             renderPipelineVersion = urpPackage.version;
 814 |                             detectionMethod = "Package Detection";
 815 |                         }
 816 |                         else if (hdrpPackage != null)
 817 |                         {
 818 |                             renderPipeline = "HDRP (Package Available)";
 819 |                             renderPipelineVersion = hdrpPackage.version;
 820 |                             detectionMethod = "Package Detection";
 821 |                         }
 822 |                         else
 823 |                         {
 824 |                             renderPipeline = "Built-in";
 825 |                             detectionMethod = "No SRP packages found";
 826 |                         }
 827 |                     }
 828 |                     catch (System.Exception ex)
 829 |                     {
 830 |                         Debug.LogWarning($"{SERVER_LOG_PREFIX} Package detection failed: {ex.Message}");
 831 |                         renderPipeline = "Built-in (Package detection failed)";
 832 |                         detectionMethod = "Package detection error";
 833 |                     }
 834 |                 }
 835 |                 
 836 |                 // Try to get version info if not already obtained
 837 |                 if (renderPipelineVersion == "N/A" && renderPipeline.StartsWith("URP"))
 838 |                 {
 839 |                     try
 840 |                     {
 841 |                         var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForPackageName("com.unity.render-pipelines.universal");
 842 |                         if (packageInfo != null)
 843 |                         {
 844 |                             renderPipelineVersion = packageInfo.version;
 845 |                         }
 846 |                     }
 847 |                     catch (System.Exception ex)
 848 |                     {
 849 |                         Debug.LogWarning($"{SERVER_LOG_PREFIX} URP version detection failed: {ex.Message}");
 850 |                         renderPipelineVersion = "Version unknown";
 851 |                     }
 852 |                 }
 853 |                 else if (renderPipelineVersion == "N/A" && renderPipeline.StartsWith("HDRP"))
 854 |                 {
 855 |                     try
 856 |                     {
 857 |                         var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForPackageName("com.unity.render-pipelines.high-definition");
 858 |                         if (packageInfo != null)
 859 |                         {
 860 |                             renderPipelineVersion = packageInfo.version;
 861 |                         }
 862 |                     }
 863 |                     catch (System.Exception ex)
 864 |                     {
 865 |                         Debug.LogWarning($"{SERVER_LOG_PREFIX} HDRP version detection failed: {ex.Message}");
 866 |                         renderPipelineVersion = "Version unknown";
 867 |                     }
 868 |                 }
 869 |                 
 870 |                 Debug.Log($"{SERVER_LOG_PREFIX} Detected render pipeline: {renderPipeline} (v{renderPipelineVersion}) via {detectionMethod}");
 871 |             }
 872 |             catch (System.Exception ex)
 873 |             {
 874 |                 Debug.LogError($"{SERVER_LOG_PREFIX} Render pipeline detection failed: {ex.Message}");
 875 |                 renderPipeline = "Detection Failed";
 876 |                 detectionMethod = "Exception occurred";
 877 |             }
 878 |             
 879 |             return new
 880 |             {
 881 |                 projectPath = Application.dataPath.Replace("/Assets", ""),
 882 |                 projectName = Application.productName,
 883 |                 unityVersion = Application.unityVersion,
 884 |                 platform = Application.platform.ToString(),
 885 |                 isPlaying = Application.isPlaying,
 886 |                 renderPipeline = renderPipeline,
 887 |                 renderPipelineVersion = renderPipelineVersion,
 888 |                 detectionMethod = detectionMethod
 889 |             };
 890 |         }
 891 |         
 892 |         static void CreateFolderRecursive(string path)
 893 |         {
 894 |             var folders = path.Split('/');
 895 |             var currentPath = folders[0];
 896 |             
 897 |             for (int i = 1; i < folders.Length; i++)
 898 |             {
 899 |                 var newPath = currentPath + "/" + folders[i];
 900 |                 if (!AssetDatabase.IsValidFolder(newPath))
 901 |                 {
 902 |                     AssetDatabase.CreateFolder(currentPath, folders[i]);
 903 |                 }
 904 |                 currentPath = newPath;
 905 |             }
 906 |         }
 907 |         
 908 |         static string GetDefaultScriptContent(string fileName)
 909 |         {
 910 |             var className = Path.GetFileNameWithoutExtension(fileName);
 911 |             return "using UnityEngine;\\n\\n" +
 912 |                    $"public class {className} : MonoBehaviour\\n" +
 913 |                    "{\\n" +
 914 |                    "    void Start()\\n" +
 915 |                    "    {\\n" +
 916 |                    "        \\n" +
 917 |                    "    }\\n" +
 918 |                    "    \\n" +
 919 |                    "    void Update()\\n" +
 920 |                    "    {\\n" +
 921 |                    "        \\n" +
 922 |                    "    }\\n" +
 923 |                    "}";
 924 |         }
 925 |         
 926 |         static string GetDefaultShaderContent(string fileName)
 927 |         {
 928 |             var shaderName = Path.GetFileNameWithoutExtension(fileName);
 929 |             return $"Shader \\"Custom/{shaderName}\\"\\n" +
 930 |                    "{\\n" +
 931 |                    "    Properties\\n" +
 932 |                    "    {\\n" +
 933 |                    "        _MainTex (\\"Texture\\", 2D) = \\"white\\" {}\\n" +
 934 |                    "    }\\n" +
 935 |                    "    SubShader\\n" +
 936 |                    "    {\\n" +
 937 |                    "        Tags { \\"RenderType\\"=\\"Opaque\\" }\\n" +
 938 |                    "        LOD 200\\n" +
 939 |                    "\\n" +
 940 |                    "        CGPROGRAM\\n" +
 941 |                    "        #pragma surface surf Standard fullforwardshadows\\n" +
 942 |                    "\\n" +
 943 |                    "        sampler2D _MainTex;\\n" +
 944 |                    "\\n" +
 945 |                    "        struct Input\\n" +
 946 |                    "        {\\n" +
 947 |                    "            float2 uv_MainTex;\\n" +
 948 |                    "        };\\n" +
 949 |                    "\\n" +
 950 |                    "        void surf (Input IN, inout SurfaceOutputStandard o)\\n" +
 951 |                    "        {\\n" +
 952 |                    "            fixed4 c = tex2D (_MainTex, IN.uv_MainTex);\\n" +
 953 |                    "            o.Albedo = c.rgb;\\n" +
 954 |                    "            o.Alpha = c.a;\\n" +
 955 |                    "        }\\n" +
 956 |                    "        ENDCG\\n" +
 957 |                    "    }\\n" +
 958 |                    "    FallBack \\"Diffuse\\"\\n" +
 959 |                    "}";
 960 |         }
 961 |         
 962 |         // Folder operations
 963 |         static object CreateFolder(JObject request)
 964 |         {
 965 |             var path = request["path"]?.ToString();
 966 |             if (string.IsNullOrEmpty(path))
 967 |                 throw new ArgumentException("path is required");
 968 |             
 969 |             if (!path.StartsWith(ASSETS_PREFIX))
 970 |                 path = Path.Combine(DEFAULT_SCRIPTS_FOLDER, path);
 971 |             
 972 |             // Use Unity-safe folder creation
 973 |             if (AssetDatabase.IsValidFolder(path))
 974 |             {
 975 |                 throw new InvalidOperationException($"Folder already exists: {path}");
 976 |             }
 977 |             
 978 |             // Create directory structure properly
 979 |             var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH));
 980 |             Directory.CreateDirectory(fullPath);
 981 |             
 982 |             // Import the folder immediately
 983 |             AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport);
 984 |             
 985 |             // Verify the folder was imported successfully
 986 |             var attempts = 0;
 987 |             const int maxAttempts = 10;
 988 |             while (!AssetDatabase.IsValidFolder(path) && attempts < maxAttempts)
 989 |             {
 990 |                 System.Threading.Thread.Sleep(100);
 991 |                 AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
 992 |                 attempts++;
 993 |             }
 994 |             
 995 |             if (!AssetDatabase.IsValidFolder(path))
 996 |             {
 997 |                 throw new InvalidOperationException($"Failed to import folder: {path}");
 998 |             }
 999 |             
1000 |             return new
1001 |             {
1002 |                 path = path,
1003 |                 guid = AssetDatabase.AssetPathToGUID(path)
1004 |             };
1005 |         }
1006 |         
1007 |         static object CreateFolderOnWorkerThread(JObject request)
1008 |         {
1009 |             var path = request["path"]?.ToString();
1010 |             if (string.IsNullOrEmpty(path))
1011 |                 throw new ArgumentException("path is required");
1012 |             
1013 |             if (!path.StartsWith(ASSETS_PREFIX))
1014 |                 path = Path.Combine(DEFAULT_SCRIPTS_FOLDER, path);
1015 |             
1016 |             var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH));
1017 |             Directory.CreateDirectory(fullPath);
1018 |             
1019 |             return new
1020 |             {
1021 |                 path = path,
1022 |                 guid = "" // GUID requires AssetDatabase
1023 |             };
1024 |         }
1025 |         
1026 |         static object RenameFolder(JObject request)
1027 |         {
1028 |             var oldPath = request["oldPath"]?.ToString();
1029 |             var newName = request["newName"]?.ToString();
1030 |             
1031 |             if (string.IsNullOrEmpty(oldPath))
1032 |                 throw new ArgumentException("oldPath is required");
1033 |             if (string.IsNullOrEmpty(newName))
1034 |                 throw new ArgumentException("newName is required");
1035 |             
1036 |             var error = AssetDatabase.RenameAsset(oldPath, newName);
1037 |             if (!string.IsNullOrEmpty(error))
1038 |                 throw new InvalidOperationException(error);
1039 |             
1040 |             // Wait for asset database to process
1041 |             System.Threading.Thread.Sleep(ASSET_REFRESH_DELAY_MS);
1042 |             
1043 |             var newPath = Path.Combine(Path.GetDirectoryName(oldPath), newName);
1044 |             return new
1045 |             {
1046 |                 oldPath = oldPath,
1047 |                 newPath = newPath,
1048 |                 guid = AssetDatabase.AssetPathToGUID(newPath)
1049 |             };
1050 |         }
1051 |         
1052 |         static object RenameFolderOnWorkerThread(JObject request)
1053 |         {
1054 |             var oldPath = request["oldPath"]?.ToString();
1055 |             var newName = request["newName"]?.ToString();
1056 |             
1057 |             if (string.IsNullOrEmpty(oldPath))
1058 |                 throw new ArgumentException("oldPath is required");
1059 |             if (string.IsNullOrEmpty(newName))
1060 |                 throw new ArgumentException("newName is required");
1061 |             
1062 |             var oldFullPath = Path.Combine(Application.dataPath, oldPath.Substring(ASSETS_PREFIX_LENGTH));
1063 |             var parentDir = Path.GetDirectoryName(oldFullPath);
1064 |             var newFullPath = Path.Combine(parentDir, newName);
1065 |             
1066 |             if (!Directory.Exists(oldFullPath))
1067 |                 throw new DirectoryNotFoundException($"Directory not found: {oldPath}");
1068 |             
1069 |             Directory.Move(oldFullPath, newFullPath);
1070 |             
1071 |             var newPath = Path.Combine(Path.GetDirectoryName(oldPath), newName);
1072 |             return new
1073 |             {
1074 |                 oldPath = oldPath,
1075 |                 newPath = newPath,
1076 |                 guid = "" // GUID requires AssetDatabase
1077 |             };
1078 |         }
1079 |         
1080 |         static object MoveFolder(JObject request)
1081 |         {
1082 |             var sourcePath = request["sourcePath"]?.ToString();
1083 |             var targetPath = request["targetPath"]?.ToString();
1084 |             
1085 |             if (string.IsNullOrEmpty(sourcePath))
1086 |                 throw new ArgumentException("sourcePath is required");
1087 |             if (string.IsNullOrEmpty(targetPath))
1088 |                 throw new ArgumentException("targetPath is required");
1089 |             
1090 |             var error = AssetDatabase.MoveAsset(sourcePath, targetPath);
1091 |             if (!string.IsNullOrEmpty(error))
1092 |                 throw new InvalidOperationException(error);
1093 |             
1094 |             // Wait for asset database to process
1095 |             System.Threading.Thread.Sleep(ASSET_REFRESH_DELAY_MS);
1096 |             
1097 |             return new
1098 |             {
1099 |                 sourcePath = sourcePath,
1100 |                 targetPath = targetPath,
1101 |                 guid = AssetDatabase.AssetPathToGUID(targetPath)
1102 |             };
1103 |         }
1104 |         
1105 |         static object MoveFolderOnWorkerThread(JObject request)
1106 |         {
1107 |             var sourcePath = request["sourcePath"]?.ToString();
1108 |             var targetPath = request["targetPath"]?.ToString();
1109 |             
1110 |             if (string.IsNullOrEmpty(sourcePath))
1111 |                 throw new ArgumentException("sourcePath is required");
1112 |             if (string.IsNullOrEmpty(targetPath))
1113 |                 throw new ArgumentException("targetPath is required");
1114 |             
1115 |             var sourceFullPath = Path.Combine(Application.dataPath, sourcePath.Substring(ASSETS_PREFIX_LENGTH));
1116 |             var targetFullPath = Path.Combine(Application.dataPath, targetPath.Substring(ASSETS_PREFIX_LENGTH));
1117 |             
1118 |             if (!Directory.Exists(sourceFullPath))
1119 |                 throw new DirectoryNotFoundException($"Directory not found: {sourcePath}");
1120 |             
1121 |             // Ensure target parent directory exists
1122 |             var targetParent = Path.GetDirectoryName(targetFullPath);
1123 |             if (!Directory.Exists(targetParent))
1124 |                 Directory.CreateDirectory(targetParent);
1125 |             
1126 |             Directory.Move(sourceFullPath, targetFullPath);
1127 |             
1128 |             return new
1129 |             {
1130 |                 sourcePath = sourcePath,
1131 |                 targetPath = targetPath,
1132 |                 guid = "" // GUID requires AssetDatabase
1133 |             };
1134 |         }
1135 |         
1136 |         static object DeleteFolder(JObject request)
1137 |         {
1138 |             var path = request["path"]?.ToString();
1139 |             var recursive = request["recursive"]?.Value<bool>() ?? true;
1140 |             
1141 |             if (string.IsNullOrEmpty(path))
1142 |                 throw new ArgumentException("path is required");
1143 |             
1144 |             var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH));
1145 |             if (!Directory.Exists(fullPath))
1146 |                 throw new DirectoryNotFoundException($"Directory not found: {path}");
1147 |             
1148 |             if (!AssetDatabase.DeleteAsset(path))
1149 |                 throw new InvalidOperationException($"Failed to delete folder: {path}");
1150 |             
1151 |             // Wait for asset database to process deletion
1152 |             System.Threading.Thread.Sleep(ASSET_REFRESH_DELAY_MS);
1153 |             
1154 |             return new { path = path };
1155 |         }
1156 |         
1157 |         static object DeleteFolderOnWorkerThread(JObject request)
1158 |         {
1159 |             var path = request["path"]?.ToString();
1160 |             var recursive = request["recursive"]?.Value<bool>() ?? true;
1161 |             
1162 |             if (string.IsNullOrEmpty(path))
1163 |                 throw new ArgumentException("path is required");
1164 |             
1165 |             var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH));
1166 |             if (!Directory.Exists(fullPath))
1167 |                 throw new DirectoryNotFoundException($"Directory not found: {path}");
1168 |             
1169 |             Directory.Delete(fullPath, recursive);
1170 |             
1171 |             // Also delete .meta file
1172 |             var metaPath = fullPath + ".meta";
1173 |             if (File.Exists(metaPath))
1174 |                 File.Delete(metaPath);
1175 |             
1176 |             return new { path = path };
1177 |         }
1178 |         
1179 |         static object ListFolder(JObject request)
1180 |         {
1181 |             var path = request["path"]?.ToString() ?? ASSETS_PREFIX;
1182 |             var recursive = request["recursive"]?.Value<bool>() ?? false;
1183 |             
1184 |             var fullPath = Path.Combine(Application.dataPath, path.StartsWith(ASSETS_PREFIX) ? path.Substring(ASSETS_PREFIX_LENGTH) : path);
1185 |             if (!Directory.Exists(fullPath))
1186 |                 throw new DirectoryNotFoundException($"Directory not found: {path}");
1187 |             
1188 |             var entries = new List<object>();
1189 |             
1190 |             // Get directories
1191 |             var dirs = Directory.GetDirectories(fullPath, "*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
1192 |             foreach (var dir in dirs)
1193 |             {
1194 |                 var relativePath = ASSETS_PREFIX + GetRelativePath(Application.dataPath, dir);
1195 |                 entries.Add(new
1196 |                 {
1197 |                     path = relativePath,
1198 |                     name = Path.GetFileName(dir),
1199 |                     type = "folder",
1200 |                     guid = AssetDatabase.AssetPathToGUID(relativePath)
1201 |                 });
1202 |             }
1203 |             
1204 |             // Get files
1205 |             var files = Directory.GetFiles(fullPath, "*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)
1206 |                                  .Where(f => !f.EndsWith(".meta"));
1207 |             foreach (var file in files)
1208 |             {
1209 |                 var relativePath = ASSETS_PREFIX + GetRelativePath(Application.dataPath, file);
1210 |                 entries.Add(new
1211 |                 {
1212 |                     path = relativePath,
1213 |                     name = Path.GetFileName(file),
1214 |                     type = "file",
1215 |                     extension = Path.GetExtension(file),
1216 |                     guid = AssetDatabase.AssetPathToGUID(relativePath)
1217 |                 });
1218 |             }
1219 |             
1220 |             return new
1221 |             {
1222 |                 path = path,
1223 |                 entries = entries
1224 |             };
1225 |         }
1226 |         
1227 |         static object ListFolderOnWorkerThread(JObject request)
1228 |         {
1229 |             var path = request["path"]?.ToString() ?? ASSETS_PREFIX;
1230 |             var recursive = request["recursive"]?.Value<bool>() ?? false;
1231 |             
1232 |             var fullPath = Path.Combine(Application.dataPath, path.StartsWith(ASSETS_PREFIX) ? path.Substring(ASSETS_PREFIX_LENGTH) : path);
1233 |             if (!Directory.Exists(fullPath))
1234 |                 throw new DirectoryNotFoundException($"Directory not found: {path}");
1235 |             
1236 |             var entries = new List<object>();
1237 |             
1238 |             // Get directories
1239 |             var dirs = Directory.GetDirectories(fullPath, "*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
1240 |             foreach (var dir in dirs)
1241 |             {
1242 |                 var relativePath = ASSETS_PREFIX + GetRelativePath(Application.dataPath, dir);
1243 |                 entries.Add(new
1244 |                 {
1245 |                     path = relativePath,
1246 |                     name = Path.GetFileName(dir),
1247 |                     type = "folder",
1248 |                     guid = "" // GUID requires AssetDatabase
1249 |                 });
1250 |             }
1251 |             
1252 |             // Get files
1253 |             var files = Directory.GetFiles(fullPath, "*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)
1254 |                                  .Where(f => !f.EndsWith(".meta"));
1255 |             foreach (var file in files)
1256 |             {
1257 |                 var relativePath = ASSETS_PREFIX + GetRelativePath(Application.dataPath, file);
1258 |                 entries.Add(new
1259 |                 {
1260 |                     path = relativePath,
1261 |                     name = Path.GetFileName(file),
1262 |                     type = "file",
1263 |                     extension = Path.GetExtension(file),
1264 |                     guid = "" // GUID requires AssetDatabase
1265 |                 });
1266 |             }
1267 |             
1268 |             return new
1269 |             {
1270 |                 path = path,
1271 |                 entries = entries
1272 |             };
1273 |         }
1274 |         
1275 |         static string GetRelativePath(string basePath, string fullPath)
1276 |         {
1277 |             if (!fullPath.StartsWith(basePath))
1278 |                 return fullPath;
1279 |             
1280 |             var relativePath = fullPath.Substring(basePath.Length);
1281 |             if (relativePath.StartsWith(Path.DirectorySeparatorChar.ToString()))
1282 |                 relativePath = relativePath.Substring(1);
1283 |             
1284 |             return relativePath.Replace(Path.DirectorySeparatorChar, '/');
1285 |         }
1286 |         
1287 |         static void SendResponse(HttpListenerResponse response, int statusCode, bool success, object result, string error)
1288 |         {
1289 |             response.StatusCode = statusCode;
1290 |             response.ContentType = "application/json; charset=utf-8";
1291 |             response.ContentEncoding = Encoding.UTF8;
1292 |             
1293 |             var responseData = new Dictionary<string, object>
1294 |             {
1295 |                 ["success"] = success
1296 |             };
1297 |             
1298 |             if (result != null)
1299 |                 responseData["result"] = result;
1300 |             
1301 |             if (!string.IsNullOrEmpty(error))
1302 |                 responseData["error"] = error;
1303 |             
1304 |             var json = JsonConvert.SerializeObject(responseData);
1305 |             var buffer = Encoding.UTF8.GetBytes(json);
1306 |             
1307 |             response.ContentLength64 = buffer.Length;
1308 |             response.OutputStream.Write(buffer, 0, buffer.Length);
1309 |             response.Close();
1310 |         }
1311 |     }
1312 | }`
1313 |     });
1314 | 
1315 |     // UnityMCPServerWindow.cs content
1316 |     this.scripts.set('UnityMCPServerWindow.cs', {
1317 |       fileName: 'UnityMCPServerWindow.cs',
1318 |       version: '1.0.0',
1319 |       content: `using System;
1320 | using UnityEngine;
1321 | using UnityEditor;
1322 | 
1323 | namespace UnityMCP
1324 | {
1325 |     /// <summary>
1326 |     /// Unity MCP Server control window
1327 |     /// </summary>
1328 |     public class UnityMCPServerWindow : EditorWindow
1329 |     {
1330 |         // Version information (should match UnityHttpServer)
1331 |         private const string SCRIPT_VERSION = "1.1.0";
1332 |         
1333 |         private int serverPort = 23457;
1334 |         private bool isServerRunning = false;
1335 |         private string serverStatus = "Stopped";
1336 |         private string lastError = "";
1337 |         
1338 |         [MenuItem("Window/Unity MCP Server")]
1339 |         public static void ShowWindow()
1340 |         {
1341 |             GetWindow<UnityMCPServerWindow>("Unity MCP Server");
1342 |         }
1343 |         
1344 |         void OnEnable()
1345 |         {
1346 |             // Load saved settings
1347 |             serverPort = EditorPrefs.GetInt("UnityMCP.ServerPort", 23457);
1348 |             UpdateStatus();
1349 |         }
1350 |         
1351 |         void OnDisable()
1352 |         {
1353 |             // Save settings
1354 |             EditorPrefs.SetInt("UnityMCP.ServerPort", serverPort);
1355 |         }
1356 |         
1357 |         void OnGUI()
1358 |         {
1359 |             GUILayout.Label("Unity MCP Server Control", EditorStyles.boldLabel);
1360 |             GUILayout.Label($"Version: {SCRIPT_VERSION}", EditorStyles.miniLabel);
1361 |             
1362 |             EditorGUILayout.Space();
1363 |             
1364 |             // Server Status
1365 |             EditorGUILayout.BeginHorizontal();
1366 |             GUILayout.Label("Status:", GUILayout.Width(60));
1367 |             var statusColor = isServerRunning ? Color.green : Color.red;
1368 |             var originalColor = GUI.color;
1369 |             GUI.color = statusColor;
1370 |             GUILayout.Label(serverStatus, EditorStyles.boldLabel);
1371 |             GUI.color = originalColor;
1372 |             EditorGUILayout.EndHorizontal();
1373 |             
1374 |             EditorGUILayout.Space();
1375 |             
1376 |             // Port Configuration
1377 |             EditorGUILayout.BeginHorizontal();
1378 |             GUILayout.Label("Port:", GUILayout.Width(60));
1379 |             var newPort = EditorGUILayout.IntField(serverPort);
1380 |             if (newPort != serverPort && newPort > 0 && newPort <= 65535)
1381 |             {
1382 |                 serverPort = newPort;
1383 |                 EditorPrefs.SetInt("UnityMCP.ServerPort", serverPort);
1384 |             }
1385 |             EditorGUILayout.EndHorizontal();
1386 |             
1387 |             // Port validation
1388 |             if (serverPort < 1024)
1389 |             {
1390 |                 EditorGUILayout.HelpBox("Warning: Ports below 1024 may require administrator privileges.", MessageType.Warning);
1391 |             }
1392 |             
1393 |             EditorGUILayout.Space();
1394 |             
1395 |             // Control Buttons
1396 |             EditorGUILayout.BeginHorizontal();
1397 |             
1398 |             GUI.enabled = !isServerRunning;
1399 |             if (GUILayout.Button("Start Server", GUILayout.Height(30)))
1400 |             {
1401 |                 StartServer();
1402 |             }
1403 |             
1404 |             GUI.enabled = isServerRunning;
1405 |             if (GUILayout.Button("Stop Server", GUILayout.Height(30)))
1406 |             {
1407 |                 StopServer();
1408 |             }
1409 |             
1410 |             GUI.enabled = true;
1411 |             EditorGUILayout.EndHorizontal();
1412 |             
1413 |             EditorGUILayout.Space();
1414 |             
1415 |             // Connection Info
1416 |             if (isServerRunning)
1417 |             {
1418 |                 EditorGUILayout.BeginVertical(EditorStyles.helpBox);
1419 |                 GUILayout.Label("Connection Information", EditorStyles.boldLabel);
1420 |                 EditorGUILayout.SelectableLabel($"http://localhost:{serverPort}/");
1421 |                 EditorGUILayout.EndVertical();
1422 |             }
1423 |             
1424 |             // Error Display
1425 |             if (!string.IsNullOrEmpty(lastError))
1426 |             {
1427 |                 EditorGUILayout.Space();
1428 |                 EditorGUILayout.HelpBox(lastError, MessageType.Error);
1429 |                 if (GUILayout.Button("Clear Error"))
1430 |                 {
1431 |                     lastError = "";
1432 |                 }
1433 |             }
1434 |             
1435 |             EditorGUILayout.Space();
1436 |             
1437 |             // Instructions
1438 |             EditorGUILayout.BeginVertical(EditorStyles.helpBox);
1439 |             GUILayout.Label("Instructions", EditorStyles.boldLabel);
1440 |             GUILayout.Label("1. Configure the port (default: 23457)");
1441 |             GUILayout.Label("2. Click 'Start Server' to begin");
1442 |             GUILayout.Label("3. Use the MCP client to connect");
1443 |             GUILayout.Label("4. Click 'Stop Server' when done");
1444 |             EditorGUILayout.EndVertical();
1445 |         }
1446 |         
1447 |         void StartServer()
1448 |         {
1449 |             try
1450 |             {
1451 |                 UnityHttpServer.Start(serverPort);
1452 |                 UpdateStatus();
1453 |                 lastError = "";
1454 |                 Debug.Log($"[UnityMCP] Server started on port {serverPort}");
1455 |             }
1456 |             catch (Exception e)
1457 |             {
1458 |                 lastError = $"Failed to start server: {e.Message}";
1459 |                 Debug.LogError($"[UnityMCP] {lastError}");
1460 |             }
1461 |         }
1462 |         
1463 |         void StopServer()
1464 |         {
1465 |             try
1466 |             {
1467 |                 UnityHttpServer.Shutdown();
1468 |                 UpdateStatus();
1469 |                 lastError = "";
1470 |                 Debug.Log("[UnityMCP] Server stopped");
1471 |             }
1472 |             catch (Exception e)
1473 |             {
1474 |                 lastError = $"Failed to stop server: {e.Message}";
1475 |                 Debug.LogError($"[UnityMCP] {lastError}");
1476 |             }
1477 |         }
1478 |         
1479 |         void UpdateStatus()
1480 |         {
1481 |             isServerRunning = UnityHttpServer.IsRunning;
1482 |             serverStatus = isServerRunning ? $"Running on port {UnityHttpServer.CurrentPort}" : "Stopped";
1483 |             Repaint();
1484 |         }
1485 |         
1486 |         void Update()
1487 |         {
1488 |             // Update status periodically
1489 |             UpdateStatus();
1490 |         }
1491 |     }
1492 | }`
1493 |     });
1494 |   }
1495 | 
1496 |   /**
1497 |    * Get script by filename
1498 |    */
1499 |   async getScript(fileName: string): Promise<EmbeddedScript | null> {
1500 |     return this.scripts.get(fileName) || null;
1501 |   }
1502 | 
1503 |   /**
1504 |    * Get script synchronously
1505 |    */
1506 |   getScriptSync(fileName: string): EmbeddedScript | null {
1507 |     return this.scripts.get(fileName) || null;
1508 |   }
1509 | 
1510 |   /**
1511 |    * Write script to file with proper UTF-8 BOM for Unity compatibility
1512 |    */
1513 |   async writeScriptToFile(fileName: string, targetPath: string): Promise<void> {
1514 |     const script = await this.getScript(fileName);
1515 |     if (!script) {
1516 |       throw new Error(`Script not found: ${fileName}`);
1517 |     }
1518 | 
1519 |     // Ensure target directory exists
1520 |     await fs.mkdir(path.dirname(targetPath), { recursive: true });
1521 |     
1522 |     // Write with UTF-8 BOM for Unity compatibility
1523 |     const utf8BOM = Buffer.from([0xEF, 0xBB, 0xBF]);
1524 |     const contentBuffer = Buffer.from(script.content, 'utf8');
1525 |     const finalBuffer = Buffer.concat([utf8BOM, contentBuffer]);
1526 |     
1527 |     await fs.writeFile(targetPath, finalBuffer);
1528 |   }
1529 | 
1530 |   /**
1531 |    * Get all available script names
1532 |    */
1533 |   getAvailableScripts(): string[] {
1534 |     return Array.from(this.scripts.keys());
1535 |   }
1536 | 
1537 |   /**
1538 |    * Get script version
1539 |    */
1540 |   getScriptVersion(fileName: string): string | null {
1541 |     const script = this.scripts.get(fileName);
1542 |     return script?.version || null;
1543 |   }
1544 | }
```
Page 2/3FirstPrevNextLast