mcpforunityserver 9.3.0b20260131003150__py3-none-any.whl → 9.3.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-9.3.0b20260131003150.dist-info → mcpforunityserver-9.3.1.dist-info}/METADATA +2 -2
- {mcpforunityserver-9.3.0b20260131003150.dist-info → mcpforunityserver-9.3.1.dist-info}/RECORD +11 -11
- transport/legacy/unity_connection.py +29 -7
- transport/models.py +5 -0
- transport/plugin_hub.py +114 -12
- transport/unity_instance_middleware.py +4 -3
- transport/unity_transport.py +1 -1
- {mcpforunityserver-9.3.0b20260131003150.dist-info → mcpforunityserver-9.3.1.dist-info}/WHEEL +0 -0
- {mcpforunityserver-9.3.0b20260131003150.dist-info → mcpforunityserver-9.3.1.dist-info}/entry_points.txt +0 -0
- {mcpforunityserver-9.3.0b20260131003150.dist-info → mcpforunityserver-9.3.1.dist-info}/licenses/LICENSE +0 -0
- {mcpforunityserver-9.3.0b20260131003150.dist-info → mcpforunityserver-9.3.1.dist-info}/top_level.txt +0 -0
{mcpforunityserver-9.3.0b20260131003150.dist-info → mcpforunityserver-9.3.1.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcpforunityserver
|
|
3
|
-
Version: 9.3.
|
|
3
|
+
Version: 9.3.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
|
|
@@ -110,7 +110,7 @@ Use this to run the latest released version from the repository. Change the vers
|
|
|
110
110
|
"command": "uvx",
|
|
111
111
|
"args": [
|
|
112
112
|
"--from",
|
|
113
|
-
"git+https://github.com/CoplayDev/unity-mcp@v9.
|
|
113
|
+
"git+https://github.com/CoplayDev/unity-mcp@v9.3.1#subdirectory=Server",
|
|
114
114
|
"mcp-for-unity",
|
|
115
115
|
"--transport",
|
|
116
116
|
"stdio"
|
{mcpforunityserver-9.3.0b20260131003150.dist-info → mcpforunityserver-9.3.1.dist-info}/RECORD
RENAMED
|
@@ -35,7 +35,7 @@ core/constants.py,sha256=PhZs926Nie7Muigju1xkVBynVaUktZlP9lJMHgu4_to,114
|
|
|
35
35
|
core/logging_decorator.py,sha256=D9CD7rFvQz-MBG-G4inizQj0Ivr6dfc9RBmTrw7q8mI,1383
|
|
36
36
|
core/telemetry.py,sha256=zIjmQKUNW0S822SSlkXyjjCIuX0ZpSTaZP4pAU0rCjw,20426
|
|
37
37
|
core/telemetry_decorator.py,sha256=ycSTrzVNCDQHSd-xmIWOpVfKFURPxpiZe_XkOQAGDAo,6705
|
|
38
|
-
mcpforunityserver-9.3.
|
|
38
|
+
mcpforunityserver-9.3.1.dist-info/licenses/LICENSE,sha256=bv5lDJZQEqxBgjjc1rkRbkEwpSIHF-8N-1Od0VnEJFw,1066
|
|
39
39
|
models/__init__.py,sha256=J2ozraI5aDkqLb53KvXjTDaWgh_xVFxpWrcRMekOwPk,231
|
|
40
40
|
models/models.py,sha256=heXuvdBtdats1SGwW8wKFFHM0qR4hA6A7qETn5s9BZ0,1827
|
|
41
41
|
models/unity_response.py,sha256=xvgJhJ3pS8-ATQhnU5owijgO4141UW59f5dQm1ohVew,2278
|
|
@@ -88,18 +88,18 @@ services/tools/script_apply_edits.py,sha256=0f-SaP5NUYGuivl4CWHjR8F-CXUpt3-5qkHp
|
|
|
88
88
|
services/tools/set_active_instance.py,sha256=j_d0GhFwhBh-HwoNKzrice1haNyWPdK-RtVrNasYH7c,4408
|
|
89
89
|
services/tools/utils.py,sha256=ETCiNnWdMZEtnJcDD-CtPsCJ7TBp5x5sPsYuhufkxac,13962
|
|
90
90
|
transport/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
91
|
-
transport/models.py,sha256=
|
|
92
|
-
transport/plugin_hub.py,sha256=
|
|
91
|
+
transport/models.py,sha256=1XFpxHGM9mzxcKU3LBGsejqReT8DiCEd2Ls3kEa_0t4,1426
|
|
92
|
+
transport/plugin_hub.py,sha256=wgTO1oJz-dM6KhZk_TbUIVdn7Vnmc3yU8PXwz_f7N9I,33647
|
|
93
93
|
transport/plugin_registry.py,sha256=Zn2QaDbpDj9Ad-oXs3q7CXxnZz3mc5SJ61GzIToEBPI,7160
|
|
94
|
-
transport/unity_instance_middleware.py,sha256=
|
|
95
|
-
transport/unity_transport.py,sha256=
|
|
94
|
+
transport/unity_instance_middleware.py,sha256=r_Q8GtMnHXLEWWACmG2R1wqZT6zoQ6jHFkAWGZxMHCg,11941
|
|
95
|
+
transport/unity_transport.py,sha256=Xj3phVcd4bhLMj2--Hq-sFpSvFzKhuMxH10IhzSfD14,3305
|
|
96
96
|
transport/legacy/port_discovery.py,sha256=JDSCqXLodfTT7fOsE0DFC1jJ3QsU6hVaYQb7x7FgdxY,12728
|
|
97
97
|
transport/legacy/stdio_port_registry.py,sha256=j4iARuP6wetppNDG8qKeuvo1bJKcSlgEhZvSyl_uf0A,2313
|
|
98
|
-
transport/legacy/unity_connection.py,sha256=
|
|
98
|
+
transport/legacy/unity_connection.py,sha256=jxPxrI0tQ9e82m9I3mzsIcY40kn-g9tGVt6P3YVz-v8,38309
|
|
99
99
|
utils/focus_nudge.py,sha256=0MCOms-SxUW7sN2hT3syy1epMdli2zc-6UHBICAfBSM,21330
|
|
100
100
|
utils/module_discovery.py,sha256=My48ofB1BUqxiBoAZAGbEaLQYdsrDhMm8MayBP_bUSQ,2005
|
|
101
|
-
mcpforunityserver-9.3.
|
|
102
|
-
mcpforunityserver-9.3.
|
|
103
|
-
mcpforunityserver-9.3.
|
|
104
|
-
mcpforunityserver-9.3.
|
|
105
|
-
mcpforunityserver-9.3.
|
|
101
|
+
mcpforunityserver-9.3.1.dist-info/METADATA,sha256=6A26-X5V92UT_WC1cEmhZUqZjzNP9p77b225b0_LaAI,11862
|
|
102
|
+
mcpforunityserver-9.3.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
103
|
+
mcpforunityserver-9.3.1.dist-info/entry_points.txt,sha256=pPm70RXQvkt3uBhPOtViDa47ZTA03RaQ6rwXvyi8oiI,70
|
|
104
|
+
mcpforunityserver-9.3.1.dist-info/top_level.txt,sha256=3-A65WsmBO6UZYH8O5mINdyhhZ63SDssr8LncRd1PSQ,46
|
|
105
|
+
mcpforunityserver-9.3.1.dist-info/RECORD,,
|
|
@@ -306,8 +306,10 @@ class UnityConnection:
|
|
|
306
306
|
for attempt in range(attempts + 1):
|
|
307
307
|
try:
|
|
308
308
|
# Ensure connected (handshake occurs within connect())
|
|
309
|
+
t_conn_start = time.time()
|
|
309
310
|
if not self.sock and not self.connect():
|
|
310
311
|
raise ConnectionError("Could not connect to Unity")
|
|
312
|
+
logger.info("[TIMING-STDIO] connect took %.3fs command=%s", time.time() - t_conn_start, command_type)
|
|
311
313
|
|
|
312
314
|
# Build payload
|
|
313
315
|
if command_type == 'ping':
|
|
@@ -324,12 +326,14 @@ class UnityConnection:
|
|
|
324
326
|
with contextlib.suppress(Exception):
|
|
325
327
|
logger.debug(
|
|
326
328
|
f"send {len(payload)} bytes; mode={mode}; head={payload[:32].decode('utf-8', 'ignore')}")
|
|
329
|
+
t_send_start = time.time()
|
|
327
330
|
if self.use_framing:
|
|
328
331
|
header = struct.pack('>Q', len(payload))
|
|
329
332
|
self.sock.sendall(header)
|
|
330
333
|
self.sock.sendall(payload)
|
|
331
334
|
else:
|
|
332
335
|
self.sock.sendall(payload)
|
|
336
|
+
logger.info("[TIMING-STDIO] sendall took %.3fs command=%s", time.time() - t_send_start, command_type)
|
|
333
337
|
|
|
334
338
|
# During retry bursts use a short receive timeout and ensure restoration
|
|
335
339
|
restore_timeout = None
|
|
@@ -337,7 +341,9 @@ class UnityConnection:
|
|
|
337
341
|
restore_timeout = self.sock.gettimeout()
|
|
338
342
|
self.sock.settimeout(1.0)
|
|
339
343
|
try:
|
|
344
|
+
t_recv_start = time.time()
|
|
340
345
|
response_data = self.receive_full_response(self.sock)
|
|
346
|
+
logger.info("[TIMING-STDIO] receive took %.3fs command=%s len=%d", time.time() - t_recv_start, command_type, len(response_data))
|
|
341
347
|
with contextlib.suppress(Exception):
|
|
342
348
|
logger.debug(
|
|
343
349
|
f"recv {len(response_data)} bytes; mode={mode}")
|
|
@@ -419,7 +425,8 @@ class UnityConnection:
|
|
|
419
425
|
|
|
420
426
|
# Cap backoff depending on state
|
|
421
427
|
if status and status.get('reloading'):
|
|
422
|
-
|
|
428
|
+
# Domain reload can take 10-20s; use longer waits
|
|
429
|
+
cap = 5.0
|
|
423
430
|
elif fast_error:
|
|
424
431
|
cap = 0.25
|
|
425
432
|
else:
|
|
@@ -761,22 +768,36 @@ def send_command_with_retry(
|
|
|
761
768
|
Uses config.reload_retry_ms and config.reload_max_retries by default. Preserves the
|
|
762
769
|
structured failure if retries are exhausted.
|
|
763
770
|
"""
|
|
771
|
+
t_retry_start = time.time()
|
|
772
|
+
logger.info("[TIMING-STDIO] send_command_with_retry START command=%s", command_type)
|
|
773
|
+
t_get_conn = time.time()
|
|
764
774
|
conn = get_unity_connection(instance_id)
|
|
775
|
+
logger.info("[TIMING-STDIO] get_unity_connection took %.3fs command=%s", time.time() - t_get_conn, command_type)
|
|
765
776
|
if max_retries is None:
|
|
766
777
|
max_retries = getattr(config, "reload_max_retries", 40)
|
|
767
778
|
if retry_ms is None:
|
|
768
779
|
retry_ms = getattr(config, "reload_retry_ms", 250)
|
|
780
|
+
# Default to 20s to handle domain reloads (which can take 10-20s after tests or script changes).
|
|
781
|
+
#
|
|
782
|
+
# NOTE: This wait can impact agentic workflows where domain reloads happen
|
|
783
|
+
# frequently (e.g., after test runs, script compilation). The 20s default
|
|
784
|
+
# balances handling slow reloads vs. avoiding unnecessary delays.
|
|
785
|
+
#
|
|
786
|
+
# TODO: Make this more deterministic by detecting Unity's actual reload state
|
|
787
|
+
# rather than blindly waiting up to 20s. See Issue #657.
|
|
788
|
+
#
|
|
789
|
+
# Configurable via: UNITY_MCP_RELOAD_MAX_WAIT_S (default: 20.0, max: 20.0)
|
|
769
790
|
try:
|
|
770
791
|
max_wait_s = float(os.environ.get(
|
|
771
|
-
"UNITY_MCP_RELOAD_MAX_WAIT_S", "
|
|
792
|
+
"UNITY_MCP_RELOAD_MAX_WAIT_S", "20.0"))
|
|
772
793
|
except ValueError as e:
|
|
773
|
-
raw_val = os.environ.get("UNITY_MCP_RELOAD_MAX_WAIT_S", "
|
|
794
|
+
raw_val = os.environ.get("UNITY_MCP_RELOAD_MAX_WAIT_S", "20.0")
|
|
774
795
|
logger.warning(
|
|
775
|
-
"Invalid UNITY_MCP_RELOAD_MAX_WAIT_S=%r, using default
|
|
796
|
+
"Invalid UNITY_MCP_RELOAD_MAX_WAIT_S=%r, using default 20.0: %s",
|
|
776
797
|
raw_val, e)
|
|
777
|
-
max_wait_s =
|
|
778
|
-
# Clamp to [0,
|
|
779
|
-
max_wait_s = max(0.0, min(max_wait_s,
|
|
798
|
+
max_wait_s = 20.0
|
|
799
|
+
# Clamp to [0, 20] to prevent misconfiguration from causing excessive waits
|
|
800
|
+
max_wait_s = max(0.0, min(max_wait_s, 20.0))
|
|
780
801
|
|
|
781
802
|
# If retry_on_reload=False, disable connection-level retries too (issue #577)
|
|
782
803
|
# Commands that trigger compilation/reload shouldn't retry on disconnect
|
|
@@ -847,6 +868,7 @@ def send_command_with_retry(
|
|
|
847
868
|
instance_id or "default",
|
|
848
869
|
waited,
|
|
849
870
|
)
|
|
871
|
+
logger.info("[TIMING-STDIO] send_command_with_retry DONE total=%.3fs command=%s", time.time() - t_retry_start, command_type)
|
|
850
872
|
return response
|
|
851
873
|
|
|
852
874
|
|
transport/models.py
CHANGED
transport/plugin_hub.py
CHANGED
|
@@ -7,7 +7,7 @@ import logging
|
|
|
7
7
|
import os
|
|
8
8
|
import time
|
|
9
9
|
import uuid
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any, ClassVar
|
|
11
11
|
|
|
12
12
|
from starlette.endpoints import WebSocketEndpoint
|
|
13
13
|
from starlette.websockets import WebSocket
|
|
@@ -21,6 +21,7 @@ from transport.models import (
|
|
|
21
21
|
WelcomeMessage,
|
|
22
22
|
RegisteredMessage,
|
|
23
23
|
ExecuteCommandMessage,
|
|
24
|
+
PingMessage,
|
|
24
25
|
RegisterMessage,
|
|
25
26
|
RegisterToolsMessage,
|
|
26
27
|
PongMessage,
|
|
@@ -29,7 +30,7 @@ from transport.models import (
|
|
|
29
30
|
SessionDetails,
|
|
30
31
|
)
|
|
31
32
|
|
|
32
|
-
logger = logging.getLogger(
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
class PluginDisconnectedError(RuntimeError):
|
|
@@ -63,6 +64,10 @@ class PluginHub(WebSocketEndpoint):
|
|
|
63
64
|
KEEP_ALIVE_INTERVAL = 15
|
|
64
65
|
SERVER_TIMEOUT = 30
|
|
65
66
|
COMMAND_TIMEOUT = 30
|
|
67
|
+
# Server-side ping interval (seconds) - how often to send pings to Unity
|
|
68
|
+
PING_INTERVAL = 10
|
|
69
|
+
# Max time (seconds) to wait for pong before considering connection dead
|
|
70
|
+
PING_TIMEOUT = 20
|
|
66
71
|
# Timeout (seconds) for fast-fail commands like ping/read_console/get_editor_state.
|
|
67
72
|
# Keep short so MCP clients aren't blocked during Unity compilation/reload/unfocused throttling.
|
|
68
73
|
FAST_FAIL_TIMEOUT = 2.0
|
|
@@ -78,6 +83,10 @@ class PluginHub(WebSocketEndpoint):
|
|
|
78
83
|
_pending: dict[str, dict[str, Any]] = {}
|
|
79
84
|
_lock: asyncio.Lock | None = None
|
|
80
85
|
_loop: asyncio.AbstractEventLoop | None = None
|
|
86
|
+
# session_id -> last pong timestamp (monotonic)
|
|
87
|
+
_last_pong: ClassVar[dict[str, float]] = {}
|
|
88
|
+
# session_id -> ping task
|
|
89
|
+
_ping_tasks: ClassVar[dict[str, asyncio.Task]] = {}
|
|
81
90
|
|
|
82
91
|
@classmethod
|
|
83
92
|
def configure(
|
|
@@ -176,12 +185,20 @@ class PluginHub(WebSocketEndpoint):
|
|
|
176
185
|
(sid for sid, ws in cls._connections.items() if ws is websocket), None)
|
|
177
186
|
if session_id:
|
|
178
187
|
cls._connections.pop(session_id, None)
|
|
188
|
+
# Stop the ping loop for this session
|
|
189
|
+
ping_task = cls._ping_tasks.pop(session_id, None)
|
|
190
|
+
if ping_task and not ping_task.done():
|
|
191
|
+
ping_task.cancel()
|
|
192
|
+
# Clean up last pong tracking
|
|
193
|
+
cls._last_pong.pop(session_id, None)
|
|
179
194
|
# Fail-fast any in-flight commands for this session to avoid waiting for COMMAND_TIMEOUT.
|
|
180
195
|
pending_ids = [
|
|
181
196
|
command_id
|
|
182
197
|
for command_id, entry in cls._pending.items()
|
|
183
198
|
if entry.get("session_id") == session_id
|
|
184
199
|
]
|
|
200
|
+
if pending_ids:
|
|
201
|
+
logger.debug(f"Cancelling {len(pending_ids)} pending commands for disconnected session")
|
|
185
202
|
for command_id in pending_ids:
|
|
186
203
|
entry = cls._pending.get(command_id)
|
|
187
204
|
future = entry.get("future") if isinstance(
|
|
@@ -364,10 +381,18 @@ class PluginHub(WebSocketEndpoint):
|
|
|
364
381
|
session = await registry.register(session_id, project_name, project_hash, unity_version, project_path, user_id=user_id)
|
|
365
382
|
async with lock:
|
|
366
383
|
cls._connections[session.session_id] = websocket
|
|
384
|
+
# Initialize last pong time and start ping loop for this session
|
|
385
|
+
cls._last_pong[session_id] = time.monotonic()
|
|
386
|
+
# Cancel any existing ping task for this session (shouldn't happen, but be safe)
|
|
387
|
+
old_task = cls._ping_tasks.pop(session_id, None)
|
|
388
|
+
if old_task and not old_task.done():
|
|
389
|
+
old_task.cancel()
|
|
390
|
+
# Start the server-side ping loop
|
|
391
|
+
ping_task = asyncio.create_task(cls._ping_loop(session_id, websocket))
|
|
392
|
+
cls._ping_tasks[session_id] = ping_task
|
|
367
393
|
|
|
368
394
|
if user_id:
|
|
369
|
-
logger.info(
|
|
370
|
-
f"Plugin registered: {project_name} ({project_hash}) for user {user_id}")
|
|
395
|
+
logger.info(f"Plugin registered: {project_name} ({project_hash}) for user {user_id}")
|
|
371
396
|
else:
|
|
372
397
|
logger.info(f"Plugin registered: {project_name} ({project_hash})")
|
|
373
398
|
|
|
@@ -429,11 +454,77 @@ class PluginHub(WebSocketEndpoint):
|
|
|
429
454
|
async def _handle_pong(self, payload: PongMessage) -> None:
|
|
430
455
|
cls = type(self)
|
|
431
456
|
registry = cls._registry
|
|
457
|
+
lock = cls._lock
|
|
432
458
|
if registry is None:
|
|
433
459
|
return
|
|
434
460
|
session_id = payload.session_id
|
|
435
461
|
if session_id:
|
|
436
462
|
await registry.touch(session_id)
|
|
463
|
+
# Record last pong time for staleness detection (under lock for consistency)
|
|
464
|
+
if lock is not None:
|
|
465
|
+
async with lock:
|
|
466
|
+
cls._last_pong[session_id] = time.monotonic()
|
|
467
|
+
|
|
468
|
+
@classmethod
|
|
469
|
+
async def _ping_loop(cls, session_id: str, websocket: WebSocket) -> None:
|
|
470
|
+
"""Server-initiated ping loop to detect dead connections.
|
|
471
|
+
|
|
472
|
+
Sends periodic pings to the Unity client. If no pong is received within
|
|
473
|
+
PING_TIMEOUT seconds, the connection is considered dead and closed.
|
|
474
|
+
This helps detect connections that die silently (e.g., Windows OSError 64).
|
|
475
|
+
"""
|
|
476
|
+
logger.debug(f"[Ping] Starting ping loop for session {session_id}")
|
|
477
|
+
try:
|
|
478
|
+
while True:
|
|
479
|
+
await asyncio.sleep(cls.PING_INTERVAL)
|
|
480
|
+
|
|
481
|
+
# Check if we're still supposed to be running and get last pong time (under lock)
|
|
482
|
+
lock = cls._lock
|
|
483
|
+
if lock is None:
|
|
484
|
+
break
|
|
485
|
+
async with lock:
|
|
486
|
+
if session_id not in cls._connections:
|
|
487
|
+
logger.debug(f"[Ping] Session {session_id} no longer in connections, stopping ping loop")
|
|
488
|
+
break
|
|
489
|
+
# Read last pong time under lock for consistency
|
|
490
|
+
last_pong = cls._last_pong.get(session_id, 0)
|
|
491
|
+
|
|
492
|
+
# Check staleness: has it been too long since we got a pong?
|
|
493
|
+
elapsed = time.monotonic() - last_pong
|
|
494
|
+
if elapsed > cls.PING_TIMEOUT:
|
|
495
|
+
logger.warning(
|
|
496
|
+
f"[Ping] Session {session_id} stale: no pong for {elapsed:.1f}s "
|
|
497
|
+
f"(timeout={cls.PING_TIMEOUT}s). Closing connection."
|
|
498
|
+
)
|
|
499
|
+
try:
|
|
500
|
+
await websocket.close(code=1001) # Going away
|
|
501
|
+
except Exception as close_ex:
|
|
502
|
+
logger.debug(f"[Ping] Error closing stale websocket: {close_ex}")
|
|
503
|
+
break
|
|
504
|
+
|
|
505
|
+
# Send a ping to the client
|
|
506
|
+
try:
|
|
507
|
+
ping_msg = PingMessage()
|
|
508
|
+
await websocket.send_json(ping_msg.model_dump())
|
|
509
|
+
logger.debug(f"[Ping] Sent ping to session {session_id}")
|
|
510
|
+
except Exception as send_ex:
|
|
511
|
+
# Send failed - connection is dead
|
|
512
|
+
logger.warning(
|
|
513
|
+
f"[Ping] Failed to send ping to session {session_id}: {send_ex}. "
|
|
514
|
+
"Connection likely dead."
|
|
515
|
+
)
|
|
516
|
+
try:
|
|
517
|
+
await websocket.close(code=1006) # Abnormal closure
|
|
518
|
+
except Exception:
|
|
519
|
+
pass
|
|
520
|
+
break
|
|
521
|
+
|
|
522
|
+
except asyncio.CancelledError:
|
|
523
|
+
logger.debug(f"[Ping] Ping loop cancelled for session {session_id}")
|
|
524
|
+
except Exception as ex:
|
|
525
|
+
logger.warning(f"[Ping] Ping loop error for session {session_id}: {ex}")
|
|
526
|
+
finally:
|
|
527
|
+
logger.debug(f"[Ping] Ping loop ended for session {session_id}")
|
|
437
528
|
|
|
438
529
|
@classmethod
|
|
439
530
|
async def _get_connection(cls, session_id: str) -> WebSocket:
|
|
@@ -465,19 +556,30 @@ class PluginHub(WebSocketEndpoint):
|
|
|
465
556
|
if cls._registry is None:
|
|
466
557
|
raise RuntimeError("Plugin registry not configured")
|
|
467
558
|
|
|
468
|
-
# Bound waiting for Unity sessions
|
|
559
|
+
# Bound waiting for Unity sessions. Default to 20s to handle domain reloads
|
|
560
|
+
# (which can take 10-20s after test runs or script changes).
|
|
561
|
+
#
|
|
562
|
+
# NOTE: This wait can impact agentic workflows where domain reloads happen
|
|
563
|
+
# frequently (e.g., after test runs, script compilation). The 20s default
|
|
564
|
+
# balances handling slow reloads vs. avoiding unnecessary delays.
|
|
565
|
+
#
|
|
566
|
+
# TODO: Make this more deterministic by detecting Unity's actual reload state
|
|
567
|
+
# (e.g., via status file, heartbeat, or explicit "reloading" signal from Unity)
|
|
568
|
+
# rather than blindly waiting up to 20s. See Issue #657.
|
|
569
|
+
#
|
|
570
|
+
# Configurable via: UNITY_MCP_SESSION_RESOLVE_MAX_WAIT_S (default: 20.0, max: 20.0)
|
|
469
571
|
try:
|
|
470
572
|
max_wait_s = float(
|
|
471
|
-
os.environ.get("UNITY_MCP_SESSION_RESOLVE_MAX_WAIT_S", "
|
|
573
|
+
os.environ.get("UNITY_MCP_SESSION_RESOLVE_MAX_WAIT_S", "20.0"))
|
|
472
574
|
except ValueError as e:
|
|
473
575
|
raw_val = os.environ.get(
|
|
474
|
-
"UNITY_MCP_SESSION_RESOLVE_MAX_WAIT_S", "
|
|
576
|
+
"UNITY_MCP_SESSION_RESOLVE_MAX_WAIT_S", "20.0")
|
|
475
577
|
logger.warning(
|
|
476
|
-
"Invalid UNITY_MCP_SESSION_RESOLVE_MAX_WAIT_S=%r, using default
|
|
578
|
+
"Invalid UNITY_MCP_SESSION_RESOLVE_MAX_WAIT_S=%r, using default 20.0: %s",
|
|
477
579
|
raw_val, e)
|
|
478
|
-
max_wait_s =
|
|
479
|
-
# Clamp to [0,
|
|
480
|
-
max_wait_s = max(0.0, min(max_wait_s,
|
|
580
|
+
max_wait_s = 20.0
|
|
581
|
+
# Clamp to [0, 20] to prevent misconfiguration from causing excessive waits
|
|
582
|
+
max_wait_s = max(0.0, min(max_wait_s, 20.0))
|
|
481
583
|
retry_ms = float(getattr(config, "reload_retry_ms", 250))
|
|
482
584
|
sleep_seconds = max(0.05, min(0.25, retry_ms / 1000.0))
|
|
483
585
|
|
|
@@ -613,7 +715,7 @@ class PluginHub(WebSocketEndpoint):
|
|
|
613
715
|
"Invalid UNITY_MCP_SESSION_READY_WAIT_SECONDS=%r, using default 6.0: %s",
|
|
614
716
|
raw_val, e)
|
|
615
717
|
max_wait_s = 6.0
|
|
616
|
-
max_wait_s = max(0.0, min(max_wait_s,
|
|
718
|
+
max_wait_s = max(0.0, min(max_wait_s, 20.0))
|
|
617
719
|
if max_wait_s > 0:
|
|
618
720
|
deadline = time.monotonic() + max_wait_s
|
|
619
721
|
while time.monotonic() < deadline:
|
|
@@ -214,9 +214,10 @@ class UnityInstanceMiddleware(Middleware):
|
|
|
214
214
|
# The 'active_instance' (Name@hash) might be valid for stdio even if PluginHub fails.
|
|
215
215
|
|
|
216
216
|
session_id: str | None = None
|
|
217
|
-
# Only validate via PluginHub if we are actually using HTTP transport
|
|
218
|
-
#
|
|
219
|
-
|
|
217
|
+
# Only validate via PluginHub if we are actually using HTTP transport.
|
|
218
|
+
# For stdio transport, skip PluginHub entirely - we only need the instance ID.
|
|
219
|
+
from transport.unity_transport import _is_http_transport
|
|
220
|
+
if _is_http_transport() and PluginHub.is_configured():
|
|
220
221
|
try:
|
|
221
222
|
# resolving session_id might fail if the plugin disconnected
|
|
222
223
|
# We only need session_id for HTTP transport routing.
|
transport/unity_transport.py
CHANGED
|
@@ -11,8 +11,8 @@ from services.api_key_service import ApiKeyService
|
|
|
11
11
|
from models.models import MCPResponse
|
|
12
12
|
from models.unity_response import normalize_unity_response
|
|
13
13
|
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
14
15
|
T = TypeVar("T")
|
|
15
|
-
logger = logging.getLogger("mcp-for-unity-server")
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def _is_http_transport() -> bool:
|
{mcpforunityserver-9.3.0b20260131003150.dist-info → mcpforunityserver-9.3.1.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcpforunityserver-9.3.0b20260131003150.dist-info → mcpforunityserver-9.3.1.dist-info}/top_level.txt
RENAMED
|
File without changes
|