mcpforunityserver 9.0.2__tar.gz → 9.0.7__tar.gz
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.
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/PKG-INFO +3 -2
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/README.md +1 -1
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/pyproject.toml +25 -1
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/mcpforunityserver.egg-info/PKG-INFO +3 -2
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/mcpforunityserver.egg-info/SOURCES.txt +1 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/mcpforunityserver.egg-info/requires.txt +1 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/refresh_unity.py +6 -3
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/run_tests.py +31 -6
- mcpforunityserver-9.0.7/src/utils/focus_nudge.py +321 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/LICENSE +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/setup.cfg +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/__init__.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/core/__init__.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/core/config.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/core/logging_decorator.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/core/telemetry.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/core/telemetry_decorator.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/main.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/mcpforunityserver.egg-info/dependency_links.txt +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/mcpforunityserver.egg-info/entry_points.txt +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/mcpforunityserver.egg-info/top_level.txt +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/models/__init__.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/models/models.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/models/unity_response.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/__init__.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/custom_tool_service.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/registry/__init__.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/registry/resource_registry.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/registry/tool_registry.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/resources/__init__.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/resources/active_tool.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/resources/custom_tools.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/resources/editor_state.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/resources/gameobject.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/resources/layers.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/resources/menu_items.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/resources/prefab_stage.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/resources/project_info.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/resources/selection.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/resources/tags.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/resources/tests.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/resources/unity_instances.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/resources/windows.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/state/external_changes_scanner.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/__init__.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/batch_execute.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/debug_request_context.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/execute_custom_tool.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/execute_menu_item.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/find_gameobjects.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/find_in_file.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/manage_asset.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/manage_components.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/manage_editor.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/manage_gameobject.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/manage_material.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/manage_prefabs.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/manage_scene.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/manage_script.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/manage_scriptable_object.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/manage_shader.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/manage_vfx.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/preflight.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/read_console.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/script_apply_edits.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/set_active_instance.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/utils.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/transport/__init__.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/transport/legacy/port_discovery.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/transport/legacy/stdio_port_registry.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/transport/legacy/unity_connection.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/transport/models.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/transport/plugin_hub.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/transport/plugin_registry.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/transport/unity_instance_middleware.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/transport/unity_transport.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/utils/module_discovery.py +0 -0
- {mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/utils/reload_sentinel.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcpforunityserver
|
|
3
|
-
Version: 9.0.
|
|
3
|
+
Version: 9.0.7
|
|
4
4
|
Summary: MCP for Unity Server: A Unity package for Unity Editor integration via the Model Context Protocol (MCP).
|
|
5
5
|
Author-email: Marcus Sanatan <msanatan@gmail.com>, David Sarno <david.sarno@gmail.com>, Wu Shutong <martinwfire@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -35,6 +35,7 @@ Requires-Dist: uvicorn>=0.35.0
|
|
|
35
35
|
Provides-Extra: dev
|
|
36
36
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
37
37
|
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
38
39
|
Dynamic: license-file
|
|
39
40
|
|
|
40
41
|
# MCP for Unity Server
|
|
@@ -108,7 +109,7 @@ Use this to run the latest released version from the repository. Change the vers
|
|
|
108
109
|
"command": "uvx",
|
|
109
110
|
"args": [
|
|
110
111
|
"--from",
|
|
111
|
-
"git+https://github.com/CoplayDev/unity-mcp@v9.0.
|
|
112
|
+
"git+https://github.com/CoplayDev/unity-mcp@v9.0.7#subdirectory=Server",
|
|
112
113
|
"mcp-for-unity",
|
|
113
114
|
"--transport",
|
|
114
115
|
"stdio"
|
|
@@ -69,7 +69,7 @@ Use this to run the latest released version from the repository. Change the vers
|
|
|
69
69
|
"command": "uvx",
|
|
70
70
|
"args": [
|
|
71
71
|
"--from",
|
|
72
|
-
"git+https://github.com/CoplayDev/unity-mcp@v9.0.
|
|
72
|
+
"git+https://github.com/CoplayDev/unity-mcp@v9.0.7#subdirectory=Server",
|
|
73
73
|
"mcp-for-unity",
|
|
74
74
|
"--transport",
|
|
75
75
|
"stdio"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mcpforunityserver"
|
|
3
|
-
version = "9.0.
|
|
3
|
+
version = "9.0.7"
|
|
4
4
|
description = "MCP for Unity Server: A Unity package for Unity Editor integration via the Model Context Protocol (MCP)."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -42,6 +42,7 @@ dependencies = [
|
|
|
42
42
|
dev = [
|
|
43
43
|
"pytest>=8.0.0",
|
|
44
44
|
"pytest-asyncio>=0.23",
|
|
45
|
+
"pytest-cov>=4.1.0",
|
|
45
46
|
]
|
|
46
47
|
|
|
47
48
|
[project.urls]
|
|
@@ -54,3 +55,26 @@ mcp-for-unity = "main:main"
|
|
|
54
55
|
[build-system]
|
|
55
56
|
requires = ["setuptools>=64.0.0", "wheel"]
|
|
56
57
|
build-backend = "setuptools.build_meta"
|
|
58
|
+
|
|
59
|
+
[tool.coverage.run]
|
|
60
|
+
source = ["src"]
|
|
61
|
+
omit = [
|
|
62
|
+
"*/tests/*",
|
|
63
|
+
"*/test_*.py",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
[tool.coverage.report]
|
|
67
|
+
exclude_lines = [
|
|
68
|
+
"pragma: no cover",
|
|
69
|
+
"def __repr__",
|
|
70
|
+
"raise AssertionError",
|
|
71
|
+
"raise NotImplementedError",
|
|
72
|
+
"if __name__ == \"__main__\":",
|
|
73
|
+
"if TYPE_CHECKING:",
|
|
74
|
+
"@abstractmethod",
|
|
75
|
+
]
|
|
76
|
+
precision = 2
|
|
77
|
+
show_missing = true
|
|
78
|
+
|
|
79
|
+
[tool.coverage.html]
|
|
80
|
+
directory = "htmlcov"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcpforunityserver
|
|
3
|
-
Version: 9.0.
|
|
3
|
+
Version: 9.0.7
|
|
4
4
|
Summary: MCP for Unity Server: A Unity package for Unity Editor integration via the Model Context Protocol (MCP).
|
|
5
5
|
Author-email: Marcus Sanatan <msanatan@gmail.com>, David Sarno <david.sarno@gmail.com>, Wu Shutong <martinwfire@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -35,6 +35,7 @@ Requires-Dist: uvicorn>=0.35.0
|
|
|
35
35
|
Provides-Extra: dev
|
|
36
36
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
37
37
|
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
38
39
|
Dynamic: license-file
|
|
39
40
|
|
|
40
41
|
# MCP for Unity Server
|
|
@@ -108,7 +109,7 @@ Use this to run the latest released version from the repository. Change the vers
|
|
|
108
109
|
"command": "uvx",
|
|
109
110
|
"args": [
|
|
110
111
|
"--from",
|
|
111
|
-
"git+https://github.com/CoplayDev/unity-mcp@v9.0.
|
|
112
|
+
"git+https://github.com/CoplayDev/unity-mcp@v9.0.7#subdirectory=Server",
|
|
112
113
|
"mcp-for-unity",
|
|
113
114
|
"--transport",
|
|
114
115
|
"stdio"
|
|
@@ -55,10 +55,13 @@ async def refresh_unity(
|
|
|
55
55
|
# interpret that as a hard failure (#503-style loops).
|
|
56
56
|
if isinstance(response, dict) and not response.get("success", True):
|
|
57
57
|
hint = response.get("hint")
|
|
58
|
-
err = (response.get("error") or response.get("message") or "")
|
|
58
|
+
err = (response.get("error") or response.get("message") or "").lower()
|
|
59
59
|
reason = _extract_response_reason(response)
|
|
60
|
-
is_retryable = (
|
|
61
|
-
"
|
|
60
|
+
is_retryable = (
|
|
61
|
+
hint == "retry"
|
|
62
|
+
or "disconnected" in err
|
|
63
|
+
or "could not connect" in err # Connection failed during domain reload
|
|
64
|
+
)
|
|
62
65
|
if (not wait_for_ready) or (not is_retryable):
|
|
63
66
|
return MCPResponse(**response)
|
|
64
67
|
if reason not in {"reloading", "no_unity_session"}:
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
5
7
|
from typing import Annotated, Any, Literal
|
|
6
8
|
|
|
7
9
|
from fastmcp import Context
|
|
@@ -14,6 +16,9 @@ from services.tools import get_unity_instance_from_context
|
|
|
14
16
|
from services.tools.preflight import preflight
|
|
15
17
|
import transport.unity_transport as unity_transport
|
|
16
18
|
from transport.legacy.unity_connection import async_send_command_with_retry
|
|
19
|
+
from utils.focus_nudge import nudge_unity_focus, should_nudge
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
17
22
|
|
|
18
23
|
|
|
19
24
|
class RunTestsSummary(BaseModel):
|
|
@@ -195,28 +200,48 @@ async def get_test_job(
|
|
|
195
200
|
if wait_timeout and wait_timeout > 0:
|
|
196
201
|
deadline = asyncio.get_event_loop().time() + wait_timeout
|
|
197
202
|
poll_interval = 2.0 # Poll Unity every 2 seconds
|
|
198
|
-
|
|
203
|
+
|
|
199
204
|
while True:
|
|
200
205
|
response = await _fetch_status()
|
|
201
|
-
|
|
206
|
+
|
|
202
207
|
if not isinstance(response, dict):
|
|
203
208
|
return MCPResponse(success=False, error=str(response))
|
|
204
|
-
|
|
209
|
+
|
|
205
210
|
if not response.get("success", True):
|
|
206
211
|
return MCPResponse(**response)
|
|
207
|
-
|
|
212
|
+
|
|
208
213
|
# Check if tests are done
|
|
209
214
|
data = response.get("data", {})
|
|
210
215
|
status = data.get("status", "")
|
|
211
216
|
if status in ("succeeded", "failed", "cancelled"):
|
|
212
217
|
return GetTestJobResponse(**response)
|
|
213
|
-
|
|
218
|
+
|
|
219
|
+
# Check if Unity needs a focus nudge to make progress
|
|
220
|
+
# This handles OS-level throttling (e.g., macOS App Nap) that can
|
|
221
|
+
# stall PlayMode tests when Unity is in the background.
|
|
222
|
+
progress = data.get("progress", {})
|
|
223
|
+
editor_is_focused = progress.get("editor_is_focused", True)
|
|
224
|
+
last_update_unix_ms = data.get("last_update_unix_ms")
|
|
225
|
+
current_time_ms = int(time.time() * 1000)
|
|
226
|
+
|
|
227
|
+
if should_nudge(
|
|
228
|
+
status=status,
|
|
229
|
+
editor_is_focused=editor_is_focused,
|
|
230
|
+
last_update_unix_ms=last_update_unix_ms,
|
|
231
|
+
current_time_ms=current_time_ms,
|
|
232
|
+
stall_threshold_ms=10_000, # 10 seconds without progress
|
|
233
|
+
):
|
|
234
|
+
logger.info(f"Test job {job_id} appears stalled (unfocused Unity), attempting nudge...")
|
|
235
|
+
nudged = await nudge_unity_focus(focus_duration_s=0.5)
|
|
236
|
+
if nudged:
|
|
237
|
+
logger.info(f"Test job {job_id} nudge completed")
|
|
238
|
+
|
|
214
239
|
# Check timeout
|
|
215
240
|
remaining = deadline - asyncio.get_event_loop().time()
|
|
216
241
|
if remaining <= 0:
|
|
217
242
|
# Timeout reached, return current status
|
|
218
243
|
return GetTestJobResponse(**response)
|
|
219
|
-
|
|
244
|
+
|
|
220
245
|
# Wait before next poll (but don't exceed remaining time)
|
|
221
246
|
await asyncio.sleep(min(poll_interval, remaining))
|
|
222
247
|
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/mcpforunityserver.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/mcpforunityserver.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/registry/resource_registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/resources/unity_instances.py
RENAMED
|
File without changes
|
|
File without changes
|
{mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/state/external_changes_scanner.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/debug_request_context.py
RENAMED
|
File without changes
|
{mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/execute_custom_tool.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/manage_scriptable_object.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/script_apply_edits.py
RENAMED
|
File without changes
|
{mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/services/tools/set_active_instance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/transport/legacy/stdio_port_registry.py
RENAMED
|
File without changes
|
{mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/transport/legacy/unity_connection.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcpforunityserver-9.0.2 → mcpforunityserver-9.0.7}/src/transport/unity_instance_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|