Skip to content

Commit 7aacf27

Browse files
authored
Merge pull request #1072 from gibertoni/fix/cross-platform-log-path
Improve python bridge log destination
2 parents b51ab59 + e619365 commit 7aacf27

4 files changed

Lines changed: 146 additions & 3 deletions

File tree

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)