Skip to content

Commit 996ccd1

Browse files
authored
Merge branch 'CoplayDev:beta' into chore/issue-fixes-2026-04-26
2 parents d7972db + 3adb569 commit 996ccd1

10 files changed

Lines changed: 299 additions & 29 deletions

File tree

MCPForUnity/Editor/MenuItems/MCPForUnityMenu.cs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,7 @@ public static class MCPForUnityMenu
1010
[MenuItem("Window/MCP For Unity/Toggle MCP Window %#m", priority = 1)]
1111
public static void ToggleMCPWindow()
1212
{
13-
if (MCPForUnityEditorWindow.HasAnyOpenWindow())
14-
{
15-
MCPForUnityEditorWindow.CloseAllOpenWindows();
16-
}
17-
else
18-
{
19-
MCPForUnityEditorWindow.ShowWindow();
20-
}
13+
MCPForUnityEditorWindow.ShowWindow();
2114
}
2215

2316
[MenuItem("Window/MCP For Unity/Local Setup Window", priority = 2)]

MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,15 +1348,12 @@ private static object SavePrefabStage()
13481348
return new ErrorResponse("Not currently in prefab editing mode. Open a prefab stage first with open_prefab_stage.");
13491349
}
13501350

1351-
string prefabPath = prefabStage.assetPath;
1352-
EditorSceneManager.MarkSceneDirty(prefabStage.scene);
1353-
bool saved = EditorSceneManager.SaveScene(prefabStage.scene);
1354-
if (!saved)
1351+
if (!TrySavePrefabStage(prefabStage, out string prefabPath, out string errorMessage))
13551352
{
1356-
return new ErrorResponse($"Failed to save prefab stage for '{prefabPath}'. The file may be read-only or the disk may be full.");
1353+
return new ErrorResponse(errorMessage);
13571354
}
13581355

1359-
return new SuccessResponse($"Saved prefab stage changes for '{prefabPath}'.", new { prefabPath, saved });
1356+
return new SuccessResponse($"Saved prefab stage changes for '{prefabPath}'.", new { prefabPath, saved = true });
13601357
}
13611358
catch (Exception e)
13621359
{
@@ -1376,10 +1373,9 @@ private static object ClosePrefabStage(bool saveBeforeClose = false)
13761373

13771374
if (saveBeforeClose)
13781375
{
1379-
var saveResult = SavePrefabStage();
1380-
if (saveResult is ErrorResponse)
1376+
if (!TrySavePrefabStage(prefabStage, out _, out string errorMessage))
13811377
{
1382-
return saveResult;
1378+
return new ErrorResponse(errorMessage);
13831379
}
13841380
}
13851381

@@ -1393,6 +1389,31 @@ private static object ClosePrefabStage(bool saveBeforeClose = false)
13931389
}
13941390
}
13951391

1392+
private static bool TrySavePrefabStage(PrefabStage prefabStage, out string prefabPath, out string errorMessage)
1393+
{
1394+
prefabPath = prefabStage.assetPath;
1395+
errorMessage = null;
1396+
1397+
if (prefabStage.prefabContentsRoot == null)
1398+
{
1399+
errorMessage = $"Failed to save prefab stage for '{prefabPath}'. The prefab contents root is missing.";
1400+
return false;
1401+
}
1402+
1403+
bool saved;
1404+
PrefabUtility.SaveAsPrefabAsset(prefabStage.prefabContentsRoot, prefabPath, out saved);
1405+
if (!saved)
1406+
{
1407+
errorMessage = $"Failed to save prefab stage for '{prefabPath}'. The file may be read-only or the disk may be full.";
1408+
return false;
1409+
}
1410+
1411+
prefabStage.ClearDirtiness();
1412+
AssetDatabase.SaveAssets();
1413+
AssetDatabase.Refresh();
1414+
return true;
1415+
}
1416+
13961417
#endregion
13971418
}
13981419
}

MCPForUnity/Editor/Tools/Profiler/Operations/SessionOps.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ internal static object Start(JObject @params)
2121

2222
UProfiler.enabled = true;
2323

24-
bool recording = false;
2524
if (!string.IsNullOrEmpty(logFile))
2625
{
2726
string dir = Path.GetDirectoryName(logFile);
@@ -30,7 +29,6 @@ internal static object Start(JObject @params)
3029

3130
UProfiler.logFile = logFile;
3231
UProfiler.enableBinaryLog = true;
33-
recording = true;
3432
}
3533

3634
if (enableCallstacks)

MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,45 @@ internal static void CloseAllWindows()
7171
}
7272
}
7373

