mcpforunityserver 8.7.0__py3-none-any.whl → 9.0.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.
Files changed (53) hide show
  1. main.py +4 -3
  2. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.0.0.dist-info}/METADATA +2 -2
  3. mcpforunityserver-9.0.0.dist-info/RECORD +72 -0
  4. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.0.0.dist-info}/top_level.txt +0 -1
  5. services/custom_tool_service.py +13 -8
  6. services/resources/active_tool.py +1 -1
  7. services/resources/custom_tools.py +2 -2
  8. services/resources/editor_state.py +283 -30
  9. services/resources/gameobject.py +243 -0
  10. services/resources/layers.py +1 -1
  11. services/resources/prefab_stage.py +1 -1
  12. services/resources/project_info.py +1 -1
  13. services/resources/selection.py +1 -1
  14. services/resources/tags.py +1 -1
  15. services/resources/unity_instances.py +1 -1
  16. services/resources/windows.py +1 -1
  17. services/state/external_changes_scanner.py +3 -4
  18. services/tools/batch_execute.py +24 -9
  19. services/tools/debug_request_context.py +8 -2
  20. services/tools/execute_custom_tool.py +6 -1
  21. services/tools/execute_menu_item.py +6 -3
  22. services/tools/find_gameobjects.py +89 -0
  23. services/tools/find_in_file.py +26 -19
  24. services/tools/manage_asset.py +13 -44
  25. services/tools/manage_components.py +131 -0
  26. services/tools/manage_editor.py +9 -8
  27. services/tools/manage_gameobject.py +115 -79
  28. services/tools/manage_material.py +80 -31
  29. services/tools/manage_prefabs.py +7 -1
  30. services/tools/manage_scene.py +30 -13
  31. services/tools/manage_script.py +62 -19
  32. services/tools/manage_scriptable_object.py +22 -10
  33. services/tools/manage_shader.py +8 -1
  34. services/tools/manage_vfx.py +738 -0
  35. services/tools/preflight.py +15 -12
  36. services/tools/read_console.py +31 -14
  37. services/tools/refresh_unity.py +28 -18
  38. services/tools/run_tests.py +162 -53
  39. services/tools/script_apply_edits.py +15 -7
  40. services/tools/set_active_instance.py +12 -7
  41. services/tools/utils.py +60 -6
  42. transport/legacy/port_discovery.py +2 -2
  43. transport/legacy/unity_connection.py +102 -17
  44. transport/plugin_hub.py +68 -24
  45. transport/unity_instance_middleware.py +4 -3
  46. transport/unity_transport.py +2 -1
  47. mcpforunityserver-8.7.0.dist-info/RECORD +0 -71
  48. routes/__init__.py +0 -0
  49. services/resources/editor_state_v2.py +0 -270
  50. services/tools/test_jobs.py +0 -94
  51. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.0.0.dist-info}/WHEEL +0 -0
  52. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.0.0.dist-info}/entry_points.txt +0 -0
  53. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.0.0.dist-info}/licenses/LICENSE +0 -0
services/tools/utils.py CHANGED
@@ -8,6 +8,7 @@ from typing import Any
8
8
  _TRUTHY = {"true", "1", "yes", "on"}
9
9
  _FALSY = {"false", "0", "no", "off"}
10
10
 
11
+
11
12
  def coerce_bool(value: Any, default: bool | None = None) -> bool | None:
12
13
  """Attempt to coerce a loosely-typed value to a boolean."""
13
14
  if value is None:
@@ -27,26 +28,26 @@ def coerce_bool(value: Any, default: bool | None = None) -> bool | None:
27
28
  def parse_json_payload(value: Any) -> Any:
28
29
  """
29
30
  Attempt to parse a value that might be a JSON string into its native object.
30
-
31
+
31
32
  This is a tolerant parser used to handle cases where MCP clients or LLMs
32
33
  serialize complex objects (lists, dicts) into strings. It also handles
33
34
  scalar values like numbers, booleans, and null.
34
-
35
+
35
36
  Args:
36
37
  value: The input value (can be str, list, dict, etc.)
37
-
38
+
38
39
  Returns:
39
40
  The parsed JSON object/list if the input was a valid JSON string,
40
41
  or the original value if parsing failed or wasn't necessary.
41
42
  """
