Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions MCPForUnity/Editor/Helpers/PortManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,17 @@ public static int GetPortWithFallback()
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Stored port {storedConfig.unity_port} became available after short wait");
return storedConfig.unity_port;
}
// Prefer sticking to the same port; let the caller handle bind retries/fallbacks
return storedConfig.unity_port;
// Port is still busy after waiting - find a new available port instead
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Stored port {storedConfig.unity_port} is occupied by another instance, finding alternative...");
int newPort = FindAvailablePort();
SavePort(newPort);
return newPort;
}

// If no valid stored port, find a new one and save it
int newPort = FindAvailablePort();
SavePort(newPort);
return newPort;
int foundPort = FindAvailablePort();
SavePort(foundPort);
return foundPort;
}

/// <summary>
Expand Down
58 changes: 58 additions & 0 deletions MCPForUnity/Editor/MCPForUnityBridge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,24 @@ public static void Start()
}
catch (SocketException se) when (se.SocketErrorCode == SocketError.AddressAlreadyInUse && attempt >= maxImmediateRetries)
{
// Port is occupied by another instance, get a new available port
int oldPort = currentUnityPort;
currentUnityPort = PortManager.GetPortWithFallback();

// GetPortWithFallback() may return the same port if it became available during wait
// or a different port if switching to an alternative
if (IsDebugEnabled())
{
if (currentUnityPort == oldPort)
{
McpLog.Info($"Port {oldPort} became available, proceeding");
}
else
{
McpLog.Info($"Port {oldPort} occupied, switching to port {currentUnityPort}");
}
}

listener = new TcpListener(IPAddress.Loopback, currentUnityPort);
listener.Server.SetSocketOption(
SocketOptionLevel.Socket,
Expand Down Expand Up @@ -474,6 +491,22 @@ public static void Stop()
try { AssemblyReloadEvents.afterAssemblyReload -= OnAfterAssemblyReload; } catch { }
try { EditorApplication.quitting -= Stop; } catch { }

// Clean up status file when Unity stops
try
{
string statusDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unity-mcp");
string statusFile = Path.Combine(statusDir, $"unity-mcp-status-{ComputeProjectHash(Application.dataPath)}.json");
if (File.Exists(statusFile))
{
File.Delete(statusFile);
if (IsDebugEnabled()) McpLog.Info($"Deleted status file: {statusFile}");
}
}
catch (Exception ex)
{
if (IsDebugEnabled()) McpLog.Warn($"Failed to delete status file: {ex.Message}");
}

if (IsDebugEnabled()) McpLog.Info("MCPForUnityBridge stopped.");
}

Expand Down Expand Up @@ -1184,13 +1217,38 @@ private static void WriteHeartbeat(bool reloading, string reason = null)
}
Directory.CreateDirectory(dir);
string filePath = Path.Combine(dir, $"unity-mcp-status-{ComputeProjectHash(Application.dataPath)}.json");

// Extract project name from path
string projectName = "Unknown";
try
{
string projectPath = Application.dataPath;
if (!string.IsNullOrEmpty(projectPath))
{
// Remove trailing /Assets or \Assets
projectPath = projectPath.TrimEnd('/', '\\');
if (projectPath.EndsWith("Assets", StringComparison.OrdinalIgnoreCase))
{
projectPath = projectPath.Substring(0, projectPath.Length - 6).TrimEnd('/', '\\');
}
projectName = Path.GetFileName(projectPath);
if (string.IsNullOrEmpty(projectName))
{
projectName = "Unknown";
}
}
}
catch { }

