mcpforunityserver 8.7.0__py3-none-any.whl → 8.7.1__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.
- {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-8.7.1.dist-info}/METADATA +2 -2
- {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-8.7.1.dist-info}/RECORD +10 -10
- services/tools/read_console.py +25 -15
- services/tools/refresh_unity.py +4 -4
- transport/legacy/unity_connection.py +101 -16
- transport/plugin_hub.py +47 -11
- {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-8.7.1.dist-info}/WHEEL +0 -0
- {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-8.7.1.dist-info}/entry_points.txt +0 -0
- {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-8.7.1.dist-info}/licenses/LICENSE +0 -0
- {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-8.7.1.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcpforunityserver
|
|
3
|
-
Version: 8.7.
|
|
3
|
+
Version: 8.7.1
|
|
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
|
|
@@ -108,7 +108,7 @@ Use this to run the latest released version from the repository. Change the vers
|
|
|
108
108
|
"command": "uvx",
|
|
109
109
|
"args": [
|
|
110
110
|
"--from",
|
|
111
|
-
"git+https://github.com/CoplayDev/unity-mcp@v8.7.
|
|
111
|
+
"git+https://github.com/CoplayDev/unity-mcp@v8.7.1#subdirectory=Server",
|
|
112
112
|
"mcp-for-unity",
|
|
113
113
|
"--transport",
|
|
114
114
|
"stdio"
|
|
@@ -5,7 +5,7 @@ core/config.py,sha256=czkTtNji1crQcQbUvmdx4OL7f-RBqkVhj_PtHh-w7rs,1623
|
|
|
5
5
|
core/logging_decorator.py,sha256=D9CD7rFvQz-MBG-G4inizQj0Ivr6dfc9RBmTrw7q8mI,1383
|
|
6
6
|
core/telemetry.py,sha256=eHjYgzd8f7eTwSwF2Kbi8D4TtJIcdaDjKLeo1c-0hVA,19829
|
|
7
7
|
core/telemetry_decorator.py,sha256=ycSTrzVNCDQHSd-xmIWOpVfKFURPxpiZe_XkOQAGDAo,6705
|
|
8
|
-
mcpforunityserver-8.7.
|
|
8
|
+
mcpforunityserver-8.7.1.dist-info/licenses/LICENSE,sha256=bv5lDJZQEqxBgjjc1rkRbkEwpSIHF-8N-1Od0VnEJFw,1066
|
|
9
9
|
models/__init__.py,sha256=JlscZkGWE9TRmSoBi99v_LSl8OAFNGmr8463PYkXin4,179
|
|
10
10
|
models/models.py,sha256=heXuvdBtdats1SGwW8wKFFHM0qR4hA6A7qETn5s9BZ0,1827
|
|
11
11
|
models/unity_response.py,sha256=oJ1PTsnNc5VBC-9OgM59C0C-R9N-GdmEdmz_yph4GSU,1454
|
|
@@ -46,8 +46,8 @@ services/tools/manage_script.py,sha256=lPA5HcS4Al0RiQVz-S6qahFTcPqsk3GSLLXJWHri8
|
|
|
46
46
|
services/tools/manage_scriptable_object.py,sha256=Oi03CJLgepaWR59V-nJiAjnCC8at4YqFhRGpACruqgw,3150
|
|
47
47
|
services/tools/manage_shader.py,sha256=HHnHKh7vLij3p8FAinNsPdZGEKivgwSUTxdgDydfmbs,2882
|
|
48
48
|
services/tools/preflight.py,sha256=VJn61h-9pvoVaCyKL7DTKLfbpoZfNK4fnRmj91c2o8M,4093
|
|
49
|
-
services/tools/read_console.py,sha256=
|
|
50
|
-
services/tools/refresh_unity.py,sha256=
|
|
49
|
+
services/tools/read_console.py,sha256=ELryGXGhZi56pqe4cSdrNDaZGBlUydQIeJ5q86fq_Uo,5201
|
|
50
|
+
services/tools/refresh_unity.py,sha256=xlmqMeAxmYEa5l4OduYdDYWSKJPm2QoJg5CrxCJY8_A,3859
|
|
51
51
|
services/tools/run_tests.py,sha256=8CqmgRN6Bata666ytF_S9no4gaFmHCmeZM82ZwNQJ68,4666
|
|
52
52
|
services/tools/script_apply_edits.py,sha256=qPm_PsmsK3mYXnziX_btyk8CaB66LTqpDFA2Y4ebZ4U,47504
|
|
53
53
|
services/tools/set_active_instance.py,sha256=B18Y8Jga0pKsx9mFywXr1tWfy0cJVopIMXYO-UJ1jOU,4136
|
|
@@ -55,17 +55,17 @@ services/tools/test_jobs.py,sha256=K6HjkzWPjJNldrp-Vq5gPH7oBkCq_sJZYXkK_Vg6I_I,4
|
|
|
55
55
|
services/tools/utils.py,sha256=4ZgfIu178eXZqRyzs8X77B5lKLP1f73OZoGBSDNokJ4,2409
|
|
56
56
|
transport/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
57
57
|
transport/models.py,sha256=6wp7wsmSaeeJEvUGXPF1m6zuJnxJ1NJlCC4YZ9oQIq0,1226
|
|
58
|
-
transport/plugin_hub.py,sha256=
|
|
58
|
+
transport/plugin_hub.py,sha256=77xYWunNw8hHRJuimx3Qrxu9wqlKURpuunkUqkqx9kE,22845
|
|
59
59
|
transport/plugin_registry.py,sha256=nW-7O7PN0QUgSWivZTkpAVKKq9ZOe2b2yeIdpaNt_3I,4359
|
|
60
60
|
transport/unity_instance_middleware.py,sha256=kf1QeA138r7PaC98dcMDYtUPGWZ4EUmZGESc2DdiWQs,10429
|
|
61
61
|
transport/unity_transport.py,sha256=_cFVgD3pzFZRcDXANq4oPFYSoI6jntSGaN22dJC8LRU,3880
|
|
62
62
|
transport/legacy/port_discovery.py,sha256=qM_mtndbYjAj4qPSZEWVeXFOt5_nKczG9pQqORXTBJ0,12768
|
|
63
63
|
transport/legacy/stdio_port_registry.py,sha256=j4iARuP6wetppNDG8qKeuvo1bJKcSlgEhZvSyl_uf0A,2313
|
|
64
|
-
transport/legacy/unity_connection.py,sha256=
|
|
64
|
+
transport/legacy/unity_connection.py,sha256=psMwMDiUXKUSrpP4UO9Lgu5PH-Mu6QPY-r5o81WIkVg,35802
|
|
65
65
|
utils/module_discovery.py,sha256=My48ofB1BUqxiBoAZAGbEaLQYdsrDhMm8MayBP_bUSQ,2005
|
|
66
66
|
utils/reload_sentinel.py,sha256=s1xMWhl-r2XwN0OUbiUv_VGUy8TvLtV5bkql-5n2DT0,373
|
|
67
|
-
mcpforunityserver-8.7.
|
|
68
|
-
mcpforunityserver-8.7.
|
|
69
|
-
mcpforunityserver-8.7.
|
|
70
|
-
mcpforunityserver-8.7.
|
|
71
|
-
mcpforunityserver-8.7.
|
|
67
|
+
mcpforunityserver-8.7.1.dist-info/METADATA,sha256=OzxsNyocEXg1K-ND-AesGUKXM8pQx_7CGUrW_XsKXMg,5712
|
|
68
|
+
mcpforunityserver-8.7.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
69
|
+
mcpforunityserver-8.7.1.dist-info/entry_points.txt,sha256=vCtkqw-J9t4pj7JwUZcB54_keVx7DpAR3fYzK6i-s6g,44
|
|
70
|
+
mcpforunityserver-8.7.1.dist-info/top_level.txt,sha256=YKU5e5dREMfCnoVpmlsTm9bku7oqnrzSZ9FeTgjoxJw,58
|
|
71
|
+
mcpforunityserver-8.7.1.dist-info/RECORD,,
|
services/tools/read_console.py
CHANGED
|
@@ -11,8 +11,15 @@ from transport.unity_transport import send_with_unity_instance
|
|
|
11
11
|
from transport.legacy.unity_connection import async_send_command_with_retry
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def _strip_stacktrace_from_list(items: list) -> None:
|
|
15
|
+
"""Remove stacktrace fields from a list of log entries."""
|
|
16
|
+
for item in items:
|
|
17
|
+
if isinstance(item, dict) and "stacktrace" in item:
|
|
18
|
+
item.pop("stacktrace", None)
|
|
19
|
+
|
|
20
|
+
|
|
14
21
|
@mcp_for_unity_tool(
|
|
15
|
-
description="Gets messages from or clears the Unity Editor console. Note: For maximum client compatibility, pass count as a quoted string (e.g., '5')."
|
|
22
|
+
description="Gets messages from or clears the Unity Editor console. Defaults to 10 most recent entries. Use page_size/cursor for paging. Note: For maximum client compatibility, pass count as a quoted string (e.g., '5')."
|
|
16
23
|
)
|
|
17
24
|
async def read_console(
|
|
18
25
|
ctx: Context,
|
|
@@ -21,10 +28,12 @@ async def read_console(
|
|
|
21
28
|
types: Annotated[list[Literal['error', 'warning',
|
|
22
29
|
'log', 'all']], "Message types to get"] | None = None,
|
|
23
30
|
count: Annotated[int | str,
|
|
24
|
-
"Max messages to return (accepts int or string, e.g., 5 or '5')"] | None = None,
|
|
31
|
+
"Max messages to return in non-paging mode (accepts int or string, e.g., 5 or '5'). Ignored when paging with page_size/cursor."] | None = None,
|
|
25
32
|
filter_text: Annotated[str, "Text filter for messages"] | None = None,
|
|
26
33
|
since_timestamp: Annotated[str,
|
|
27
34
|
"Get messages after this timestamp (ISO 8601)"] | None = None,
|
|
35
|
+
page_size: Annotated[int | str, "Page size for paginated console reads. Defaults to 50 when omitted."] | None = None,
|
|
36
|
+
cursor: Annotated[int | str, "Opaque cursor for paging (0-based offset). Defaults to 0."] | None = None,
|
|
28
37
|
format: Annotated[Literal['plain', 'detailed',
|
|
29
38
|
'json'], "Output format"] | None = None,
|
|
30
39
|
include_stacktrace: Annotated[bool | str,
|
|
@@ -35,11 +44,13 @@ async def read_console(
|
|
|
35
44
|
unity_instance = get_unity_instance_from_context(ctx)
|
|
36
45
|
# Set defaults if values are None
|
|
37
46
|
action = action if action is not None else 'get'
|
|
38
|
-
types = types if types is not None else ['error', 'warning'
|
|
39
|
-
format = format if format is not None else '
|
|
47
|
+
types = types if types is not None else ['error', 'warning']
|
|
48
|
+
format = format if format is not None else 'plain'
|
|
40
49
|
# Coerce booleans defensively (strings like 'true'/'false')
|
|
41
50
|
|
|
42
|
-
include_stacktrace = coerce_bool(include_stacktrace, default=
|
|
51
|
+
include_stacktrace = coerce_bool(include_stacktrace, default=False)
|
|
52
|
+
coerced_page_size = coerce_int(page_size, default=None)
|
|
53
|
+
coerced_cursor = coerce_int(cursor, default=None)
|
|
43
54
|
|
|
44
55
|
# Normalize action if it's a string
|
|
45
56
|
if isinstance(action, str):
|
|
@@ -56,7 +67,7 @@ async def read_console(
|
|
|
56
67
|
count = coerce_int(count)
|
|
57
68
|
|
|
58
69
|
if action == "get" and count is None:
|
|
59
|
-
count =
|
|
70
|
+
count = 10
|
|
60
71
|
|
|
61
72
|
# Prepare parameters for the C# handler
|
|
62
73
|
params_dict = {
|
|
@@ -65,6 +76,8 @@ async def read_console(
|
|
|
65
76
|
"count": count,
|
|
66
77
|
"filterText": filter_text,
|
|
67
78
|
"sinceTimestamp": since_timestamp,
|
|
79
|
+
"pageSize": coerced_page_size,
|
|
80
|
+
"cursor": coerced_cursor,
|
|
68
81
|
"format": format.lower() if isinstance(format, str) else format,
|
|
69
82
|
"includeStacktrace": include_stacktrace
|
|
70
83
|
}
|
|
@@ -83,16 +96,13 @@ async def read_console(
|
|
|
83
96
|
# Strip stacktrace fields from returned lines if present
|
|
84
97
|
try:
|
|
85
98
|
data = resp.get("data")
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
# Handle legacy/direct list format if any
|
|
99
|
+
if isinstance(data, dict):
|
|
100
|
+
for key in ("lines", "items"):
|
|
101
|
+
if key in data and isinstance(data[key], list):
|
|
102
|
+
_strip_stacktrace_from_list(data[key])
|
|
103
|
+
break
|
|
92
104
|
elif isinstance(data, list):
|
|
93
|
-
|
|
94
|
-
if isinstance(line, dict) and "stacktrace" in line:
|
|
95
|
-
line.pop("stacktrace", None)
|
|
105
|
+
_strip_stacktrace_from_list(data)
|
|
96
106
|
except Exception:
|
|
97
107
|
pass
|
|
98
108
|
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
|
services/tools/refresh_unity.py
CHANGED
|
@@ -10,7 +10,7 @@ from models import MCPResponse
|
|
|
10
10
|
from services.registry import mcp_for_unity_tool
|
|
11
11
|
from services.tools import get_unity_instance_from_context
|
|
12
12
|
import transport.unity_transport as unity_transport
|
|
13
|
-
from transport.legacy.unity_connection import async_send_command_with_retry
|
|
13
|
+
from transport.legacy.unity_connection import async_send_command_with_retry, _extract_response_reason
|
|
14
14
|
from services.state.external_changes_scanner import external_changes_scanner
|
|
15
15
|
|
|
16
16
|
|
|
@@ -47,10 +47,12 @@ async def refresh_unity(
|
|
|
47
47
|
if isinstance(response, dict) and not response.get("success", True):
|
|
48
48
|
hint = response.get("hint")
|
|
49
49
|
err = (response.get("error") or response.get("message") or "")
|
|
50
|
+
reason = _extract_response_reason(response)
|
|
50
51
|
is_retryable = (hint == "retry") or ("disconnected" in str(err).lower())
|
|
51
52
|
if (not wait_for_ready) or (not is_retryable):
|
|
52
53
|
return MCPResponse(**response)
|
|
53
|
-
|
|
54
|
+
if reason not in {"reloading", "no_unity_session"}:
|
|
55
|
+
recovered_from_disconnect = True
|
|
54
56
|
|
|
55
57
|
# Optional server-side wait loop (defensive): if Unity tool doesn't wait or returns quickly,
|
|
56
58
|
# poll the canonical editor_state v2 resource until ready or timeout.
|
|
@@ -86,5 +88,3 @@ async def refresh_unity(
|
|
|
86
88
|
)
|
|
87
89
|
|
|
88
90
|
return MCPResponse(**response) if isinstance(response, dict) else response
|
|
89
|
-
|
|
90
|
-
|
|
@@ -686,28 +686,46 @@ def get_unity_connection(instance_identifier: str | None = None) -> UnityConnect
|
|
|
686
686
|
# Centralized retry helpers
|
|
687
687
|
# -----------------------------
|
|
688
688
|
|
|
689
|
-
def
|
|
690
|
-
"""
|
|
689
|
+
def _extract_response_reason(resp: object) -> str | None:
|
|
690
|
+
"""Extract a normalized (lowercase) reason string from a response.
|
|
691
691
|
|
|
692
|
-
|
|
693
|
-
by
|
|
692
|
+
Returns lowercase reason values to enable case-insensitive comparisons
|
|
693
|
+
by callers (e.g. _is_reloading_response, refresh_unity).
|
|
694
694
|
"""
|
|
695
|
-
# Structured MCPResponse from preflight/transport
|
|
696
695
|
if isinstance(resp, MCPResponse):
|
|
697
|
-
|
|
698
|
-
if
|
|
699
|
-
|
|
696
|
+
data = getattr(resp, "data", None)
|
|
697
|
+
if isinstance(data, dict):
|
|
698
|
+
reason = data.get("reason")
|
|
699
|
+
if isinstance(reason, str):
|
|
700
|
+
return reason.lower()
|
|
700
701
|
message_text = f"{resp.message or ''} {resp.error or ''}".lower()
|
|
701
|
-
|
|
702
|
+
if "reload" in message_text:
|
|
703
|
+
return "reloading"
|
|
704
|
+
return None
|
|
702
705
|
|
|
703
|
-
# Raw Unity payloads
|
|
704
706
|
if isinstance(resp, dict):
|
|
705
707
|
if resp.get("state") == "reloading":
|
|
706
|
-
return
|
|
708
|
+
return "reloading"
|
|
709
|
+
data = resp.get("data")
|
|
710
|
+
if isinstance(data, dict):
|
|
711
|
+
reason = data.get("reason")
|
|
712
|
+
if isinstance(reason, str):
|
|
713
|
+
return reason.lower()
|
|
707
714
|
message_text = (resp.get("message") or resp.get("error") or "").lower()
|
|
708
|
-
|
|
715
|
+
if "reload" in message_text:
|
|
716
|
+
return "reloading"
|
|
717
|
+
return None
|
|
709
718
|
|
|
710
|
-
return
|
|
719
|
+
return None
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _is_reloading_response(resp: object) -> bool:
|
|
723
|
+
"""Return True if the Unity response indicates the editor is reloading.
|
|
724
|
+
|
|
725
|
+
Supports both raw dict payloads from Unity and MCPResponse objects returned
|
|
726
|
+
by preflight checks or transport helpers.
|
|
727
|
+
"""
|
|
728
|
+
return _extract_response_reason(resp) == "reloading"
|
|
711
729
|
|
|
712
730
|
|
|
713
731
|
def send_command_with_retry(
|
|
@@ -738,15 +756,82 @@ def send_command_with_retry(
|
|
|
738
756
|
max_retries = getattr(config, "reload_max_retries", 40)
|
|
739
757
|
if retry_ms is None:
|
|
740
758
|
retry_ms = getattr(config, "reload_retry_ms", 250)
|
|
759
|
+
try:
|
|
760
|
+
max_wait_s = float(os.environ.get(
|
|
761
|
+
"UNITY_MCP_RELOAD_MAX_WAIT_S", "2.0"))
|
|
762
|
+
except ValueError as e:
|
|
763
|
+
raw_val = os.environ.get("UNITY_MCP_RELOAD_MAX_WAIT_S", "2.0")
|
|
764
|
+
logger.warning(
|
|
765
|
+
"Invalid UNITY_MCP_RELOAD_MAX_WAIT_S=%r, using default 2.0: %s",
|
|
766
|
+
raw_val, e)
|
|
767
|
+
max_wait_s = 2.0
|
|
768
|
+
# Clamp to [0, 30] to prevent misconfiguration from causing excessive waits
|
|
769
|
+
max_wait_s = max(0.0, min(max_wait_s, 30.0))
|
|
741
770
|
|
|
742
771
|
response = conn.send_command(command_type, params)
|
|
743
772
|
retries = 0
|
|
773
|
+
wait_started = None
|
|
774
|
+
reason = _extract_response_reason(response)
|
|
744
775
|
while _is_reloading_response(response) and retries < max_retries:
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
776
|
+
if wait_started is None:
|
|
777
|
+
wait_started = time.monotonic()
|
|
778
|
+
logger.debug(
|
|
779
|
+
"Unity reload wait started: command=%s instance=%s reason=%s max_wait_s=%.2f",
|
|
780
|
+
command_type,
|
|
781
|
+
instance_id or "default",
|
|
782
|
+
reason or "reloading",
|
|
783
|
+
max_wait_s,
|
|
784
|
+
)
|
|
785
|
+
if max_wait_s <= 0:
|
|
786
|
+
break
|
|
787
|
+
elapsed = time.monotonic() - wait_started
|
|
788
|
+
if elapsed >= max_wait_s:
|
|
789
|
+
break
|
|
790
|
+
delay_ms = retry_ms
|
|
791
|
+
if isinstance(response, dict):
|
|
792
|
+
retry_after = response.get("retry_after_ms")
|
|
793
|
+
if retry_after is None and isinstance(response.get("data"), dict):
|
|
794
|
+
retry_after = response["data"].get("retry_after_ms")
|
|
795
|
+
if retry_after is not None:
|
|
796
|
+
delay_ms = int(retry_after)
|
|
797
|
+
sleep_ms = max(50, min(int(delay_ms), 250))
|
|
798
|
+
logger.debug(
|
|
799
|
+
"Unity reload wait retry: command=%s instance=%s reason=%s retry_after_ms=%s sleep_ms=%s",
|
|
800
|
+
command_type,
|
|
801
|
+
instance_id or "default",
|
|
802
|
+
reason or "reloading",
|
|
803
|
+
delay_ms,
|
|
804
|
+
sleep_ms,
|
|
805
|
+
)
|
|
806
|
+
time.sleep(max(0.0, sleep_ms / 1000.0))
|
|
748
807
|
retries += 1
|
|
749
808
|
response = conn.send_command(command_type, params)
|
|
809
|
+
reason = _extract_response_reason(response)
|
|
810
|
+
|
|
811
|
+
if wait_started is not None:
|
|
812
|
+
waited = time.monotonic() - wait_started
|
|
813
|
+
if _is_reloading_response(response):
|
|
814
|
+
logger.debug(
|
|
815
|
+
"Unity reload wait exceeded budget: command=%s instance=%s waited_s=%.3f",
|
|
816
|
+
command_type,
|
|
817
|
+
instance_id or "default",
|
|
818
|
+
waited,
|
|
819
|
+
)
|
|
820
|
+
return MCPResponse(
|
|
821
|
+
success=False,
|
|
822
|
+
error="Unity is reloading; please retry",
|
|
823
|
+
hint="retry",
|
|
824
|
+
data={
|
|
825
|
+
"reason": "reloading",
|
|
826
|
+
"retry_after_ms": min(250, max(50, retry_ms)),
|
|
827
|
+
},
|
|
828
|
+
)
|
|
829
|
+
logger.debug(
|
|
830
|
+
"Unity reload wait completed: command=%s instance=%s waited_s=%.3f",
|
|
831
|
+
command_type,
|
|
832
|
+
instance_id or "default",
|
|
833
|
+
waited,
|
|
834
|
+
)
|
|
750
835
|
return response
|
|
751
836
|
|
|
752
837
|
|
transport/plugin_hub.py
CHANGED
|
@@ -34,6 +34,10 @@ class PluginDisconnectedError(RuntimeError):
|
|
|
34
34
|
"""Raised when a plugin WebSocket disconnects while commands are in flight."""
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
class NoUnitySessionError(RuntimeError):
|
|
38
|
+
"""Raised when no Unity plugins are available."""
|
|
39
|
+
|
|
40
|
+
|
|
37
41
|
class PluginHub(WebSocketEndpoint):
|
|
38
42
|
"""Manages persistent WebSocket connections to Unity plugins."""
|
|
39
43
|
|
|
@@ -361,11 +365,20 @@ class PluginHub(WebSocketEndpoint):
|
|
|
361
365
|
if cls._registry is None:
|
|
362
366
|
raise RuntimeError("Plugin registry not configured")
|
|
363
367
|
|
|
364
|
-
#
|
|
365
|
-
|
|
366
|
-
|
|
368
|
+
# Bound waiting for Unity sessions so calls fail fast when editors are not ready.
|
|
369
|
+
try:
|
|
370
|
+
max_wait_s = float(
|
|
371
|
+
os.environ.get("UNITY_MCP_SESSION_RESOLVE_MAX_WAIT_S", "2.0"))
|
|
372
|
+
except ValueError as e:
|
|
373
|
+
raw_val = os.environ.get("UNITY_MCP_SESSION_RESOLVE_MAX_WAIT_S", "2.0")
|
|
374
|
+
logger.warning(
|
|
375
|
+
"Invalid UNITY_MCP_SESSION_RESOLVE_MAX_WAIT_S=%r, using default 2.0: %s",
|
|
376
|
+
raw_val, e)
|
|
377
|
+
max_wait_s = 2.0
|
|
378
|
+
# Clamp to [0, 30] to prevent misconfiguration from causing excessive waits
|
|
379
|
+
max_wait_s = max(0.0, min(max_wait_s, 30.0))
|
|
367
380
|
retry_ms = float(getattr(config, "reload_retry_ms", 250))
|
|
368
|
-
sleep_seconds = max(0.05, retry_ms / 1000.0)
|
|
381
|
+
sleep_seconds = max(0.05, min(0.25, retry_ms / 1000.0))
|
|
369
382
|
|
|
370
383
|
# Allow callers to provide either just the hash or Name@hash
|
|
371
384
|
target_hash: str | None = None
|
|
@@ -394,7 +407,7 @@ class PluginHub(WebSocketEndpoint):
|
|
|
394
407
|
return None, count
|
|
395
408
|
|
|
396
409
|
session_id, session_count = await _try_once()
|
|
397
|
-
deadline = time.monotonic() +
|
|
410
|
+
deadline = time.monotonic() + max_wait_s
|
|
398
411
|
wait_started = None
|
|
399
412
|
|
|
400
413
|
# If there is no active plugin yet (e.g., Unity starting up or reloading),
|
|
@@ -408,14 +421,18 @@ class PluginHub(WebSocketEndpoint):
|
|
|
408
421
|
if wait_started is None:
|
|
409
422
|
wait_started = time.monotonic()
|
|
410
423
|
logger.debug(
|
|
411
|
-
|
|
424
|
+
"No plugin session available (instance=%s); waiting up to %.2fs",
|
|
425
|
+
unity_instance or "default",
|
|
426
|
+
max_wait_s,
|
|
412
427
|
)
|
|
413
428
|
await asyncio.sleep(sleep_seconds)
|
|
414
429
|
session_id, session_count = await _try_once()
|
|
415
430
|
|
|
416
431
|
if session_id is not None and wait_started is not None:
|
|
417
432
|
logger.debug(
|
|
418
|
-
|
|
433
|
+
"Plugin session restored after %.3fs (instance=%s)",
|
|
434
|
+
time.monotonic() - wait_started,
|
|
435
|
+
unity_instance or "default",
|
|
419
436
|
)
|
|
420
437
|
if session_id is None and not target_hash and session_count > 1:
|
|
421
438
|
raise RuntimeError(
|
|
@@ -425,11 +442,13 @@ class PluginHub(WebSocketEndpoint):
|
|
|
425
442
|
|
|
426
443
|
if session_id is None:
|
|
427
444
|
logger.warning(
|
|
428
|
-
|
|
445
|
+
"No Unity plugin reconnected within %.2fs (instance=%s)",
|
|
446
|
+
max_wait_s,
|
|
447
|
+
unity_instance or "default",
|
|
429
448
|
)
|
|
430
449
|
# At this point we've given the plugin ample time to reconnect; surface
|
|
431
450
|
# a clear error so the client can prompt the user to open Unity.
|
|
432
|
-
raise
|
|
451
|
+
raise NoUnitySessionError("No Unity plugins are currently connected")
|
|
433
452
|
|
|
434
453
|
return session_id
|
|
435
454
|
|
|
@@ -440,7 +459,20 @@ class PluginHub(WebSocketEndpoint):
|
|
|
440
459
|
command_type: str,
|
|
441
460
|
params: dict[str, Any],
|
|
442
461
|
) -> dict[str, Any]:
|
|
443
|
-
|
|
462
|
+
try:
|
|
463
|
+
session_id = await cls._resolve_session_id(unity_instance)
|
|
464
|
+
except NoUnitySessionError:
|
|
465
|
+
logger.debug(
|
|
466
|
+
"Unity session unavailable; returning retry: command=%s instance=%s",
|
|
467
|
+
command_type,
|
|
468
|
+
unity_instance or "default",
|
|
469
|
+
)
|
|
470
|
+
return MCPResponse(
|
|
471
|
+
success=False,
|
|
472
|
+
error="Unity session not available; please retry",
|
|
473
|
+
hint="retry",
|
|
474
|
+
data={"reason": "no_unity_session", "retry_after_ms": 250},
|
|
475
|
+
).model_dump()
|
|
444
476
|
|
|
445
477
|
# During domain reload / immediate reconnect windows, the plugin may be connected but not yet
|
|
446
478
|
# ready to process execute commands on the Unity main thread (which can be further delayed when
|
|
@@ -450,7 +482,11 @@ class PluginHub(WebSocketEndpoint):
|
|
|
450
482
|
if command_type in cls._FAST_FAIL_COMMANDS and command_type != "ping":
|
|
451
483
|
try:
|
|
452
484
|
max_wait_s = float(os.environ.get("UNITY_MCP_SESSION_READY_WAIT_SECONDS", "6"))
|
|
453
|
-
except
|
|
485
|
+
except ValueError as e:
|
|
486
|
+
raw_val = os.environ.get("UNITY_MCP_SESSION_READY_WAIT_SECONDS", "6")
|
|
487
|
+
logger.warning(
|
|
488
|
+
"Invalid UNITY_MCP_SESSION_READY_WAIT_SECONDS=%r, using default 6.0: %s",
|
|
489
|
+
raw_val, e)
|
|
454
490
|
max_wait_s = 6.0
|
|
455
491
|
max_wait_s = max(0.0, min(max_wait_s, 30.0))
|
|
456
492
|
if max_wait_s > 0:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|