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 | } ```