Skip to content

Commit b9cb600

Browse files
📝 Add docstrings to sakura/multiple-instance
Docstrings generation was requested by @sakura-habby. * #360 (comment) The following files were modified: * `MCPForUnity/Editor/Helpers/PortManager.cs` * `MCPForUnity/Editor/MCPForUnityBridge.cs` * `MCPForUnity/UnityMcpServer~/src/models.py` * `MCPForUnity/UnityMcpServer~/src/port_discovery.py` * `MCPForUnity/UnityMcpServer~/src/resources/__init__.py` * `MCPForUnity/UnityMcpServer~/src/resources/menu_items.py` * `MCPForUnity/UnityMcpServer~/src/resources/tests.py` * `MCPForUnity/UnityMcpServer~/src/server.py` * `MCPForUnity/UnityMcpServer~/src/tools/execute_menu_item.py` * `MCPForUnity/UnityMcpServer~/src/tools/manage_asset.py` * `MCPForUnity/UnityMcpServer~/src/tools/manage_editor.py` * `MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.py` * `MCPForUnity/UnityMcpServer~/src/tools/manage_prefabs.py` * `MCPForUnity/UnityMcpServer~/src/tools/manage_scene.py` * `MCPForUnity/UnityMcpServer~/src/tools/manage_script.py` * `MCPForUnity/UnityMcpServer~/src/tools/manage_shader.py` * `MCPForUnity/UnityMcpServer~/src/tools/read_console.py` * `MCPForUnity/UnityMcpServer~/src/tools/resource_tools.py` * `MCPForUnity/UnityMcpServer~/src/tools/run_tests.py` * `MCPForUnity/UnityMcpServer~/src/tools/script_apply_edits.py` * `MCPForUnity/UnityMcpServer~/src/unity_connection.py` * `Server/models.py` * `Server/port_discovery.py` * `Server/resources/__init__.py` * `Server/resources/menu_items.py` * `Server/resources/tests.py` * `Server/server.py` * `Server/tools/execute_menu_item.py` * `Server/tools/list_unity_instances.py` * `Server/tools/manage_asset.py` * `Server/tools/manage_editor.py` * `Server/tools/manage_gameobject.py` * `Server/tools/manage_prefabs.py` * `Server/tools/manage_scene.py` * `Server/tools/manage_script.py` * `Server/tools/manage_shader.py` * `Server/tools/read_console.py` * `Server/tools/resource_tools.py` * `Server/tools/run_tests.py` * `Server/tools/script_apply_edits.py` * `Server/unity_connection.py`
1 parent 8f227ff commit b9cb600

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2673
-324
lines changed

MCPForUnity/Editor/Helpers/PortManager.cs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ public class PortConfig
3838
/// Get the port to use - either from storage or discover a new one
3939
/// Will try stored port first, then fallback to discovering new port
4040
/// </summary>
41-
/// <returns>Port number to use</returns>
41+
/// <summary>
42+
/// Selects a TCP port for the current Unity project, preferring a previously saved project-specific port when it is valid and free; if the saved port is busy the method waits briefly for release and otherwise finds and persists an alternative.
43+
/// </summary>
44+
/// <returns>The port number to use. Returns the stored project port if it exists and is available (or becomes available after a short wait); otherwise returns a newly discovered available port which is saved for future use.</returns>
4245
public static int GetPortWithFallback()
4346
{
4447
// Try to load stored port first, but only if it's from the current project
@@ -60,14 +63,17 @@ public static int GetPortWithFallback()
6063
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Stored port {storedConfig.unity_port} became available after short wait");
6164
return storedConfig.unity_port;
6265
}
63-
// Prefer sticking to the same port; let the caller handle bind retries/fallbacks
64-
return storedConfig.unity_port;
66+
// Port is still busy after waiting - find a new available port instead
67+
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Stored port {storedConfig.unity_port} is occupied by another instance, finding alternative...");
68+
int newPort = FindAvailablePort();
69+
SavePort(newPort);
70+
return newPort;
6571
}
6672

