Skip to main content
Glama
Singtaa
by Singtaa
NodeProcessManager.cs14.1 kB
using System; using System.Diagnostics; using System.IO; using System.Threading.Tasks; using UnityEditor; using UnityEngine; using Debug = UnityEngine.Debug; namespace UnityMcp { /// <summary> /// Manages the Node.js MCP server process lifecycle. /// Handles automatic startup, npm install, and graceful shutdown. /// </summary> public static class NodeProcessManager { static Process _serverProcess; static string _serverPath; static bool _isStarting; public static bool IsRunning => _serverProcess != null && !_serverProcess.HasExited; public static bool IsStarting => _isStarting; public static string ServerPath => _serverPath; public static event Action OnServerStarted; public static event Action OnServerStopped; public static event Action<string> OnServerOutput; public static event Action<string> OnServerError; // MARK: Public API public static async Task<bool> EnsureServerRunning() { if (IsRunning) return true; if (_isStarting) return false; _isStarting = true; try { // 1. Find Server~ folder _serverPath = FindServerFolder(); if (_serverPath == null) { Debug.LogError("[UnityMcp] Server~ folder not found in package"); return false; } // 2. Check Node.js availability if (!await IsNodeInstalled()) { Debug.LogError("[UnityMcp] Node.js not found. Please install Node.js 18+ from https://nodejs.org"); return false; } // 3. Check/install node_modules var nodeModulesPath = Path.Combine(_serverPath, "node_modules"); if (!Directory.Exists(nodeModulesPath)) { Debug.Log("[UnityMcp] Installing dependencies (first run)..."); if (!await RunNpmInstall()) { Debug.LogError("[UnityMcp] npm install failed"); return false; } Debug.Log("[UnityMcp] Dependencies installed successfully"); } // 4. Check if server already running (another Unity instance?) if (await IsPortInUse(McpSettings.HttpPort)) { Debug.Log($"[UnityMcp] Server already running on port {McpSettings.HttpPort}"); return true; } // 5. Start server return StartServer(); } finally { _isStarting = false; } } public static void StopServer() { if (_serverProcess != null && !_serverProcess.HasExited) { try { _serverProcess.Kill(); _serverProcess.WaitForExit(1000); } catch (Exception e) { Debug.LogWarning($"[UnityMcp] Error stopping server: {e.Message}"); } } if (_serverProcess != null) { _serverProcess.Dispose(); _serverProcess = null; } OnServerStopped?.Invoke(); } public static void RestartServer() { StopServer(); EditorApplication.delayCall += async () => { await Task.Delay(500); // Brief delay to ensure port is released await EnsureServerRunning(); }; } // MARK: Internal static string FindServerFolder() { // Look for Server~ in the package folder var packagePath = GetPackagePath(); if (string.IsNullOrEmpty(packagePath)) return null; var serverPath = Path.Combine(packagePath, "Server~"); if (Directory.Exists(serverPath)) { return serverPath; } return null; } static string GetPackagePath() { // Find the package path by looking for our assembly var assembly = typeof(NodeProcessManager).Assembly; var assemblyPath = assembly.Location; // Navigate up from the assembly location to find the package root // Assembly is typically in Library/ScriptAssemblies/ var projectRoot = ProjectPaths.ProjectRoot; // Check Packages folder var packagesPath = Path.Combine(projectRoot, "Packages", "com.singtaa.unity-mcp"); if (Directory.Exists(packagesPath)) return packagesPath; // Check Library/PackageCache for installed packages var packageCachePath = Path.Combine(projectRoot, "Library", "PackageCache"); if (Directory.Exists(packageCachePath)) { foreach (var dir in Directory.GetDirectories(packageCachePath)) { if (Path.GetFileName(dir).StartsWith("com.singtaa.unity-mcp@")) { return dir; } } } return null; } static async Task<bool> IsNodeInstalled() { try { var psi = new ProcessStartInfo { FileName = GetNodeExecutable(), Arguments = "--version", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; using (var process = Process.Start(psi)) { if (process == null) return false; var output = await process.StandardOutput.ReadToEndAsync(); process.WaitForExit(5000); if (process.ExitCode == 0) { var version = output.Trim(); Debug.Log($"[UnityMcp] Found Node.js {version}"); return true; } } } catch { // Node not found } return false; } static async Task<bool> RunNpmInstall() { try { var psi = new ProcessStartInfo { FileName = GetNpmExecutable(), Arguments = "install", WorkingDirectory = _serverPath, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; // Ensure PATH includes the node/npm directory EnsureNodeInPath(psi); using (var process = Process.Start(psi)) { if (process == null) return false; var outputTask = process.StandardOutput.ReadToEndAsync(); var errorTask = process.StandardError.ReadToEndAsync(); process.WaitForExit(60000); // 60 second timeout for npm install var output = await outputTask; var error = await errorTask; if (process.ExitCode != 0) { Debug.LogError($"[UnityMcp] npm install failed:\n{error}"); return false; } return true; } } catch (Exception e) { Debug.LogError($"[UnityMcp] npm install exception: {e.Message}"); return false; } } static async Task<bool> IsPortInUse(int port) { try { using (var client = new System.Net.Sockets.TcpClient()) { var connectTask = client.ConnectAsync("127.0.0.1", port); var timeoutTask = Task.Delay(1000); var completedTask = await Task.WhenAny(connectTask, timeoutTask); if (completedTask == connectTask && client.Connected) { return true; } } } catch { // Port not in use or connection refused } return false; } static bool StartServer() { try { var psi = new ProcessStartInfo { FileName = GetNodeExecutable(), Arguments = "src/server.js", WorkingDirectory = _serverPath, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; // Ensure PATH includes the node directory EnsureNodeInPath(psi); // Set environment variables psi.Environment["MCP_HTTP_PORT"] = McpSettings.HttpPort.ToString(); psi.Environment["MCP_IPC_PORT"] = McpSettings.IpcPort.ToString(); psi.Environment["MCP_REQUIRE_AUTH"] = McpSettings.AuthEnabled ? "true" : "false"; psi.Environment["MCP_TOKEN"] = McpSettings.AuthToken; _serverProcess = Process.Start(psi); if (_serverProcess == null) { Debug.LogError("[UnityMcp] Failed to start Node.js server process"); return false; } _serverProcess.OutputDataReceived += (s, e) => { if (!string.IsNullOrEmpty(e.Data)) { Debug.Log($"[MCP Server] {e.Data}"); OnServerOutput?.Invoke(e.Data); } }; _serverProcess.ErrorDataReceived += (s, e) => { if (!string.IsNullOrEmpty(e.Data)) { Debug.LogWarning($"[MCP Server] {e.Data}"); OnServerError?.Invoke(e.Data); } }; _serverProcess.EnableRaisingEvents = true; _serverProcess.Exited += (s, e) => { Debug.Log("[UnityMcp] Server process exited"); OnServerStopped?.Invoke(); }; _serverProcess.BeginOutputReadLine(); _serverProcess.BeginErrorReadLine(); // Register cleanup handlers EditorApplication.quitting -= OnEditorQuitting; EditorApplication.quitting += OnEditorQuitting; AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeReload; AssemblyReloadEvents.beforeAssemblyReload += OnBeforeReload; OnServerStarted?.Invoke(); Debug.Log($"[UnityMcp] Server started on port {McpSettings.HttpPort}"); return true; } catch (Exception e) { Debug.LogError($"[UnityMcp] Failed to start server: {e.Message}"); return false; } } static void OnEditorQuitting() { StopServer(); } static void OnBeforeReload() { // Don't stop the server on domain reload - let it keep running // The TCP client will reconnect after reload } static string GetNodeExecutable() { // On Windows, just use "node" and let PATH resolve it // On macOS/Linux, check common locations if (Application.platform == RuntimePlatform.WindowsEditor) { return "node"; } // Check common macOS/Linux Node.js locations var commonPaths = new[] { "/usr/local/bin/node", "/opt/homebrew/bin/node", "/usr/bin/node", Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nvm/versions/node"), }; foreach (var path in commonPaths) { if (path.Contains(".nvm")) { // For nvm, find the latest version if (Directory.Exists(path)) { var versions = Directory.GetDirectories(path); if (versions.Length > 0) { Array.Sort(versions); var latestNode = Path.Combine(versions[versions.Length - 1], "bin/node"); if (File.Exists(latestNode)) return latestNode; } } } else if (File.Exists(path)) { return path; } } // Fall back to PATH return "node"; } static string GetNpmExecutable() { if (Application.platform == RuntimePlatform.WindowsEditor) { return "npm"; } var nodePath = GetNodeExecutable(); if (nodePath != "node") { // Use npm from same directory as node var nodeDir = Path.GetDirectoryName(nodePath); var npmPath = Path.Combine(nodeDir, "npm"); if (File.Exists(npmPath)) return npmPath; } return "npm"; } static void EnsureNodeInPath(ProcessStartInfo psi) { if (Application.platform == RuntimePlatform.WindowsEditor) return; var nodePath = GetNodeExecutable(); if (nodePath == "node") return; var nodeDir = Path.GetDirectoryName(nodePath); if (string.IsNullOrEmpty(nodeDir)) return; // Get current PATH or use a sensible default var currentPath = psi.Environment.ContainsKey("PATH") ? psi.Environment["PATH"] : Environment.GetEnvironmentVariable("PATH") ?? "/usr/bin:/bin:/usr/sbin:/sbin"; // Prepend the node directory to PATH if (!currentPath.Contains(nodeDir)) { psi.Environment["PATH"] = $"{nodeDir}:{currentPath}"; } } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Singtaa/UnityMCP'

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