74-
public static void ShowWindow()
75-
{
76-
var window = GetWindow<MCPForUnityEditorWindow>("MCP For Unity");
77-
window.minSize = new Vector2(500, 340);
78-
}
74+
public static void ShowWindow()
75+
{
76+
var existingWindows = UnityEngine.Resources.FindObjectsOfTypeAll<MCPForUnityEditorWindow>();
77+
MCPForUnityEditorWindow window = null;
78+
79+
if (existingWindows.Length > 0)
80+
{
81+
window = existingWindows[0];
82+
83+
// If multiple instances exist, keep one and close the extras to avoid stale hidden tabs.
84+
for (int i = 1; i < existingWindows.Length; i++)
85+
{
86+
try
87+
{
88+
existingWindows[i].Close();
89+
}
90+
catch (Exception ex)
91+
{
92+
McpLog.Warn($"Error closing duplicate MCP window: {ex.Message}");
93+
}
94+
}
95+
}
96+
else
97+
{
98+
window = GetWindow<MCPForUnityEditorWindow>("MCP For Unity");
99+
}
100+
101+
window.titleContent = new GUIContent("MCP For Unity");
102+
window.minSize = new Vector2(500, 340);
103+
104+
if (window.position.width < 100 || window.position.height < 100)
105+
{
106+
window.position = new Rect(120, 120, 900, 700);
107+
}
108+
109+
window.Show();
110+
window.ShowTab();
111+
window.Focus();
112+
}
79113

80114
// Helper to check and manage open windows from other classes
81115
public static bool HasAnyOpenWindow()
@@ -125,7 +159,8 @@ public void CreateGUI()
125159
return;
126160
}
127161

128-
visualTree.CloneTree(rootVisualElement);
162+
rootVisualElement.Clear();
163+
visualTree.CloneTree(rootVisualElement);
129164