6773
// If no valid stored port, find a new one and save it
68-
int newPort = FindAvailablePort();
69-
SavePort(newPort);
70-
return newPort;
74+
int foundPort = FindAvailablePort();
75+
SavePort(foundPort);
76+
return foundPort;
7177
}
7278

7379
/// <summary>
@@ -316,4 +322,4 @@ private static string ComputeProjectHash(string input)
316322
}
317323
}
318324
}
319-
}
325+
}

MCPForUnity/Editor/MCPForUnityBridge.cs

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,12 @@ private static bool IsCompiling()
297297
return false;
298298
}
299299

300+
/// <summary>
301+
/// Starts the MCPForUnity bridge: binds a local TCP listener, marks the bridge running, starts the background listener loop, registers editor update and lifecycle handlers, and emits an initial heartbeat.
302+
/// </summary>
303+
/// <remarks>
304+
/// The method prefers a persisted per-project port and will attempt short retries; if the preferred port is occupied it will obtain an alternative port and continue. On success it sets bridge runtime state (including <c>isRunning</c> and <c>currentUnityPort</c>), initializes command handling, and schedules regular heartbeats. Errors encountered while binding the listener are logged.
305+
/// </remarks>
300306
public static void Start()
301307
{
302308
lock (startStopLock)
@@ -362,7 +368,22 @@ public static void Start()
362368
}
363369
catch (SocketException se) when (se.SocketErrorCode == SocketError.AddressAlreadyInUse && attempt >= maxImmediateRetries)
364370
{
371+
// Port is occupied by another instance, get a new available port
372+
int oldPort = currentUnityPort;
365373
currentUnityPort = PortManager.GetPortWithFallback();
374+
375+
// Safety check: ensure we got a different port
376+
if (currentUnityPort == oldPort)
377+
{
378+
McpLog.Error($"Port {oldPort} is occupied and no alternative port available");
379+
throw;
380+
}
381+
382+
if (IsDebugEnabled())
383+
{
384+
McpLog.Info($"Port {oldPort} occupied, switching to port {currentUnityPort}");
385+
}
386+
366387
listener = new TcpListener(IPAddress.Loopback, currentUnityPort);
367388
listener.Server.SetSocketOption(
368389
SocketOptionLevel.Socket,
@@ -417,6 +438,18 @@ public static void Start()
417438
}
418439
}
419440

441+
/// <summary>
442+
/// Stops the MCP-for-Unity bridge, tears down network listeners and client connections, and cleans up runtime state.
443+
/// </summary>
444+
/// <remarks>
445+
/// This method is safe to call multiple times and performs a best-effort, non-blocking shutdown:
446+
/// - Cancels the listener loop and stops the TCP listener.
447+
/// - Closes active client sockets to unblock pending I/O.
448+
/// - Waits briefly for the listener task to exit.
449+
/// - Unsubscribes editor and assembly reload events.
450+
/// - Attempts to delete the per-project status file under the user's profile directory.
451+
/// Exceptions encountered during shutdown are caught and logged; callers should not rely on exceptions being thrown.
452+
/// </remarks>
420453
public static void Stop()
421454
{
422455
Task toWait = null;
@@ -474,6 +507,22 @@ public static void Stop()
474507
try { AssemblyReloadEvents.afterAssemblyReload -= OnAfterAssemblyReload; } catch { }
475508
try { EditorApplication.quitting -= Stop; } catch { }
476509

510+
// Clean up status file when Unity stops
511+
try
512+
{
513+
string statusDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unity-mcp");
514+
string statusFile = Path.Combine(statusDir, $"unity-mcp-status-{ComputeProjectHash(Application.dataPath)}.json");
515+
if (File.Exists(statusFile))
516+
{
517+
File.Delete(statusFile);
518+
if (IsDebugEnabled()) McpLog.Info($"Deleted status file: {statusFile}");
519+
}
520+
}
521+
catch (Exception ex)
522+
{
523+
if (IsDebugEnabled()) McpLog.Warn($"Failed to delete status file: {ex.Message}");
524+
}
525+
477526
if (IsDebugEnabled()) McpLog.Info("MCPForUnityBridge stopped.");
478527
}
479528

@@ -1172,6 +1221,11 @@ private static void OnAfterAssemblyReload()
11721221
ScheduleInitRetry();
11731222
}
11741223