42
43
  if not isinstance(value, str):
43
44
  return value
44
-
45
+
45
46
  val_trimmed = value.strip()
46
-
47
+
47
48
  # Fast path: if it doesn't look like JSON structure, return as is
48
49
  if not (
49
- (val_trimmed.startswith("{") and val_trimmed.endswith("}")) or
50
+ (val_trimmed.startswith("{") and val_trimmed.endswith("}")) or
50
51
  (val_trimmed.startswith("[") and val_trimmed.endswith("]")) or
51
52
  val_trimmed in ("true", "false", "null") or
52
53
  (val_trimmed.replace(".", "", 1).replace("-", "", 1).isdigit())
@@ -75,3 +76,56 @@ def coerce_int(value: Any, default: int | None = None) -> int | None:
75
76
  return int(float(s))
76
77
  except Exception:
77
78
  return default
79
+
80
+
81
+ def coerce_float(value: Any, default: float | None = None) -> float | None:
82
+ """Attempt to coerce a loosely-typed value to a float-like number."""
83
+ if value is None:
84
+ return default
85
+ try:
86
+ # Treat booleans as invalid numeric input instead of coercing to 0/1.
87
+ if isinstance(value, bool):
88
+ return default
89
+ if isinstance(value, (int, float)):
90
+ return float(value)
91
+ s = str(value).strip()
92
+ if s.lower() in ("", "none", "null"):
93
+ return default
94
+ return float(s)
95
+ except (TypeError, ValueError):
96
+ return default
97
+
98
+
99
+ def normalize_properties(value: Any) -> tuple[dict[str, Any] | None, str | None]:
100
+ """
101
+ Robustly normalize a properties parameter to a dict.
102
+
103
+ Handles various input formats from MCP clients/LLMs:
104
+ - None -> (None, None)
105
+ - dict -> (dict, None)
106
+ - JSON string -> (parsed_dict, None) or (None, error_message)
107
+ - Invalid values -> (None, error_message)
108
+
109
+ Returns:
110
+ Tuple of (parsed_dict, error_message). If error_message is set, parsed_dict is None.
111
+ """
112
+ if value is None:
113
+ return None, None
114
+
115
+ # Already a dict - return as-is
116
+ if isinstance(value, dict):
117
+ return value, None
118
+
119
+ # Try parsing as string
120
+ if isinstance(value, str):
121
+ # Check for obviously invalid values from serialization bugs
122
+ if value in ("[object Object]", "undefined", "null", ""):
123
+ return None, f"properties received invalid value: '{value}'. Expected a JSON object like {{\"key\": value}}"
124
+
125
+ parsed = parse_json_payload(value)
126
+ if isinstance(parsed, dict):
127
+ return parsed, None
128
+
129
+ return None, f"properties must be a JSON object (dict), got string that parsed to {type(parsed).__name__}"
130
+
131
+ return None, f"properties must be a dict or JSON string, got {type(value).__name__}"
@@ -279,9 +279,9 @@ class PortDiscovery:
279
279
  if freshness.tzinfo:
280
280
  from datetime import timezone
281
281
  now = datetime.now(timezone.utc)
282
-
282
+
283
283
  age_s = (now - freshness).total_seconds()
284
-
284
+
285
285
  if is_reloading and age_s < 60:
286
286
  pass # keep it, status="reloading"
287
287
  else:
@@ -584,7 +584,7 @@ class UnityConnectionPool:
584
584
  raise ConnectionError(
585
585
  f"Unity instance '{identifier}' not found. "
586
586
  f"Available instances: {available_ids}. "
587
- f"Check unity://instances resource for all instances."
587
+ f"Check mcpforunity://instances resource for all instances."
588
588
  )
589
589
 
590
590
  def get_connection(self, instance_identifier: str | None = None) -> UnityConnection:
@@ -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 _is_reloading_response(resp: object) -> bool:
690
- """Return True if the Unity response indicates the editor is reloading.
689
+ def _extract_response_reason(resp: object) -> str | None:
690
+ """Extract a normalized (lowercase) reason string from a response.
691
691
 
692
- Supports both raw dict payloads from Unity and MCPResponse objects returned
693
- by preflight checks or transport helpers.
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
- # Explicit "please retry" hint from preflight
698
- if getattr(resp, "hint", None) == "retry":
699
- return True
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
- return "reload" in message_text
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 True
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
- return "reload" in message_text
715
+ if "reload" in message_text:
716
+ return "reloading"
717
+ return None
709
718
 
710
- return False
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
- delay_ms = int(response.get("retry_after_ms", retry_ms)
746
- ) if isinstance(response, dict) else retry_ms
747
- time.sleep(max(0.0, delay_ms / 1000.0))
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
 
@@ -41,10 +45,14 @@ class PluginHub(WebSocketEndpoint):
41
45
  KEEP_ALIVE_INTERVAL = 15
42
46
  SERVER_TIMEOUT = 30
43
47
  COMMAND_TIMEOUT = 30
48
+ # Timeout (seconds) for fast-fail commands like ping/read_console/get_editor_state.
49
+ # Keep short so MCP clients aren't blocked during Unity compilation/reload/unfocused throttling.
50
+ FAST_FAIL_TIMEOUT = 2.0
44
51
  # Fast-path commands should never block the client for long; return a retry hint instead.
45
52
  # This helps avoid the Cursor-side ~30s tool-call timeout when Unity is compiling/reloading
46
53
  # or is throttled while unfocused.
47
- _FAST_FAIL_COMMANDS: set[str] = {"read_console", "get_editor_state", "ping"}
54
+ _FAST_FAIL_COMMANDS: set[str] = {
55
+ "read_console", "get_editor_state", "ping"}
48
56
 
49
57
  _registry: PluginRegistry | None = None
50
58
  _connections: dict[str, WebSocket] = {}
@@ -114,7 +122,8 @@ class PluginHub(WebSocketEndpoint):
114
122
  ]
