mcpforunityserver 8.5.0__py3-none-any.whl → 9.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cli/__init__.py +3 -0
- cli/commands/__init__.py +3 -0
- cli/commands/animation.py +87 -0
- cli/commands/asset.py +310 -0
- cli/commands/audio.py +133 -0
- cli/commands/batch.py +184 -0
- cli/commands/code.py +189 -0
- cli/commands/component.py +212 -0
- cli/commands/editor.py +487 -0
- cli/commands/gameobject.py +510 -0
- cli/commands/instance.py +101 -0
- cli/commands/lighting.py +128 -0
- cli/commands/material.py +268 -0
- cli/commands/prefab.py +144 -0
- cli/commands/scene.py +255 -0
- cli/commands/script.py +240 -0
- cli/commands/shader.py +238 -0
- cli/commands/ui.py +263 -0
- cli/commands/vfx.py +439 -0
- cli/main.py +248 -0
- cli/utils/__init__.py +31 -0
- cli/utils/config.py +58 -0
- cli/utils/connection.py +191 -0
- cli/utils/output.py +195 -0
- main.py +207 -62
- {mcpforunityserver-8.5.0.dist-info → mcpforunityserver-9.1.0.dist-info}/METADATA +4 -2
- mcpforunityserver-9.1.0.dist-info/RECORD +96 -0
- {mcpforunityserver-8.5.0.dist-info → mcpforunityserver-9.1.0.dist-info}/WHEEL +1 -1
- {mcpforunityserver-8.5.0.dist-info → mcpforunityserver-9.1.0.dist-info}/entry_points.txt +1 -0
- {mcpforunityserver-8.5.0.dist-info → mcpforunityserver-9.1.0.dist-info}/top_level.txt +1 -2
- services/custom_tool_service.py +179 -19
- services/resources/__init__.py +6 -1
- services/resources/active_tool.py +1 -1
- services/resources/custom_tools.py +2 -2
- services/resources/editor_state.py +283 -21
- services/resources/gameobject.py +243 -0
- services/resources/layers.py +1 -1
- services/resources/prefab_stage.py +1 -1
- services/resources/project_info.py +1 -1
- services/resources/selection.py +1 -1
- services/resources/tags.py +1 -1
- services/resources/unity_instances.py +1 -1
- services/resources/windows.py +1 -1
- services/state/external_changes_scanner.py +245 -0
- services/tools/__init__.py +6 -1
- services/tools/batch_execute.py +24 -9
- services/tools/debug_request_context.py +8 -2
- services/tools/execute_custom_tool.py +6 -1
- services/tools/execute_menu_item.py +6 -3
- services/tools/find_gameobjects.py +89 -0
- services/tools/find_in_file.py +26 -19
- services/tools/manage_asset.py +19 -43
- services/tools/manage_components.py +131 -0
- services/tools/manage_editor.py +9 -8
- services/tools/manage_gameobject.py +120 -79
- services/tools/manage_material.py +80 -31
- services/tools/manage_prefabs.py +7 -1
- services/tools/manage_scene.py +34 -13
- services/tools/manage_script.py +62 -19
- services/tools/manage_scriptable_object.py +22 -10
- services/tools/manage_shader.py +8 -1
- services/tools/manage_vfx.py +738 -0
- services/tools/preflight.py +110 -0
- services/tools/read_console.py +81 -18
- services/tools/refresh_unity.py +153 -0
- services/tools/run_tests.py +202 -41
- services/tools/script_apply_edits.py +15 -7
- services/tools/set_active_instance.py +12 -7
- services/tools/utils.py +60 -6
- transport/legacy/port_discovery.py +2 -2
- transport/legacy/unity_connection.py +129 -26
- transport/plugin_hub.py +191 -19
- transport/unity_instance_middleware.py +93 -2
- transport/unity_transport.py +17 -6
- utils/focus_nudge.py +321 -0
- __init__.py +0 -0
- mcpforunityserver-8.5.0.dist-info/RECORD +0 -66
- routes/__init__.py +0 -0
- {mcpforunityserver-8.5.0.dist-info → mcpforunityserver-9.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -27,7 +27,7 @@ def get_unity_instance_middleware() -> 'UnityInstanceMiddleware':
|
|
|
27
27
|
if _unity_instance_middleware is None:
|
|
28
28
|
# Auto-initialize if not set (lazy singleton) to handle import order or test cases
|
|
29
29
|
_unity_instance_middleware = UnityInstanceMiddleware()
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
return _unity_instance_middleware
|
|
32
32
|
|
|
33
33
|
|
|
@@ -83,17 +83,108 @@ class UnityInstanceMiddleware(Middleware):
|
|
|
83
83
|
with self._lock:
|
|
84
84
|
self._active_by_key.pop(key, None)
|
|
85
85
|
|
|
86
|
+
async def _maybe_autoselect_instance(self, ctx) -> str | None:
|
|
87
|
+
"""
|
|
88
|
+
Auto-select the sole Unity instance when no active instance is set.
|
|
89
|
+
|
|
90
|
+
Note: This method both *discovers* and *persists* the selection via
|
|
91
|
+
`set_active_instance` as a side-effect, since callers expect the selection
|
|
92
|
+
to stick for subsequent tool/resource calls in the same session.
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
# Import here to avoid circular dependencies / optional transport modules.
|
|
96
|
+
from transport.unity_transport import _current_transport
|
|
97
|
+
|
|
98
|
+
transport = _current_transport()
|
|
99
|
+
if PluginHub.is_configured():
|
|
100
|
+
try:
|
|
101
|
+
sessions_data = await PluginHub.get_sessions()
|
|
102
|
+
sessions = sessions_data.sessions or {}
|
|
103
|
+
ids: list[str] = []
|
|
104
|
+
for session_info in sessions.values():
|
|
105
|
+
project = getattr(
|
|
106
|
+
session_info, "project", None) or "Unknown"
|
|
107
|
+
hash_value = getattr(session_info, "hash", None)
|
|
108
|
+
if hash_value:
|
|
109
|
+
ids.append(f"{project}@{hash_value}")
|
|
110
|
+
if len(ids) == 1:
|
|
111
|
+
chosen = ids[0]
|
|
112
|
+
self.set_active_instance(ctx, chosen)
|
|
113
|
+
logger.info(
|
|
114
|
+
"Auto-selected sole Unity instance via PluginHub: %s",
|
|
115
|
+
chosen,
|
|
116
|
+
)
|
|
117
|
+
return chosen
|
|
118
|
+
except (ConnectionError, ValueError, KeyError, TimeoutError, AttributeError) as exc:
|
|
119
|
+
logger.debug(
|
|
120
|
+
"PluginHub auto-select probe failed (%s); falling back to stdio",
|
|
121
|
+
type(exc).__name__,
|
|
122
|
+
exc_info=True,
|
|
123
|
+
)
|
|
124
|
+
except Exception as exc:
|
|
125
|
+
if isinstance(exc, (SystemExit, KeyboardInterrupt)):
|
|
126
|
+
raise
|
|
127
|
+
logger.debug(
|
|
128
|
+
"PluginHub auto-select probe failed with unexpected error (%s); falling back to stdio",
|
|
129
|
+
type(exc).__name__,
|
|
130
|
+
exc_info=True,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if transport != "http":
|
|
134
|
+
try:
|
|
135
|
+
# Import here to avoid circular imports in legacy transport paths.
|
|
136
|
+
from transport.legacy.unity_connection import get_unity_connection_pool
|
|
137
|
+
|
|
138
|
+
pool = get_unity_connection_pool()
|
|
139
|
+
instances = pool.discover_all_instances(force_refresh=True)
|
|
140
|
+
ids = [getattr(inst, "id", None) for inst in instances]
|
|
141
|
+
ids = [inst_id for inst_id in ids if inst_id]
|
|
142
|
+
if len(ids) == 1:
|
|
143
|
+
chosen = ids[0]
|
|
144
|
+
self.set_active_instance(ctx, chosen)
|
|
145
|
+
logger.info(
|
|
146
|
+
"Auto-selected sole Unity instance via stdio discovery: %s",
|
|
147
|
+
chosen,
|
|
148
|
+
)
|
|
149
|
+
return chosen
|
|
150
|
+
except (ConnectionError, ValueError, KeyError, TimeoutError, AttributeError) as exc:
|
|
151
|
+
logger.debug(
|
|
152
|
+
"Stdio auto-select probe failed (%s)",
|
|
153
|
+
type(exc).__name__,
|
|
154
|
+
exc_info=True,
|
|
155
|
+
)
|
|
156
|
+
except Exception as exc:
|
|
157
|
+
if isinstance(exc, (SystemExit, KeyboardInterrupt)):
|
|
158
|
+
raise
|
|
159
|
+
logger.debug(
|
|
160
|
+
"Stdio auto-select probe failed with unexpected error (%s)",
|
|
161
|
+
type(exc).__name__,
|
|
162
|
+
exc_info=True,
|
|
163
|
+
)
|
|
164
|
+
except Exception as exc:
|
|
165
|
+
if isinstance(exc, (SystemExit, KeyboardInterrupt)):
|
|
166
|
+
raise
|
|
167
|
+
logger.debug(
|
|
168
|
+
"Auto-select path encountered an unexpected error (%s)",
|
|
169
|
+
type(exc).__name__,
|
|
170
|
+
exc_info=True,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return None
|
|
174
|
+
|
|
86
175
|
async def _inject_unity_instance(self, context: MiddlewareContext) -> None:
|
|
87
176
|
"""Inject active Unity instance into context if available."""
|
|
88
177
|
ctx = context.fastmcp_context
|
|
89
178
|
|
|
90
179
|
active_instance = self.get_active_instance(ctx)
|
|
180
|
+
if not active_instance:
|
|
181
|
+
active_instance = await self._maybe_autoselect_instance(ctx)
|
|
91
182
|
if active_instance:
|
|
92
183
|
# If using HTTP transport (PluginHub configured), validate session
|
|
93
184
|
# But for stdio transport (no PluginHub needed or maybe partially configured),
|
|
94
185
|
# we should be careful not to clear instance just because PluginHub can't resolve it.
|
|
95
186
|
# The 'active_instance' (Name@hash) might be valid for stdio even if PluginHub fails.
|
|
96
|
-
|
|
187
|
+
|
|
97
188
|
session_id: str | None = None
|
|
98
189
|
# Only validate via PluginHub if we are actually using HTTP transport
|
|
99
190
|
# OR if we want to support hybrid mode. For now, let's be permissive.
|
transport/unity_transport.py
CHANGED
|
@@ -9,6 +9,7 @@ from typing import Awaitable, Callable, TypeVar
|
|
|
9
9
|
from fastmcp import Context
|
|
10
10
|
|
|
11
11
|
from transport.plugin_hub import PluginHub
|
|
12
|
+
from models.models import MCPResponse
|
|
12
13
|
from models.unity_response import normalize_unity_response
|
|
13
14
|
from services.tools import get_unity_instance_from_context
|
|
14
15
|
|
|
@@ -91,12 +92,22 @@ async def send_with_unity_instance(
|
|
|
91
92
|
if not isinstance(params, dict):
|
|
92
93
|
raise TypeError(
|
|
93
94
|
"Command parameters must be a dict for HTTP transport")
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
try:
|
|
96
|
+
raw = await PluginHub.send_command_for_instance(
|
|
97
|
+
unity_instance,
|
|
98
|
+
command_type,
|
|
99
|
+
params,
|
|
100
|
+
)
|
|
101
|
+
return normalize_unity_response(raw)
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
# NOTE: asyncio.TimeoutError has an empty str() by default, which is confusing for clients.
|
|
104
|
+
err = str(exc) or f"{type(exc).__name__}"
|
|
105
|
+
# Fail fast with a retry hint instead of hanging for COMMAND_TIMEOUT.
|
|
106
|
+
# The client can decide whether retrying is appropriate for the command.
|
|
107
|
+
return normalize_unity_response(
|
|
108
|
+
MCPResponse(success=False, error=err,
|
|
109
|
+
hint="retry").model_dump()
|
|
110
|
+
)
|
|
100
111
|
|
|
101
112
|
if unity_instance:
|
|
102
113
|
kwargs.setdefault("instance_id", unity_instance)
|
utils/focus_nudge.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Focus nudge utility for handling OS-level throttling of background Unity.
|
|
3
|
+
|
|
4
|
+
When Unity is unfocused, the OS (especially macOS App Nap) can heavily throttle
|
|
5
|
+
the process, causing PlayMode tests to stall. This utility temporarily brings
|
|
6
|
+
Unity to focus, allows it to process, then returns focus to the original app.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import logging
|
|
13
|
+
import platform
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Minimum seconds between nudges to avoid focus thrashing
|
|
21
|
+
_MIN_NUDGE_INTERVAL_S = 5.0
|
|
22
|
+
_last_nudge_time: float = 0.0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _is_available() -> bool:
|
|
26
|
+
"""Check if focus nudging is available on this platform."""
|
|
27
|
+
system = platform.system()
|
|
28
|
+
if system == "Darwin":
|
|
29
|
+
return shutil.which("osascript") is not None
|
|
30
|
+
elif system == "Windows":
|
|
31
|
+
# PowerShell is typically available on Windows
|
|
32
|
+
return shutil.which("powershell") is not None
|
|
33
|
+
elif system == "Linux":
|
|
34
|
+
return shutil.which("xdotool") is not None
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_frontmost_app_macos() -> str | None:
|
|
39
|
+
"""Get the name of the frontmost application on macOS."""
|
|
40
|
+
try:
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
[
|
|
43
|
+
"osascript", "-e",
|
|
44
|
+
'tell application "System Events" to get name of first process whose frontmost is true'
|
|
45
|
+
],
|
|
46
|
+
capture_output=True,
|
|
47
|
+
text=True,
|
|
48
|
+
timeout=5,
|
|
49
|
+
)
|
|
50
|
+
if result.returncode == 0:
|
|
51
|
+
return result.stdout.strip()
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.debug(f"Failed to get frontmost app: {e}")
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _focus_app_macos(app_name: str) -> bool:
|
|
58
|
+
"""Focus an application by name on macOS."""
|
|
59
|
+
try:
|
|
60
|
+
result = subprocess.run(
|
|
61
|
+
["osascript", "-e", f'tell application "{app_name}" to activate'],
|
|
62
|
+
capture_output=True,
|
|
63
|
+
text=True,
|
|
64
|
+
timeout=5,
|
|
65
|
+
)
|
|
66
|
+
return result.returncode == 0
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.debug(f"Failed to focus app {app_name}: {e}")
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _get_frontmost_app_windows() -> str | None:
|
|
73
|
+
"""Get the title of the frontmost window on Windows."""
|
|
74
|
+
try:
|
|
75
|
+
# PowerShell command to get active window title
|
|
76
|
+
script = '''
|
|
77
|
+
Add-Type @"
|
|
78
|
+
using System;
|
|
79
|
+
using System.Runtime.InteropServices;
|
|
80
|
+
public class Win32 {
|
|
81
|
+
[DllImport("user32.dll")]
|
|
82
|
+
public static extern IntPtr GetForegroundWindow();
|
|
83
|
+
[DllImport("user32.dll")]
|
|
84
|
+
public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder text, int count);
|
|
85
|
+
}
|
|
86
|
+
"@
|
|
87
|
+
$hwnd = [Win32]::GetForegroundWindow()
|
|
88
|
+
$sb = New-Object System.Text.StringBuilder 256
|
|
89
|
+
[Win32]::GetWindowText($hwnd, $sb, 256)
|
|
90
|
+
$sb.ToString()
|
|
91
|
+
'''
|
|
92
|
+
result = subprocess.run(
|
|
93
|
+
["powershell", "-Command", script],
|
|
94
|
+
capture_output=True,
|
|
95
|
+
text=True,
|
|
96
|
+
timeout=5,
|
|
97
|
+
)
|
|
98
|
+
if result.returncode == 0:
|
|
99
|
+
return result.stdout.strip()
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.debug(f"Failed to get frontmost window: {e}")
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _focus_app_windows(window_title: str) -> bool:
|
|
106
|
+
"""Focus a window by title on Windows. For Unity, uses Unity Editor pattern."""
|
|
107
|
+
try:
|
|
108
|
+
# For Unity, we use a pattern match since the title varies
|
|
109
|
+
if window_title == "Unity":
|
|
110
|
+
script = '''
|
|
111
|
+
Add-Type @"
|
|
112
|
+
using System;
|
|
113
|
+
using System.Runtime.InteropServices;
|
|
114
|
+
public class Win32 {
|
|
115
|
+
[DllImport("user32.dll")]
|
|
116
|
+
public static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
117
|
+
[DllImport("user32.dll")]
|
|
118
|
+
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
|
119
|
+
}
|
|
120
|
+
"@
|
|
121
|
+
$unity = Get-Process | Where-Object {$_.MainWindowTitle -like "*Unity*"} | Select-Object -First 1
|
|
122
|
+
if ($unity) {
|
|
123
|
+
[Win32]::ShowWindow($unity.MainWindowHandle, 9)
|
|
124
|
+
[Win32]::SetForegroundWindow($unity.MainWindowHandle)
|
|
125
|
+
}
|
|
126
|
+
'''
|
|
127
|
+
else:
|
|
128
|
+
# Try to find window by title - escape special PowerShell characters
|
|
129
|
+
safe_title = window_title.replace("'", "''").replace("`", "``")
|
|
130
|
+
script = f'''
|
|
131
|
+
Add-Type @"
|
|
132
|
+
using System;
|
|
133
|
+
using System.Runtime.InteropServices;
|
|
134
|
+
public class Win32 {{
|
|
135
|
+
[DllImport("user32.dll")]
|
|
136
|
+
public static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
137
|
+
[DllImport("user32.dll")]
|
|
138
|
+
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
|
139
|
+
}}
|
|
140
|
+
"@
|
|
141
|
+
$proc = Get-Process | Where-Object {{$_.MainWindowTitle -eq '{safe_title}'}} | Select-Object -First 1
|
|
142
|
+
if ($proc) {{
|
|
143
|
+
[Win32]::ShowWindow($proc.MainWindowHandle, 9)
|
|
144
|
+
[Win32]::SetForegroundWindow($proc.MainWindowHandle)
|
|
145
|
+
}}
|
|
146
|
+
'''
|
|
147
|
+
result = subprocess.run(
|
|
148
|
+
["powershell", "-Command", script],
|
|
149
|
+
capture_output=True,
|
|
150
|
+
text=True,
|
|
151
|
+
timeout=5,
|
|
152
|
+
)
|
|
153
|
+
return result.returncode == 0
|
|
154
|
+
except Exception as e:
|
|
155
|
+
logger.debug(f"Failed to focus window {window_title}: {e}")
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _get_frontmost_app_linux() -> str | None:
|
|
160
|
+
"""Get the window ID of the frontmost window on Linux."""
|
|
161
|
+
try:
|
|
162
|
+
result = subprocess.run(
|
|
163
|
+
["xdotool", "getactivewindow"],
|
|
164
|
+
capture_output=True,
|
|
165
|
+
text=True,
|
|
166
|
+
timeout=5,
|
|
167
|
+
)
|
|
168
|
+
if result.returncode == 0:
|
|
169
|
+
return result.stdout.strip()
|
|
170
|
+
except Exception as e:
|
|
171
|
+
logger.debug(f"Failed to get active window: {e}")
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _focus_app_linux(window_id: str) -> bool:
|
|
176
|
+
"""Focus a window by ID on Linux, or Unity by name."""
|
|
177
|
+
try:
|
|
178
|
+
if window_id == "Unity":
|
|
179
|
+
# Find Unity window by name pattern
|
|
180
|
+
result = subprocess.run(
|
|
181
|
+
["xdotool", "search", "--name", "Unity"],
|
|
182
|
+
capture_output=True,
|
|
183
|
+
text=True,
|
|
184
|
+
timeout=5,
|
|
185
|
+
)
|
|
186
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
187
|
+
window_id = result.stdout.strip().split("\n")[0]
|
|
188
|
+
else:
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
result = subprocess.run(
|
|
192
|
+
["xdotool", "windowactivate", window_id],
|
|
193
|
+
capture_output=True,
|
|
194
|
+
text=True,
|
|
195
|
+
timeout=5,
|
|
196
|
+
)
|
|
197
|
+
return result.returncode == 0
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.debug(f"Failed to focus window {window_id}: {e}")
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _get_frontmost_app() -> str | None:
|
|
204
|
+
"""Get the frontmost application/window (platform-specific)."""
|
|
205
|
+
system = platform.system()
|
|
206
|
+
if system == "Darwin":
|
|
207
|
+
return _get_frontmost_app_macos()
|
|
208
|
+
elif system == "Windows":
|
|
209
|
+
return _get_frontmost_app_windows()
|
|
210
|
+
elif system == "Linux":
|
|
211
|
+
return _get_frontmost_app_linux()
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _focus_app(app_or_window: str) -> bool:
|
|
216
|
+
"""Focus an application/window (platform-specific)."""
|
|
217
|
+
system = platform.system()
|
|
218
|
+
if system == "Darwin":
|
|
219
|
+
return _focus_app_macos(app_or_window)
|
|
220
|
+
elif system == "Windows":
|
|
221
|
+
return _focus_app_windows(app_or_window)
|
|
222
|
+
elif system == "Linux":
|
|
223
|
+
return _focus_app_linux(app_or_window)
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
async def nudge_unity_focus(
|
|
228
|
+
focus_duration_s: float = 0.5,
|
|
229
|
+
force: bool = False,
|
|
230
|
+
) -> bool:
|
|
231
|
+
"""
|
|
232
|
+
Temporarily focus Unity to allow it to process, then return focus.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
focus_duration_s: How long to keep Unity focused (seconds)
|
|
236
|
+
force: If True, ignore the minimum interval between nudges
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
True if nudge was performed, False if skipped or failed
|
|
240
|
+
"""
|
|
241
|
+
global _last_nudge_time
|
|
242
|
+
|
|
243
|
+
if not _is_available():
|
|
244
|
+
logger.debug("Focus nudging not available on this platform")
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
# Rate limit nudges
|
|
248
|
+
now = time.monotonic()
|
|
249
|
+
if not force and (now - _last_nudge_time) < _MIN_NUDGE_INTERVAL_S:
|
|
250
|
+
logger.info("Skipping nudge - too soon since last nudge")
|
|
251
|
+
return False
|
|
252
|
+
|
|
253
|
+
# Get current frontmost app
|
|
254
|
+
original_app = _get_frontmost_app()
|
|
255
|
+
if original_app is None:
|
|
256
|
+
logger.debug("Could not determine frontmost app")
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
# Check if Unity is already focused (no nudge needed)
|
|
260
|
+
if "Unity" in original_app:
|
|
261
|
+
logger.debug("Unity already focused, no nudge needed")
|
|
262
|
+
return False
|
|
263
|
+
|
|
264
|
+
logger.info(f"Nudging Unity focus (will return to {original_app})")
|
|
265
|
+
_last_nudge_time = now
|
|
266
|
+
|
|
267
|
+
# Focus Unity
|
|
268
|
+
if not _focus_app("Unity"):
|
|
269
|
+
logger.warning("Failed to focus Unity")
|
|
270
|
+
return False
|
|
271
|
+
|
|
272
|
+
# Wait for Unity to process
|
|
273
|
+
await asyncio.sleep(focus_duration_s)
|
|
274
|
+
|
|
275
|
+
# Return focus to original app
|
|
276
|
+
if original_app and original_app != "Unity":
|
|
277
|
+
if _focus_app(original_app):
|
|
278
|
+
logger.info(f"Returned focus to {original_app}")
|
|
279
|
+
else:
|
|
280
|
+
logger.warning(f"Failed to return focus to {original_app}")
|
|
281
|
+
|
|
282
|
+
return True
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def should_nudge(
|
|
286
|
+
status: str,
|
|
287
|
+
editor_is_focused: bool,
|
|
288
|
+
last_update_unix_ms: int | None,
|
|
289
|
+
current_time_ms: int | None = None,
|
|
290
|
+
stall_threshold_ms: int = 10_000,
|
|
291
|
+
) -> bool:
|
|
292
|
+
"""
|
|
293
|
+
Determine if we should nudge Unity based on test job state.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
status: Job status ("running", "succeeded", "failed")
|
|
297
|
+
editor_is_focused: Whether Unity reports being focused
|
|
298
|
+
last_update_unix_ms: Last time the job was updated (Unix ms)
|
|
299
|
+
current_time_ms: Current time (Unix ms), or None to use current time
|
|
300
|
+
stall_threshold_ms: How long without updates before considering it stalled
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
True if conditions suggest a nudge would help
|
|
304
|
+
"""
|
|
305
|
+
# Only nudge running jobs
|
|
306
|
+
if status != "running":
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
# Only nudge unfocused Unity
|
|
310
|
+
if editor_is_focused:
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
# Check if job appears stalled
|
|
314
|
+
if last_update_unix_ms is None:
|
|
315
|
+
return True # No updates yet, might be stuck at start
|
|
316
|
+
|
|
317
|
+
if current_time_ms is None:
|
|
318
|
+
current_time_ms = int(time.time() * 1000)
|
|
319
|
+
|
|
320
|
+
time_since_update_ms = current_time_ms - last_update_unix_ms
|
|
321
|
+
return time_since_update_ms > stall_threshold_ms
|
__init__.py
DELETED
|
File without changes
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
main.py,sha256=OH_Ux5Aj43q1lXPu4AS8zafRnFHZWFYCDZbswReQOac,18011
|
|
3
|
-
core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
core/config.py,sha256=czkTtNji1crQcQbUvmdx4OL7f-RBqkVhj_PtHh-w7rs,1623
|
|
5
|
-
core/logging_decorator.py,sha256=D9CD7rFvQz-MBG-G4inizQj0Ivr6dfc9RBmTrw7q8mI,1383
|
|
6
|
-
core/telemetry.py,sha256=eHjYgzd8f7eTwSwF2Kbi8D4TtJIcdaDjKLeo1c-0hVA,19829
|
|
7
|
-
core/telemetry_decorator.py,sha256=ycSTrzVNCDQHSd-xmIWOpVfKFURPxpiZe_XkOQAGDAo,6705
|
|
8
|
-
mcpforunityserver-8.5.0.dist-info/licenses/LICENSE,sha256=bv5lDJZQEqxBgjjc1rkRbkEwpSIHF-8N-1Od0VnEJFw,1066
|
|
9
|
-
models/__init__.py,sha256=JlscZkGWE9TRmSoBi99v_LSl8OAFNGmr8463PYkXin4,179
|
|
10
|
-
models/models.py,sha256=heXuvdBtdats1SGwW8wKFFHM0qR4hA6A7qETn5s9BZ0,1827
|
|
11
|
-
models/unity_response.py,sha256=oJ1PTsnNc5VBC-9OgM59C0C-R9N-GdmEdmz_yph4GSU,1454
|
|
12
|
-
routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
services/custom_tool_service.py,sha256=ZOb6LsOQc3C9n3iLn5b7QaG3WJ25M-Id3WG_iE1YhMY,11960
|
|
15
|
-
services/registry/__init__.py,sha256=QCwcYThvGF0kBt3WR6DBskdyxkegJC7NymEChgJA-YM,470
|
|
16
|
-
services/registry/resource_registry.py,sha256=T_Kznqgvt5kKgV7mU85nb0LlFuB4rg-Tm4Cjhxt-IcI,1467
|
|
17
|
-
services/registry/tool_registry.py,sha256=9tMwOP07JE92QFYUS4KvoysO0qC9pkBD5B79kjRsSPw,1304
|
|
18
|
-
services/resources/__init__.py,sha256=O5heeMcgCswnQX1qG2nNtMeAZIaLut734qD7t5UsA0k,2801
|
|
19
|
-
services/resources/active_tool.py,sha256=YTbsiy_hmnKH2q7IoM7oYD7pJkoveZTszRiL1PlhO9M,1474
|
|
20
|
-
services/resources/custom_tools.py,sha256=8lyryGhN3vD2LwMt6ZyKIp5ONtxdI1nfcCAlYjlfQnQ,1704
|
|
21
|
-
services/resources/editor_state.py,sha256=acrSyMfdulRgYQIn7wKHqKqyw4uED_oUf9GU-4o4GAg,1497
|
|
22
|
-
services/resources/layers.py,sha256=q4UQ5PUVUVhmM5l3oXID1wa_wOWAS8l5BGXadBgFuwY,1080
|
|
23
|
-
services/resources/menu_items.py,sha256=9SNycjwTXoeS1ZHra0Y1fTyCjSEdPCo34JyxtuqauG8,1021
|
|
24
|
-
services/resources/prefab_stage.py,sha256=C3mn3UapKYVOA8QUNmLsYreG5YiXdlvGm9ypHQeKBeQ,1382
|
|
25
|
-
services/resources/project_info.py,sha256=gSVSFfwP0u2FmxSowOkdbNoSSQHxfQtLfndvoCXTVSw,1323
|
|
26
|
-
services/resources/selection.py,sha256=4rI5Bdkes4uxtMc_5jQhUaqUl-iprhIiTWqnOJl8tmg,1839
|
|
27
|
-
services/resources/tags.py,sha256=7EhmQjMotz85DSSr7cVKYIy7LPT5mmPfrEySr1mTE6w,1049
|
|
28
|
-
services/resources/tests.py,sha256=xDvvgesPSU93nLD_ERQopOpkpq69pbMEqmFsJd0jekI,2063
|
|
29
|
-
services/resources/unity_instances.py,sha256=fR0cVopGQnmF41IFDycwlo2XniKstfJWLGobgJeiabE,4348
|
|
30
|
-
services/resources/windows.py,sha256=--QVsb0oyoBpSjK2D4kPcZFSe2zdR-t_KSHP-e2QNoY,1427
|
|
31
|
-
services/tools/__init__.py,sha256=3Qav7fAowZ1_TbDRdZQQQES53gv2lTs-2D7PGECnlbM,2353
|
|
32
|
-
services/tools/batch_execute.py,sha256=_ByjffeXQB9j64mcjaxJmrnbSJrMn0f9_6Zh9BBI_2c,2898
|
|
33
|
-
services/tools/debug_request_context.py,sha256=WQBtQdXSH5stw2MAwIM32H6jGwUVQOgU2r35VUWLlYo,2765
|
|
34
|
-
services/tools/execute_custom_tool.py,sha256=K2qaO4-FTPz0_3j53hhDP9idjC002ugc8C03FtHGTbY,1376
|
|
35
|
-
services/tools/execute_menu_item.py,sha256=FAC-1v_TwOcy6GSxkogDsVxeRtdap0DsPlIngf8uJdU,1184
|
|
36
|
-
services/tools/find_in_file.py,sha256=xp80lqRN2cdZc3XGJWlCpeQEy6WnwyKOj2l5WiHNx0Q,6379
|
|
37
|
-
services/tools/manage_asset.py,sha256=Kpqr82cmXH7wxXub3O0D8whksORSqn9nDRjskDe_A_w,7534
|
|
38
|
-
services/tools/manage_editor.py,sha256=_HZRT-_hBakH0g6p7BpxTv3gWpxsaV6KNGRol-qknwo,3243
|
|
39
|
-
services/tools/manage_gameobject.py,sha256=kYIouvt-iNUEsY0VIWp4FqagLjo7Up2TwKDhB4Nfxmo,14213
|
|
40
|
-
services/tools/manage_material.py,sha256=wZB2H4orhL6wG9TTnmnk-Lj2Gj_zvg7koxW3t319BLU,3545
|
|
41
|
-
services/tools/manage_prefabs.py,sha256=73XzznjFNOm1SazW_Y7l6uGIE7wosMpAIVQs8xpvK9A,3037
|
|
42
|
-
services/tools/manage_scene.py,sha256=3BhIsbbtGiMNqBMQMqEsB4ajYmtx-VwWl-krOkFR_Bw,4648
|
|
43
|
-
services/tools/manage_script.py,sha256=lPA5HcS4Al0RiQVz-S6qahFTcPqsk3GSLLXJWHri8P4,27557
|
|
44
|
-
services/tools/manage_scriptable_object.py,sha256=Oi03CJLgepaWR59V-nJiAjnCC8at4YqFhRGpACruqgw,3150
|
|
45
|
-
services/tools/manage_shader.py,sha256=HHnHKh7vLij3p8FAinNsPdZGEKivgwSUTxdgDydfmbs,2882
|
|
46
|
-
services/tools/read_console.py,sha256=gZWEf0Ru0hvN9oJUZqZ4w-mMBBLm5Z5KAUPv282XbYQ,4091
|
|
47
|
-
services/tools/run_tests.py,sha256=LBVwGasLvmF4k1FiX3DdBQ8udh89WZJFiVHfJRWGvOs,3313
|
|
48
|
-
services/tools/script_apply_edits.py,sha256=qPm_PsmsK3mYXnziX_btyk8CaB66LTqpDFA2Y4ebZ4U,47504
|
|
49
|
-
services/tools/set_active_instance.py,sha256=B18Y8Jga0pKsx9mFywXr1tWfy0cJVopIMXYO-UJ1jOU,4136
|
|
50
|
-
services/tools/utils.py,sha256=4ZgfIu178eXZqRyzs8X77B5lKLP1f73OZoGBSDNokJ4,2409
|
|
51
|
-
transport/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
52
|
-
transport/models.py,sha256=6wp7wsmSaeeJEvUGXPF1m6zuJnxJ1NJlCC4YZ9oQIq0,1226
|
|
53
|
-
transport/plugin_hub.py,sha256=55R00ohrmUI0mk_smc_8BsYTvrQMPX4wwsvqXprj0Vk,15596
|
|
54
|
-
transport/plugin_registry.py,sha256=nW-7O7PN0QUgSWivZTkpAVKKq9ZOe2b2yeIdpaNt_3I,4359
|
|
55
|
-
transport/unity_instance_middleware.py,sha256=a-ULWU9b86w0CbYN3meyLxWGxTBXL5CQmBKZmmQ0xZQ,6197
|
|
56
|
-
transport/unity_transport.py,sha256=dvwCjo2jRvnFXd8ruOL36C8W4P1VIQ91qreS2750lPM,3307
|
|
57
|
-
transport/legacy/port_discovery.py,sha256=qM_mtndbYjAj4qPSZEWVeXFOt5_nKczG9pQqORXTBJ0,12768
|
|
58
|
-
transport/legacy/stdio_port_registry.py,sha256=j4iARuP6wetppNDG8qKeuvo1bJKcSlgEhZvSyl_uf0A,2313
|
|
59
|
-
transport/legacy/unity_connection.py,sha256=ujUX9WX7Gb-fxQveHts3uiepTPzFq8i7-XG7u5gSPuM,32668
|
|
60
|
-
utils/module_discovery.py,sha256=My48ofB1BUqxiBoAZAGbEaLQYdsrDhMm8MayBP_bUSQ,2005
|
|
61
|
-
utils/reload_sentinel.py,sha256=s1xMWhl-r2XwN0OUbiUv_VGUy8TvLtV5bkql-5n2DT0,373
|
|
62
|
-
mcpforunityserver-8.5.0.dist-info/METADATA,sha256=3YWJc7I-EKxcLzMgG0MweNSJm3QR_VYCNTTwwB45UNE,5712
|
|
63
|
-
mcpforunityserver-8.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
64
|
-
mcpforunityserver-8.5.0.dist-info/entry_points.txt,sha256=vCtkqw-J9t4pj7JwUZcB54_keVx7DpAR3fYzK6i-s6g,44
|
|
65
|
-
mcpforunityserver-8.5.0.dist-info/top_level.txt,sha256=YKU5e5dREMfCnoVpmlsTm9bku7oqnrzSZ9FeTgjoxJw,58
|
|
66
|
-
mcpforunityserver-8.5.0.dist-info/RECORD,,
|
routes/__init__.py
DELETED
|
File without changes
|
|
File without changes
|