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