115
123
  for command_id in pending_ids:
116
124
  entry = cls._pending.get(command_id)
117
- future = entry.get("future") if isinstance(entry, dict) else None
125
+ future = entry.get("future") if isinstance(
126
+ entry, dict) else None
118
127
  if future and not future.done():
119
128
  future.set_exception(
120
129
  PluginDisconnectedError(
@@ -136,18 +145,15 @@ class PluginHub(WebSocketEndpoint):
136
145
  future: asyncio.Future = asyncio.get_running_loop().create_future()
137
146
  # Compute a per-command timeout:
138
147
  # - fast-path commands: short timeout (encourage retry)
139
- # - long-running commands (e.g., run_tests): allow caller to request a longer timeout via params
148
+ # - long-running commands: allow caller to request a longer timeout via params
140
149
  unity_timeout_s = float(cls.COMMAND_TIMEOUT)
141
150
  server_wait_s = float(cls.COMMAND_TIMEOUT)
142
151
  if command_type in cls._FAST_FAIL_COMMANDS:
143
- try:
144
- fast_timeout = float(os.environ.get("UNITY_MCP_FAST_COMMAND_TIMEOUT", "3"))
145
- except Exception:
146
- fast_timeout = 3.0
152
+ fast_timeout = float(cls.FAST_FAIL_TIMEOUT)
147
153
  unity_timeout_s = fast_timeout
148
154
  server_wait_s = fast_timeout
149
155
  else:
150
- # Common tools pass a requested timeout in seconds (e.g., run_tests(timeout_seconds=900)).
156
+ # Common tools pass a requested timeout in seconds (e.g., timeout_seconds=900).
151
157
  requested = None
152
158
  try:
153
159
  if isinstance(params, dict):
@@ -176,7 +182,8 @@ class PluginHub(WebSocketEndpoint):
176
182
  if command_id in cls._pending:
177
183
  raise RuntimeError(
178
184
  f"Duplicate command id generated: {command_id}")
179
- cls._pending[command_id] = {"future": future, "session_id": session_id}
185
+ cls._pending[command_id] = {
186
+ "future": future, "session_id": session_id}
180
187
 
181
188
  try:
182
189
  msg = ExecuteCommandMessage(
@@ -361,11 +368,21 @@ class PluginHub(WebSocketEndpoint):
361
368
  if cls._registry is None:
362
369
  raise RuntimeError("Plugin registry not configured")
363
370
 
364
- # Use the same defaults as the stdio transport reload handling so that
365
- # HTTP/WebSocket and TCP behave consistently without per-project env.
366
- max_retries = max(1, int(getattr(config, "reload_max_retries", 40)))
371
+ # Bound waiting for Unity sessions so calls fail fast when editors are not ready.
372
+ try:
373
+ max_wait_s = float(
374
+ os.environ.get("UNITY_MCP_SESSION_RESOLVE_MAX_WAIT_S", "2.0"))
375
+ except ValueError as e:
376
+ raw_val = os.environ.get(
377
+ "UNITY_MCP_SESSION_RESOLVE_MAX_WAIT_S", "2.0")
378
+ logger.warning(
379
+ "Invalid UNITY_MCP_SESSION_RESOLVE_MAX_WAIT_S=%r, using default 2.0: %s",
380
+ raw_val, e)
381
+ max_wait_s = 2.0
382
+ # Clamp to [0, 30] to prevent misconfiguration from causing excessive waits
383
+ max_wait_s = max(0.0, min(max_wait_s, 30.0))
367
384
  retry_ms = float(getattr(config, "reload_retry_ms", 250))
368
- sleep_seconds = max(0.05, retry_ms / 1000.0)
385
+ sleep_seconds = max(0.05, min(0.25, retry_ms / 1000.0))
369
386
 
370
387
  # Allow callers to provide either just the hash or Name@hash
371
388
  target_hash: str | None = None
@@ -394,7 +411,7 @@ class PluginHub(WebSocketEndpoint):
394
411
  return None, count
395
412
 
396
413
  session_id, session_count = await _try_once()
397
- deadline = time.monotonic() + (max_retries * sleep_seconds)
414
+ deadline = time.monotonic() + max_wait_s
398
415
  wait_started = None
399
416
 
400
417
  # If there is no active plugin yet (e.g., Unity starting up or reloading),
@@ -403,33 +420,40 @@ class PluginHub(WebSocketEndpoint):
403
420
  if not target_hash and session_count > 1:
404
421
  raise RuntimeError(
405
422
  "Multiple Unity instances are connected. "
406
- "Call set_active_instance with Name@hash from unity://instances."
423
+ "Call set_active_instance with Name@hash from mcpforunity://instances."
407
424
  )
408
425
  if wait_started is None:
409
426
  wait_started = time.monotonic()
410
427
  logger.debug(
411
- f"No plugin session available (instance={unity_instance or 'default'}); waiting up to {deadline - wait_started:.2f}s",
428
+ "No plugin session available (instance=%s); waiting up to %.2fs",
429
+ unity_instance or "default",
430
+ max_wait_s,
412
431
  )
413
432
  await asyncio.sleep(sleep_seconds)
414
433
  session_id, session_count = await _try_once()
415
434
 
416
435
  if session_id is not None and wait_started is not None:
417
436
  logger.debug(
418
- f"Plugin session restored after {time.monotonic() - wait_started:.3f}s (instance={unity_instance or 'default'})",
437
+ "Plugin session restored after %.3fs (instance=%s)",
438
+ time.monotonic() - wait_started,
439
+ unity_instance or "default",
419
440
  )
420
441
  if session_id is None and not target_hash and session_count > 1:
421
442
  raise RuntimeError(
422
443
  "Multiple Unity instances are connected. "
423
- "Call set_active_instance with Name@hash from unity://instances."
444
+ "Call set_active_instance with Name@hash from mcpforunity://instances."
424
445
  )
425
446
 
426
447
  if session_id is None:
427
448
  logger.warning(
428
- f"No Unity plugin reconnected within {max_retries * sleep_seconds:.2f}s (instance={unity_instance or 'default'})",
449
+ "No Unity plugin reconnected within %.2fs (instance=%s)",
450
+ max_wait_s,
451
+ unity_instance or "default",
429
452
  )
430
453
  # At this point we've given the plugin ample time to reconnect; surface
431
454
  # a clear error so the client can prompt the user to open Unity.
432
- raise RuntimeError("No Unity plugins are currently connected")
455
+ raise NoUnitySessionError(
456
+ "No Unity plugins are currently connected")
433
457
 
434
458
  return session_id
435
459
 
@@ -440,7 +464,20 @@ class PluginHub(WebSocketEndpoint):
440
464
  command_type: str,
441
465
  params: dict[str, Any],
442
466
  ) -> dict[str, Any]:
443
- session_id = await cls._resolve_session_id(unity_instance)
467
+ try:
468
+ session_id = await cls._resolve_session_id(unity_instance)
469
+ except NoUnitySessionError:
470
+ logger.debug(
471
+ "Unity session unavailable; returning retry: command=%s instance=%s",
472
+ command_type,
473
+ unity_instance or "default",
474
+ )
475
+ return MCPResponse(
476
+ success=False,
477
+ error="Unity session not available; please retry",
478
+ hint="retry",
479
+ data={"reason": "no_unity_session", "retry_after_ms": 250},
480
+ ).model_dump()
444
481
 
445
482
  # During domain reload / immediate reconnect windows, the plugin may be connected but not yet
446
483
  # ready to process execute commands on the Unity main thread (which can be further delayed when
@@ -449,8 +486,14 @@ class PluginHub(WebSocketEndpoint):
449
486
  # register_tools (which can be delayed by EditorApplication.delayCall).
450
487
  if command_type in cls._FAST_FAIL_COMMANDS and command_type != "ping":
451
488
  try:
452
- max_wait_s = float(os.environ.get("UNITY_MCP_SESSION_READY_WAIT_SECONDS", "6"))
453
- except Exception:
489
+ max_wait_s = float(os.environ.get(
490
+ "UNITY_MCP_SESSION_READY_WAIT_SECONDS", "6"))
491
+ except ValueError as e:
492
+ raw_val = os.environ.get(
493
+ "UNITY_MCP_SESSION_READY_WAIT_SECONDS", "6")
494
+ logger.warning(
495
+ "Invalid UNITY_MCP_SESSION_READY_WAIT_SECONDS=%r, using default 6.0: %s",
496
+ raw_val, e)
454
497
  max_wait_s = 6.0
455
498
  max_wait_s = max(0.0, min(max_wait_s, 30.0))
456
499
  if max_wait_s > 0:
@@ -463,7 +506,8 @@ class PluginHub(WebSocketEndpoint):
463
506
 
464
507
  # The Unity-side dispatcher responds with {status:"success", result:{message:"pong"}}
465
508
  if isinstance(probe, dict) and probe.get("status") == "success":
466
- result = probe.get("result") if isinstance(probe.get("result"), dict) else {}
509
+ result = probe.get("result") if isinstance(
510
+ probe.get("result"), dict) else {}
467
511
  if result.get("message") == "pong":
468
512
  break
469
513
  await asyncio.sleep(0.1)
@@ -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
 
@@ -102,7 +102,8 @@ class UnityInstanceMiddleware(Middleware):
102
102
  sessions = sessions_data.sessions or {}
103
103
  ids: list[str] = []
104
104
  for session_info in sessions.values():
105
- project = getattr(session_info, "project", None) or "Unknown"
105
+ project = getattr(
106
+ session_info, "project", None) or "Unknown"
106
107
  hash_value = getattr(session_info, "hash", None)
107
108
  if hash_value:
108
109
  ids.append(f"{project}@{hash_value}")
@@ -183,7 +184,7 @@ class UnityInstanceMiddleware(Middleware):
183
184
  # But for stdio transport (no PluginHub needed or maybe partially configured),
184
185
  # we should be careful not to clear instance just because PluginHub can't resolve it.
185
186
  # The 'active_instance' (Name@hash) might be valid for stdio even if PluginHub fails.
186
-
187
+
187
188
  session_id: str | None = None
188
189
  # Only validate via PluginHub if we are actually using HTTP transport
189
190
  # OR if we want to support hybrid mode. For now, let's be permissive.
@@ -105,7 +105,8 @@ async def send_with_unity_instance(
105
105
  # Fail fast with a retry hint instead of hanging for COMMAND_TIMEOUT.
106
106
  # The client can decide whether retrying is appropriate for the command.
107
107
  return normalize_unity_response(
108
- MCPResponse(success=False, error=err, hint="retry").model_dump()
108
+ MCPResponse(success=False, error=err,
109
+ hint="retry").model_dump()
109
110
  )
110
111
 
111
112
  if unity_instance:
@@ -1,71 +0,0 @@
1
- __init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- main.py,sha256=ITxelXUAmr9BoasG0knZifXKp3lWJyml_RLaIwvEyVs,19184
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.7.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=8hrNnskSFdsvdKagAYEeZGJ0Oz9QRlkWJjpM4q0XeNo,2013
22
- services/resources/editor_state_v2.py,sha256=zgss1EEhJo7oZeHnjOXsdJPuFQsHLwpsZmzcDy3ybq0,10874
23
- services/resources/layers.py,sha256=q4UQ5PUVUVhmM5l3oXID1wa_wOWAS8l5BGXadBgFuwY,1080
24
- services/resources/menu_items.py,sha256=9SNycjwTXoeS1ZHra0Y1fTyCjSEdPCo34JyxtuqauG8,1021
25
- services/resources/prefab_stage.py,sha256=C3mn3UapKYVOA8QUNmLsYreG5YiXdlvGm9ypHQeKBeQ,1382
26
- services/resources/project_info.py,sha256=gSVSFfwP0u2FmxSowOkdbNoSSQHxfQtLfndvoCXTVSw,1323
27
- services/resources/selection.py,sha256=4rI5Bdkes4uxtMc_5jQhUaqUl-iprhIiTWqnOJl8tmg,1839
28
- services/resources/tags.py,sha256=7EhmQjMotz85DSSr7cVKYIy7LPT5mmPfrEySr1mTE6w,1049
29
- services/resources/tests.py,sha256=xDvvgesPSU93nLD_ERQopOpkpq69pbMEqmFsJd0jekI,2063
30
- services/resources/unity_instances.py,sha256=fR0cVopGQnmF41IFDycwlo2XniKstfJWLGobgJeiabE,4348
31
- services/resources/windows.py,sha256=--QVsb0oyoBpSjK2D4kPcZFSe2zdR-t_KSHP-e2QNoY,1427
32
- services/state/external_changes_scanner.py,sha256=qwdiriHR1D11aPiLUbpS7COXtfVOjNj9DpzcSDK067o,9042
33
- services/tools/__init__.py,sha256=3Qav7fAowZ1_TbDRdZQQQES53gv2lTs-2D7PGECnlbM,2353
34
- services/tools/batch_execute.py,sha256=_ByjffeXQB9j64mcjaxJmrnbSJrMn0f9_6Zh9BBI_2c,2898
35
- services/tools/debug_request_context.py,sha256=WQBtQdXSH5stw2MAwIM32H6jGwUVQOgU2r35VUWLlYo,2765
36
- services/tools/execute_custom_tool.py,sha256=K2qaO4-FTPz0_3j53hhDP9idjC002ugc8C03FtHGTbY,1376
37
- services/tools/execute_menu_item.py,sha256=FAC-1v_TwOcy6GSxkogDsVxeRtdap0DsPlIngf8uJdU,1184
38
- services/tools/find_in_file.py,sha256=xp80lqRN2cdZc3XGJWlCpeQEy6WnwyKOj2l5WiHNx0Q,6379
39
- services/tools/manage_asset.py,sha256=6YjWOl2b58vRwjp-9XbQE9e1l3ajhGookVY8ncQN0wo,7877
40
- services/tools/manage_editor.py,sha256=_HZRT-_hBakH0g6p7BpxTv3gWpxsaV6KNGRol-qknwo,3243
41
- services/tools/manage_gameobject.py,sha256=OgFIsoPGiWHOj6-d3Lmtp3xlAW9Tr0c38tV4atAaFAU,14400
42
- services/tools/manage_material.py,sha256=wZB2H4orhL6wG9TTnmnk-Lj2Gj_zvg7koxW3t319BLU,3545
43
- services/tools/manage_prefabs.py,sha256=73XzznjFNOm1SazW_Y7l6uGIE7wosMpAIVQs8xpvK9A,3037
44
- services/tools/manage_scene.py,sha256=oJ1qDX0T06mINZ1hX2AcDp3ItHo-oUz7Uck0yujI9eA,4834
45
- services/tools/manage_script.py,sha256=lPA5HcS4Al0RiQVz-S6qahFTcPqsk3GSLLXJWHri8P4,27557
46
- services/tools/manage_scriptable_object.py,sha256=Oi03CJLgepaWR59V-nJiAjnCC8at4YqFhRGpACruqgw,3150
47
- services/tools/manage_shader.py,sha256=HHnHKh7vLij3p8FAinNsPdZGEKivgwSUTxdgDydfmbs,2882
48
- services/tools/preflight.py,sha256=VJn61h-9pvoVaCyKL7DTKLfbpoZfNK4fnRmj91c2o8M,4093
49
- services/tools/read_console.py,sha256=MdQcrnVXra9PLu5AFkmARjriObT0miExtQKkFaANznU,4662
50
- services/tools/refresh_unity.py,sha256=anTEuEzxKTFse6ldZxTsk43zI6ahRBDv3Sg_pMHYRYA,3719
51
- services/tools/run_tests.py,sha256=8CqmgRN6Bata666ytF_S9no4gaFmHCmeZM82ZwNQJ68,4666
52
- services/tools/script_apply_edits.py,sha256=qPm_PsmsK3mYXnziX_btyk8CaB66LTqpDFA2Y4ebZ4U,47504
53
- services/tools/set_active_instance.py,sha256=B18Y8Jga0pKsx9mFywXr1tWfy0cJVopIMXYO-UJ1jOU,4136
54
- services/tools/test_jobs.py,sha256=K6HjkzWPjJNldrp-Vq5gPH7oBkCq_sJZYXkK_Vg6I_I,4059
55
- services/tools/utils.py,sha256=4ZgfIu178eXZqRyzs8X77B5lKLP1f73OZoGBSDNokJ4,2409
56
- transport/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
- transport/models.py,sha256=6wp7wsmSaeeJEvUGXPF1m6zuJnxJ1NJlCC4YZ9oQIq0,1226
58
- transport/plugin_hub.py,sha256=g_DOhCThgJ9Oco_z3m2qpwDeUcFvvt7Z47xMS0diihw,21497
59
- transport/plugin_registry.py,sha256=nW-7O7PN0QUgSWivZTkpAVKKq9ZOe2b2yeIdpaNt_3I,4359
60
- transport/unity_instance_middleware.py,sha256=kf1QeA138r7PaC98dcMDYtUPGWZ4EUmZGESc2DdiWQs,10429
61
- transport/unity_transport.py,sha256=_cFVgD3pzFZRcDXANq4oPFYSoI6jntSGaN22dJC8LRU,3880
62
- transport/legacy/port_discovery.py,sha256=qM_mtndbYjAj4qPSZEWVeXFOt5_nKczG9pQqORXTBJ0,12768
63
- transport/legacy/stdio_port_registry.py,sha256=j4iARuP6wetppNDG8qKeuvo1bJKcSlgEhZvSyl_uf0A,2313
64
- transport/legacy/unity_connection.py,sha256=ujUX9WX7Gb-fxQveHts3uiepTPzFq8i7-XG7u5gSPuM,32668
65
- utils/module_discovery.py,sha256=My48ofB1BUqxiBoAZAGbEaLQYdsrDhMm8MayBP_bUSQ,2005
66
- utils/reload_sentinel.py,sha256=s1xMWhl-r2XwN0OUbiUv_VGUy8TvLtV5bkql-5n2DT0,373
67
- mcpforunityserver-8.7.0.dist-info/METADATA,sha256=m3U2_aFTIAFPWg8YE3v2KcMJmP-Ffz0R-EJsJwoD6pA,5712
68
- mcpforunityserver-8.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
69
- mcpforunityserver-8.7.0.dist-info/entry_points.txt,sha256=vCtkqw-J9t4pj7JwUZcB54_keVx7DpAR3fYzK6i-s6g,44
70
- mcpforunityserver-8.7.0.dist-info/top_level.txt,sha256=YKU5e5dREMfCnoVpmlsTm9bku7oqnrzSZ9FeTgjoxJw,58
71
- mcpforunityserver-8.7.0.dist-info/RECORD,,
routes/__init__.py DELETED
File without changes