130165
// Load main window USS
131166
var mainStyleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(

MCPForUnity/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "com.coplaydev.unity-mcp",
3-
"version": "9.6.7-beta.8",
3+
"version": "9.6.7-beta.11",
44
"displayName": "MCP for Unity",
55
"description": "A bridge that connects AI assistants to Unity via the MCP (Model Context Protocol). Allows AI clients like Claude Code, Cursor, and VSCode to directly control your Unity Editor for enhanced development workflows.\n\nFeatures automated setup wizard, cross-platform support, and seamless integration with popular AI development tools.\n\nJoin Our Discord: https://discord.gg/y4p8KfzrN4",
66
"unity": "2021.3",

Server/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ These options apply to the `mcp-for-unity` command (whether run via `uvx`, Docke
147147
- `UNITY_MCP_HTTP_REMOTE_HOSTED` - Enable remote-hosted mode (`true`, `1`, or `yes`)
148148
- `UNITY_MCP_DEFAULT_INSTANCE` - Default Unity instance to target (project name, hash, or `Name@hash`)
149149
- `UNITY_MCP_SKIP_STARTUP_CONNECT=1` - Skip initial Unity connection attempt on startup
150+
- `UNITY_MCP_LOG_DIR` - Override the rotating server log directory. Default: `%LOCALAPPDATA%\UnityMCP\Logs` (Windows), `~/Library/Application Support/UnityMCP/Logs` (macOS), `$XDG_STATE_HOME/UnityMCP/Logs` (Linux/BSD, defaults to `~/.local/state/UnityMCP/Logs`).
150151

151152
API key authentication (remote-hosted mode):
152153

Server/src/main.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,11 @@ def doRollover(self):
8787
)
8888
logger = logging.getLogger("mcp-for-unity-server")
8989

90-
# Also write logs to a rotating file so logs are available when launched via stdio
90+
# Also write logs to a rotating file so logs are available when launched via stdio.
91+
# Location follows OS conventions; override with UNITY_MCP_LOG_DIR.
9192
try:
92-
_log_dir = os.path.join(os.path.expanduser(
93-
"~/Library/Application Support/UnityMCP"), "Logs")
93+
from utils.log_paths import resolve_log_dir
94+
_log_dir = resolve_log_dir()
9495
os.makedirs(_log_dir, exist_ok=True)
9596
_file_path = os.path.join(_log_dir, "unity_mcp_server.log")
9697
_fh = WindowsSafeRotatingFileHandler(

Server/src/utils/log_paths.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""
2+
OS-aware log directory resolution
3+
4+
Picks a platform-appropriate location for the rotating server log:
5+
- Windows: %LOCALAPPDATA%\\UnityMCP\\Logs
6+
- macOS: ~/Library/Application Support/UnityMCP/Logs
7+
- Linux/BSD: $XDG_STATE_HOME/UnityMCP/Logs (default ~/.local/state/UnityMCP/Logs)
8+
9+
UNITY_MCP_LOG_DIR overrides all of the above.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import os
15+
import sys
16+
from collections.abc import Mapping
17+
18+
19+
def resolve_log_dir(
20+
*,
21+
platform: str | None = None,
22+
env: Mapping[str, str] | None = None,
23+
) -> str:
24+
"""Return the absolute log directory path for the current OS.
25+
"""
26+
if platform is None:
27+
platform = sys.platform
28+
if env is None:
29+
env = os.environ
30+
31+
override = env.get("UNITY_MCP_LOG_DIR")
32+
if override:
33+
return os.path.expanduser(override)
34+
35+
if platform == "darwin":
36+
return os.path.expanduser("~/Library/Application Support/UnityMCP/Logs")
37+
38+
if platform == "win32":
39+
base = env.get("LOCALAPPDATA") or os.path.expanduser("~/AppData/Local")
40+
return os.path.join(base, "UnityMCP", "Logs")
41+
42+
# Linux/BSD and anything else: XDG_STATE_HOME per freedesktop.org basedir spec.
43+
base = env.get("XDG_STATE_HOME") or os.path.expanduser("~/.local/state")
44+
return os.path.join(base, "UnityMCP", "Logs")

Server/tests/test_log_paths.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Unit tests for utils.log_paths.resolve_log_dir."""
2+
3+
from __future__ import annotations
4+
5+
import os
6+
7+
import pytest
8+
9+
from utils.log_paths import resolve_log_dir
10+
11+
12+
class TestEnvOverride:
13+
def test_override_wins_on_macos(self):
14+
path = resolve_log_dir(
15+
platform="darwin",
16+
env={"UNITY_MCP_LOG_DIR": "/tmp/custom-logs"},
17+
)
18+
assert path == "/tmp/custom-logs"
19+
20+
def test_override_wins_on_windows(self):
21+
path = resolve_log_dir(
22+
platform="win32",
23+
env={"UNITY_MCP_LOG_DIR": "/tmp/custom-logs", "LOCALAPPDATA": r"C:\ignored"},
24+
)
25+
assert path == "/tmp/custom-logs"
26+
27+
def test_override_wins_on_linux(self):
28+
path = resolve_log_dir(
29+
platform="linux",
30+
env={"UNITY_MCP_LOG_DIR": "/tmp/custom-logs", "XDG_STATE_HOME": "/ignored"},
31+
)
32+
assert path == "/tmp/custom-logs"
33+
34+
def test_override_expands_user(self):
35+
path = resolve_log_dir(
36+
platform="linux",
37+
env={"UNITY_MCP_LOG_DIR": "~/my-logs"},
38+
)
39+
assert path == os.path.expanduser("~/my-logs")
40+
assert "~" not in path
41+
42+
43+
def _norm(p: str) -> str:
44+
"""Normalize for cross-host comparison: os.path.expanduser can leave
45+
mixed separators when a foreign platform is injected during testing."""
46+
return os.path.normpath(p)
47+
48+
49+
class TestPlatformDefaults:
50+
def test_macos_uses_application_support(self):
51+
path = resolve_log_dir(platform="darwin", env={})
52+
assert _norm(path).endswith(_norm(
53+
"Library/Application Support/UnityMCP/Logs"))
54+
assert "~" not in path
55+
56+
def test_windows_uses_localappdata_env(self):
57+
path = resolve_log_dir(
58+
platform="win32",
59+
env={"LOCALAPPDATA": r"C:\Users\alice\AppData\Local"},
60+
)
61+
assert _norm(path) == _norm(os.path.join(
62+
r"C:\Users\alice\AppData\Local", "UnityMCP", "Logs"))
63+
64+
def test_windows_falls_back_when_localappdata_missing(self):
65+
path = resolve_log_dir(platform="win32", env={})
66+
# Fallback expands ~/AppData/Local; assert shape, not the exact user home.
67+
assert _norm(path).endswith(_norm("AppData/Local/UnityMCP/Logs"))
68+
assert "~" not in path
69+
70+
def test_linux_uses_xdg_state_home(self):
71+
path = resolve_log_dir(
72+
platform="linux",
73+
env={"XDG_STATE_HOME": "/home/alice/.local/state"},
74+
)
75+
assert _norm(path) == _norm(os.path.join(
76+
"/home/alice/.local/state", "UnityMCP", "Logs"))
77+
78+
def test_linux_falls_back_to_default_xdg_path(self):
79+
path = resolve_log_dir(platform="linux", env={})
80+
assert _norm(path).endswith(_norm(".local/state/UnityMCP/Logs"))
81+
assert "~" not in path
82+
83+
@pytest.mark.parametrize("unix_like", ["freebsd", "openbsd", "sunos5"])
84+
def test_unknown_unix_variants_follow_linux_branch(self, unix_like):
85+
path = resolve_log_dir(
86+
platform=unix_like,
87+
env={"XDG_STATE_HOME": "/var/state"},
88+
)
89+
assert _norm(path) == _norm(os.path.join(
90+
"/var/state", "UnityMCP", "Logs"))
91+
92+
93+
class TestDefaults:
94+
def test_no_args_reads_live_environment(self, monkeypatch):
95+
"""With no args, should read sys.platform and os.environ directly."""
96+
monkeypatch.setenv("UNITY_MCP_LOG_DIR", "/tmp/live-env-test")
97+
assert resolve_log_dir() == "/tmp/live-env-test"

0 commit comments

Comments
 (0)