1224+
/// <summary>
1225+
/// Writes a per-project status JSON file for external monitoring containing bridge state and metadata.
1226+
/// </summary>
1227+
/// <param name="reloading">If true, indicates the editor is reloading; affects the default reason value.</param>
1228+
/// <param name="reason">Optional custom reason to include in the status file. If null, defaults to "reloading" when <paramref name="reloading"/> is true, otherwise "ready".</param>
11751229
private static void WriteHeartbeat(bool reloading, string reason = null)
11761230
{
11771231
try
@@ -1184,13 +1238,38 @@ private static void WriteHeartbeat(bool reloading, string reason = null)
11841238
}
11851239
Directory.CreateDirectory(dir);
11861240
string filePath = Path.Combine(dir, $"unity-mcp-status-{ComputeProjectHash(Application.dataPath)}.json");
1241+
1242+
// Extract project name from path
1243+
string projectName = "Unknown";
1244+
try
1245+
{
1246+
string projectPath = Application.dataPath;
1247+
if (!string.IsNullOrEmpty(projectPath))
1248+
{
1249+
// Remove trailing /Assets or \Assets
1250+
projectPath = projectPath.TrimEnd('/', '\\');
1251+
if (projectPath.EndsWith("Assets", StringComparison.OrdinalIgnoreCase))
1252+
{
1253+
projectPath = projectPath.Substring(0, projectPath.Length - 6).TrimEnd('/', '\\');
1254+
}
1255+
projectName = Path.GetFileName(projectPath);
1256+
if (string.IsNullOrEmpty(projectName))
1257+
{
1258+
projectName = "Unknown";
1259+
}
1260+
}
1261+
}
1262+
catch { }
1263+
11871264
var payload = new
11881265
{
11891266
unity_port = currentUnityPort,
11901267
reloading,
11911268
reason = reason ?? (reloading ? "reloading" : "ready"),
11921269
seq = heartbeatSeq,
11931270
project_path = Application.dataPath,
1271+
project_name = projectName,
1272+
unity_version = Application.unityVersion,
11941273
last_heartbeat = DateTime.UtcNow.ToString("O")
11951274
};
11961275
File.WriteAllText(filePath, JsonConvert.SerializeObject(payload), new System.Text.UTF8Encoding(false));
@@ -1237,4 +1316,4 @@ private static string ComputeProjectHash(string input)
12371316
}
12381317
}
12391318
}
1240-
}
1319+
}

MCPForUnity/UnityMcpServer~/src/models.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import Any
2+
from datetime import datetime
23
from pydantic import BaseModel
34

45

@@ -7,3 +8,36 @@ class MCPResponse(BaseModel):
78
message: str | None = None
89
error: str | None = None
910
data: Any | None = None
11+
12+
13+
class UnityInstanceInfo(BaseModel):
14+
"""Information about a Unity Editor instance"""
15+
id: str # "ProjectName@hash" or fallback to hash
16+
name: str # Project name extracted from path
17+
path: str # Full project path (Assets folder)
18+
hash: str # 8-char hash of project path
19+
port: int # TCP port
20+
status: str # "running", "reloading", "offline"
21+
last_heartbeat: datetime | None = None
22+
unity_version: str | None = None
23+
24+
def to_dict(self) -> dict[str, Any]:
25+
"""
26+
Serialize the UnityInstanceInfo to a JSON-serializable dictionary.
27+
28+
last_heartbeat is converted to an ISO 8601 string when present; otherwise it is None.
29+
30+
Returns:
31+
dict[str, Any]: Dictionary with keys "id", "name", "path", "hash", "port", "status",
32+
"last_heartbeat", and "unity_version" containing the corresponding field values.
33+
"""
34+
return {
35+
"id": self.id,
36+
"name": self.name,
37+
"path": self.path,
38+
"hash": self.hash,
39+
"port": self.port,
40+
"status": self.status,
41+
"last_heartbeat": self.last_heartbeat.isoformat() if self.last_heartbeat else None,
42+
"unity_version": self.unity_version
43+
}

0 commit comments

Comments
 (0)