Skip to content

Commit 5939d23

Browse files
authored
Merge branch 'CoplayDev:main' into main
2 parents 532b30d + 14b11ba commit 5939d23

File tree

170 files changed

+7501
-1805
lines changed

Some content is hidden

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

170 files changed

+7501
-1805
lines changed

.github/workflows/python-tests.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Python Tests
2+
3+
on:
4+
push:
5+
branches: ["**"]
6+
paths:
7+
- MCPForUnity/UnityMcpServer~/src/**
8+
- .github/workflows/python-tests.yml
9+
workflow_dispatch: {}
10+
11+
jobs:
12+
test:
13+
name: Run Python Tests
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout repository
17+
uses: actions/checkout@v4
18+
19+
- name: Install uv
20+
uses: astral-sh/setup-uv@v4
21+
with:
22+
version: "latest"
23+
24+
- name: Set up Python
25+
run: uv python install 3.10
26+
27+
- name: Install dependencies
28+
run: |
29+
cd MCPForUnity/UnityMcpServer~/src
30+
uv sync
31+
uv pip install -e ".[dev]"
32+
33+
- name: Run tests
34+
run: |
35+
cd MCPForUnity/UnityMcpServer~/src
36+
uv run pytest tests/ -v --tb=short
37+
38+
- name: Upload test results
39+
uses: actions/upload-artifact@v4
40+
if: always()
41+
with:
42+
name: pytest-results
43+
path: |
44+
MCPForUnity/UnityMcpServer~/src/.pytest_cache/
45+
MCPForUnity/UnityMcpServer~/src/tests/

.github/workflows/unity-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: Unity Tests
33
on:
44
workflow_dispatch: {}
55
push:
6-
branches: [main]
6+
branches: ["**"]
77
paths:
88
- TestProjects/UnityMCPTests/**
99
- MCPForUnity/Editor/**

MCPForUnity/Editor/Helpers/PortManager.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,17 @@ public static int GetPortWithFallback()
6060
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Stored port {storedConfig.unity_port} became available after short wait");
6161
return storedConfig.unity_port;
6262
}
63-
// Prefer sticking to the same port; let the caller handle bind retries/fallbacks
64-
return storedConfig.unity_port;
63+
// Port is still busy after waiting - find a new available port instead
64+
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Stored port {storedConfig.unity_port} is occupied by another instance, finding alternative...");
65+
int newPort = FindAvailablePort();
66+
SavePort(newPort);
67+
return newPort;
6568
}
6669

6770
// If no valid stored port, find a new one and save it
68-
int newPort = FindAvailablePort();
69-
SavePort(newPort);
70-
return newPort;
71+
int foundPort = FindAvailablePort();
72+
SavePort(foundPort);
73+
return foundPort;
7174
}
7275

7376
/// <summary>

MCPForUnity/Editor/MCPForUnityBridge.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,24 @@ public static void Start()
362362
}
363363
catch (SocketException se) when (se.SocketErrorCode == SocketError.AddressAlreadyInUse && attempt >= maxImmediateRetries)
364364
{
365+
// Port is occupied by another instance, get a new available port
366+
int oldPort = currentUnityPort;
365367
currentUnityPort = PortManager.GetPortWithFallback();
368+
369+
// GetPortWithFallback() may return the same port if it became available during wait
370+
// or a different port if switching to an alternative
371+
if (IsDebugEnabled())
372+
{
373+
if (currentUnityPort == oldPort)
374+
{
375+
McpLog.Info($"Port {oldPort} became available, proceeding");
376+
}
377+
else
378+
{
379+
McpLog.Info($"Port {oldPort} occupied, switching to port {currentUnityPort}");
380+
}
381+
}
382+
366383
listener = new TcpListener(IPAddress.Loopback, currentUnityPort);
367384
listener.Server.SetSocketOption(
368385
SocketOptionLevel.Socket,
@@ -474,6 +491,22 @@ public static void Stop()
474491
try { AssemblyReloadEvents.afterAssemblyReload -= OnAfterAssemblyReload; } catch { }
475492
try { EditorApplication.quitting -= Stop; } catch { }
476493

494+
// Clean up status file when Unity stops
495+
try
496+
{
497+
string statusDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unity-mcp");
498+
string statusFile = Path.Combine(statusDir, $"unity-mcp-status-{ComputeProjectHash(Application.dataPath)}.json");
499+
if (File.Exists(statusFile))
500+
{
501+
File.Delete(statusFile);
502+
if (IsDebugEnabled()) McpLog.Info($"Deleted status file: {statusFile}");
503+
}
504+
}
505+
catch (Exception ex)
506+
{
507+
if (IsDebugEnabled()) McpLog.Warn($"Failed to delete status file: {ex.Message}");
508+
}
509+
477510
if (IsDebugEnabled()) McpLog.Info("MCPForUnityBridge stopped.");
478511
}
479512

@@ -1184,13 +1217,38 @@ private static void WriteHeartbeat(bool reloading, string reason = null)
11841217
}
11851218
Directory.CreateDirectory(dir);
11861219
string filePath = Path.Combine(dir, $"unity-mcp-status-{ComputeProjectHash(Application.dataPath)}.json");
1220+
1221+
// Extract project name from path
1222+
string projectName = "Unknown";
1223+
try
1224+
{
1225+
string projectPath = Application.dataPath;
1226+
if (!string.IsNullOrEmpty(projectPath))
1227+
{
1228+
// Remove trailing /Assets or \Assets
1229+
projectPath = projectPath.TrimEnd('/', '\\');
1230+
if (projectPath.EndsWith("Assets", StringComparison.OrdinalIgnoreCase))
1231+
{
1232+
projectPath = projectPath.Substring(0, projectPath.Length - 6).TrimEnd('/', '\\');
1233+
}
1234+
projectName = Path.GetFileName(projectPath);
1235+
if (string.IsNullOrEmpty(projectName))
1236+
{
1237+
projectName = "Unknown";
1238+
}
1239+
}
1240+
}
1241+
catch { }
1242+
11871243
var payload = new
11881244
{
11891245
unity_port = currentUnityPort,
11901246
reloading,
11911247
reason = reason ?? (reloading ? "reloading" : "ready"),
11921248
seq = heartbeatSeq,
11931249
project_path = Application.dataPath,
1250+
project_name = projectName,
1251+
unity_version = Application.unityVersion,
11941252
last_heartbeat = DateTime.UtcNow.ToString("O")
11951253
};
11961254
File.WriteAllText(filePath, JsonConvert.SerializeObject(payload), new System.Text.UTF8Encoding(false));

TestProjects/UnityMCPTests/Assets/Materials.meta renamed to MCPForUnity/Editor/Resources/Editor.meta

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using System;
2+
using MCPForUnity.Editor.Helpers;
3+
using Newtonsoft.Json.Linq;
4+
using UnityEditor;
5+
6+
namespace MCPForUnity.Editor.Resources.Editor
7+
{
8+
/// <summary>
9+
/// Provides information about the currently active editor tool.
10+
/// </summary>
11+
[McpForUnityResource("get_active_tool")]
12+
public static class ActiveTool
13+
{
14+
public static object HandleCommand(JObject @params)
15+
{
16+
try
17+
{
18+
Tool currentTool = UnityEditor.Tools.current;
19+
string toolName = currentTool.ToString();
20+
bool customToolActive = UnityEditor.Tools.current == Tool.Custom;
21+
string activeToolName = customToolActive ? EditorTools.GetActiveToolName() : toolName;
22+
23+
var toolInfo = new
24+
{
25+
activeTool = activeToolName,
26+
isCustom = customToolActive,
27+
pivotMode = UnityEditor.Tools.pivotMode.ToString(),
28+
pivotRotation = UnityEditor.Tools.pivotRotation.ToString(),
29+
handleRotation = new
30+
{
31+
x = UnityEditor.Tools.handleRotation.eulerAngles.x,
32+
y = UnityEditor.Tools.handleRotation.eulerAngles.y,
33+
z = UnityEditor.Tools.handleRotation.eulerAngles.z
34+
},
35+
handlePosition = new
36+
{
37+
x = UnityEditor.Tools.handlePosition.x,
38+
y = UnityEditor.Tools.handlePosition.y,
39+
z = UnityEditor.Tools.handlePosition.z
40+
}
41+
};
42+
43+
return Response.Success("Retrieved active tool information.", toolInfo);
44+
}
45+
catch (Exception e)
46+
{
47+
return Response.Error($"Error getting active tool: {e.Message}");
48+
}
49+
}
50+
}
51+
52+
// Helper class for custom tool names
53+
internal static class EditorTools
54+
{
55+
public static string GetActiveToolName()
56+
{
57+
if (UnityEditor.Tools.current == Tool.Custom)
58+
{
59+
return "Unknown Custom Tool";
60+
}
61+
return UnityEditor.Tools.current.ToString();
62+
}
63+
}
64+
}

MCPForUnity/Editor/Resources/Editor/ActiveTool.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System;
2+
using MCPForUnity.Editor.Helpers;
3+
using Newtonsoft.Json.Linq;
4+
using UnityEditor;
5+
using UnityEditor.SceneManagement;
6+
7+
namespace MCPForUnity.Editor.Resources.Editor
8+
{
9+
/// <summary>
10+
/// Provides dynamic editor state information that changes frequently.
11+
/// </summary>
12+
[McpForUnityResource("get_editor_state")]
13+
public static class EditorState
14+
{
15+
public static object HandleCommand(JObject @params)
16+
{
17+
try
18+
{
19+
var activeScene = EditorSceneManager.GetActiveScene();
20+
var state = new
21+
{
22+
isPlaying = EditorApplication.isPlaying,
23+
isPaused = EditorApplication.isPaused,
24+
isCompiling = EditorApplication.isCompiling,
25+
isUpdating = EditorApplication.isUpdating,
26+
timeSinceStartup = EditorApplication.timeSinceStartup,
27+
activeSceneName = activeScene.name ?? "",
28+
selectionCount = UnityEditor.Selection.count,
29+
activeObjectName = UnityEditor.Selection.activeObject?.name
30+
};
31+
32+
return Response.Success("Retrieved editor state.", state);
33+
}
34+
catch (Exception e)
35+
{
36+
return Response.Error($"Error getting editor state: {e.Message}");
37+
}
38+
}
39+
}
40+
}

MCPForUnity/Editor/Resources/Editor/EditorState.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System;
2+
using MCPForUnity.Editor.Helpers;
3+
using Newtonsoft.Json.Linq;
4+
using UnityEditor.SceneManagement;
5+
6+
namespace MCPForUnity.Editor.Resources.Editor
7+
{
8+
/// <summary>
9+
/// Provides information about the current prefab editing context.
10+
/// </summary>
11+
[McpForUnityResource("get_prefab_stage")]
12+
public static class PrefabStage
13+
{
14+
public static object HandleCommand(JObject @params)
15+
{
16+
try
17+
{
18+
var stage = PrefabStageUtility.GetCurrentPrefabStage();
19+
20+
if (stage == null)
21+
{
22+
return Response.Success("No prefab stage is currently open.", new { isOpen = false });
23+
}
24+
25+
var stageInfo = new
26+
{
27+
isOpen = true,
28+
assetPath = stage.assetPath,
29+
prefabRootName = stage.prefabContentsRoot?.name,
30+
mode = stage.mode.ToString(),
31+
isDirty = stage.scene.isDirty
32+
};
33+
34+
return Response.Success("Prefab stage info retrieved.", stageInfo);
35+
}
36+
catch (Exception e)
37+
{
38+
return Response.Error($"Error getting prefab stage info: {e.Message}");
39+
}
40+
}
41+
}
42+
}

0 commit comments

Comments
 (0)