var payload = new
{
unity_port = currentUnityPort,
reloading,
reason = reason ?? (reloading ? "reloading" : "ready"),
seq = heartbeatSeq,
project_path = Application.dataPath,
project_name = projectName,
unity_version = Application.unityVersion,
last_heartbeat = DateTime.UtcNow.ToString("O")
};
File.WriteAllText(filePath, JsonConvert.SerializeObject(payload), new System.Text.UTF8Encoding(false));
Expand Down
58 changes: 50 additions & 8 deletions MCPForUnity/Editor/Tools/ManageAsset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -911,7 +911,7 @@ private static bool ApplyMaterialProperties(Material mat, JObject properties)
// Example: Set color property
if (properties["color"] is JObject colorProps)
{
string propName = colorProps["name"]?.ToString() ?? "_Color"; // Default main color
string propName = colorProps["name"]?.ToString() ?? GetMainColorPropertyName(mat); // Auto-detect if not specified
if (colorProps["value"] is JArray colArr && colArr.Count >= 3)
{
try
Expand All @@ -922,10 +922,20 @@ private static bool ApplyMaterialProperties(Material mat, JObject properties)
colArr[2].ToObject<float>(),
colArr.Count > 3 ? colArr[3].ToObject<float>() : 1.0f
);
if (mat.HasProperty(propName) && mat.GetColor(propName) != newColor)
if (mat.HasProperty(propName))
{
mat.SetColor(propName, newColor);
modified = true;
if (mat.GetColor(propName) != newColor)
{
mat.SetColor(propName, newColor);
modified = true;
}
}
else
{
Debug.LogWarning(
$"Material '{mat.name}' with shader '{mat.shader.name}' does not have color property '{propName}'. " +
$"Color not applied. Common color properties: _BaseColor (URP), _Color (Standard)"
);
}
}
catch (Exception ex)
Expand All @@ -938,7 +948,8 @@ private static bool ApplyMaterialProperties(Material mat, JObject properties)
}
else if (properties["color"] is JArray colorArr) //Use color now with examples set in manage_asset.py
{
string propName = "_Color";
// Auto-detect the main color property for the shader
string propName = GetMainColorPropertyName(mat);
try
{
if (colorArr.Count >= 3)
Expand All @@ -949,10 +960,20 @@ private static bool ApplyMaterialProperties(Material mat, JObject properties)
colorArr[2].ToObject<float>(),
colorArr.Count > 3 ? colorArr[3].ToObject<float>() : 1.0f
);
if (mat.HasProperty(propName) && mat.GetColor(propName) != newColor)
if (mat.HasProperty(propName))
{
mat.SetColor(propName, newColor);
modified = true;
if (mat.GetColor(propName) != newColor)
{
mat.SetColor(propName, newColor);
modified = true;
}
}
else
{
Debug.LogWarning(
$"Material '{mat.name}' with shader '{mat.shader.name}' does not have color property '{propName}'. " +
$"Color not applied. Common color properties: _BaseColor (URP), _Color (Standard)"
);
}
}
}
Expand Down Expand Up @@ -1140,6 +1161,27 @@ string ResolvePropertyName(string name)
return modified;
}

/// <summary>
/// Auto-detects the main color property name for a material's shader.
/// Tries common color property names in order: _BaseColor (URP), _Color (Standard), etc.
/// </summary>
private static string GetMainColorPropertyName(Material mat)
{
if (mat == null || mat.shader == null)
return "_Color";

// Try common color property names in order of likelihood
string[] commonColorProps = { "_BaseColor", "_Color", "_MainColor", "_Tint", "_TintColor" };
foreach (var prop in commonColorProps)
{
if (mat.HasProperty(prop))
return prop;
}

// Fallback to _Color if none found
return "_Color";
}

/// <summary>
/// Applies properties from JObject to a PhysicsMaterial.
/// </summary>
Expand Down
26 changes: 26 additions & 0 deletions MCPForUnity/UnityMcpServer~/src/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Any
from datetime import datetime
from pydantic import BaseModel


Expand All @@ -7,3 +8,28 @@ class MCPResponse(BaseModel):
message: str | None = None
error: str | None = None
data: Any | None = None


class UnityInstanceInfo(BaseModel):
"""Information about a Unity Editor instance"""
id: str # "ProjectName@hash" or fallback to hash
name: str # Project name extracted from path
path: str # Full project path (Assets folder)
hash: str # 8-char hash of project path
port: int # TCP port
status: str # "running", "reloading", "offline"
last_heartbeat: datetime | None = None
unity_version: str | None = None

def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary for JSON serialization"""
return {
"id": self.id,
"name": self.name,
"path": self.path,
"hash": self.hash,
"port": self.port,
"status": self.status,
"last_heartbeat": self.last_heartbeat.isoformat() if self.last_heartbeat else None,
"unity_version": self.unity_version
}
Loading