mcpforunityserver 8.7.0__py3-none-any.whl → 9.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. cli/__init__.py +3 -0
  2. cli/commands/__init__.py +3 -0
  3. cli/commands/animation.py +87 -0
  4. cli/commands/asset.py +310 -0
  5. cli/commands/audio.py +133 -0
  6. cli/commands/batch.py +184 -0
  7. cli/commands/code.py +189 -0
  8. cli/commands/component.py +212 -0
  9. cli/commands/editor.py +487 -0
  10. cli/commands/gameobject.py +510 -0
  11. cli/commands/instance.py +101 -0
  12. cli/commands/lighting.py +128 -0
  13. cli/commands/material.py +268 -0
  14. cli/commands/prefab.py +144 -0
  15. cli/commands/scene.py +255 -0
  16. cli/commands/script.py +240 -0
  17. cli/commands/shader.py +238 -0
  18. cli/commands/ui.py +263 -0
  19. cli/commands/vfx.py +439 -0
  20. cli/main.py +248 -0
  21. cli/utils/__init__.py +31 -0
  22. cli/utils/config.py +58 -0
  23. cli/utils/connection.py +191 -0
  24. cli/utils/output.py +195 -0
  25. main.py +177 -62
  26. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.1.0.dist-info}/METADATA +4 -2
  27. mcpforunityserver-9.1.0.dist-info/RECORD +96 -0
  28. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.1.0.dist-info}/WHEEL +1 -1
  29. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.1.0.dist-info}/entry_points.txt +1 -0
  30. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.1.0.dist-info}/top_level.txt +1 -2
  31. services/custom_tool_service.py +179 -19
  32. services/resources/__init__.py +6 -1
  33. services/resources/active_tool.py +1 -1
  34. services/resources/custom_tools.py +2 -2
  35. services/resources/editor_state.py +283 -30
  36. services/resources/gameobject.py +243 -0
  37. services/resources/layers.py +1 -1
  38. services/resources/prefab_stage.py +1 -1
  39. services/resources/project_info.py +1 -1
  40. services/resources/selection.py +1 -1
  41. services/resources/tags.py +1 -1
  42. services/resources/unity_instances.py +1 -1
  43. services/resources/windows.py +1 -1
  44. services/state/external_changes_scanner.py +3 -4
  45. services/tools/__init__.py +6 -1
  46. services/tools/batch_execute.py +24 -9
  47. services/tools/debug_request_context.py +8 -2
  48. services/tools/execute_custom_tool.py +6 -1
  49. services/tools/execute_menu_item.py +6 -3
  50. services/tools/find_gameobjects.py +89 -0
  51. services/tools/find_in_file.py +26 -19
  52. services/tools/manage_asset.py +13 -44
  53. services/tools/manage_components.py +131 -0
  54. services/tools/manage_editor.py +9 -8
  55. services/tools/manage_gameobject.py +115 -79
  56. services/tools/manage_material.py +80 -31
  57. services/tools/manage_prefabs.py +7 -1
  58. services/tools/manage_scene.py +30 -13
  59. services/tools/manage_script.py +62 -19
  60. services/tools/manage_scriptable_object.py +22 -10
  61. services/tools/manage_shader.py +8 -1
  62. services/tools/manage_vfx.py +738 -0
  63. services/tools/preflight.py +15 -12
  64. services/tools/read_console.py +70 -17
  65. services/tools/refresh_unity.py +92 -29
  66. services/tools/run_tests.py +187 -53
  67. services/tools/script_apply_edits.py +15 -7
  68. services/tools/set_active_instance.py +12 -7
  69. services/tools/utils.py +60 -6
  70. transport/legacy/port_discovery.py +2 -2
  71. transport/legacy/unity_connection.py +129 -26
  72. transport/plugin_hub.py +85 -24
  73. transport/unity_instance_middleware.py +4 -3
  74. transport/unity_transport.py +2 -1
  75. utils/focus_nudge.py +321 -0
  76. __init__.py +0 -0
  77. mcpforunityserver-8.7.0.dist-info/RECORD +0 -71
  78. routes/__init__.py +0 -0
  79. services/resources/editor_state_v2.py +0 -270
  80. services/tools/test_jobs.py +0 -94
  81. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -4,6 +4,7 @@ import re
4
4
  from typing import Annotated, Any, Union
5
5
 
6
6
  from fastmcp import Context
7
+ from mcp.types import ToolAnnotations
7
8
 
8
9
  from services.registry import mcp_for_unity_tool
9
10
  from services.tools import get_unity_instance_from_context
@@ -228,7 +229,7 @@ def _normalize_script_locator(name: str, path: str) -> tuple[str, str]:
228
229
  - name = "SmartReach.cs", path = "Assets/Scripts/Interaction"
229
230
  - name = "Assets/Scripts/Interaction/SmartReach.cs", path = ""
230
231
  - path = "Assets/Scripts/Interaction/SmartReach.cs" (name empty)
231
- - name or path using uri prefixes: unity://path/..., file://...
232
+ - name or path using uri prefixes: mcpforunity://path/..., file://...
232
233
  - accidental duplicates like "Assets/.../SmartReach.cs/SmartReach.cs"
233
234
 
234
235
  Returns (name_without_extension, directory_path_under_Assets).
@@ -237,8 +238,8 @@ def _normalize_script_locator(name: str, path: str) -> tuple[str, str]:
237
238
  p = (path or "").strip()
238
239
 
239
240
  def strip_prefix(s: str) -> str:
240
- if s.startswith("unity://path/"):
241
- return s[len("unity://path/"):]
241
+ if s.startswith("mcpforunity://path/"):
242
+ return s[len("mcpforunity://path/"):]
242
243
  if s.startswith("file://"):
243
244
  return s[len("file://"):]
244
245
  return s
@@ -309,8 +310,10 @@ def _err(code: str, message: str, *, expected: dict[str, Any] | None = None, rew
309
310
  # Natural-language parsing removed; clients should send structured edits.
310
311
 
311
312
 
312
- @mcp_for_unity_tool(name="script_apply_edits", description=(
313
- """Structured C# edits (methods/classes) with safer boundaries - prefer this over raw text.
313
+ @mcp_for_unity_tool(
314
+ name="script_apply_edits",
315
+ description=(
316
+ """Structured C# edits (methods/classes) with safer boundaries - prefer this over raw text.
314
317
  Best practices:
315
318
  - Prefer anchor_* ops for pattern-based insert/replace near stable markers
316
319
  - Use replace_method/delete_method for whole-method changes (keeps signatures balanced)
@@ -356,7 +359,12 @@ def _err(code: str, message: str, *, expected: dict[str, Any] | None = None, rew
356
359
  ],
357
360
  }
358
361
  ]"""
359
- ))
362
+ ),
363
+ annotations=ToolAnnotations(
364
+ title="Script Apply Edits",
365
+ destructiveHint=True,
366
+ ),
367
+ )
360
368
  async def script_apply_edits(
361
369
  ctx: Context,
362
370
  name: Annotated[str, "Name of the script to edit"],
@@ -372,7 +380,7 @@ async def script_apply_edits(
372
380
  unity_instance = get_unity_instance_from_context(ctx)
373
381
  await ctx.info(
374
382
  f"Processing script_apply_edits: {name} (unity_instance={unity_instance or 'default'})")
375
-
383
+
376
384
  # Parse edits if they came as a stringified JSON
377
385
  edits = parse_json_payload(edits)
378
386
  if not isinstance(edits, list):
@@ -2,6 +2,8 @@ from typing import Annotated, Any
2
2
  from types import SimpleNamespace
3
3
 
4
4
  from fastmcp import Context
5
+ from mcp.types import ToolAnnotations
6
+
5
7
  from services.registry import mcp_for_unity_tool
6
8
  from transport.legacy.unity_connection import get_unity_connection_pool
7
9
  from transport.unity_instance_middleware import get_unity_instance_middleware
@@ -10,7 +12,10 @@ from transport.unity_transport import _current_transport
10
12
 
11
13
 
12
14
  @mcp_for_unity_tool(
13
- description="Set the active Unity instance for this client/session. Accepts Name@hash or hash."
15
+ description="Set the active Unity instance for this client/session. Accepts Name@hash or hash.",
16
+ annotations=ToolAnnotations(
17
+ title="Set Active Instance",
18
+ ),
14
19
  )
15
20
  async def set_active_instance(
16
21
  ctx: Context,
@@ -51,7 +56,7 @@ async def set_active_instance(
51
56
  return {
52
57
  "success": False,
53
58
  "error": "Instance identifier is required. "
54
- "Use unity://instances to copy a Name@hash or provide a hash prefix."
59
+ "Use mcpforunity://instances to copy a Name@hash or provide a hash prefix."
55
60
  }
56
61
  resolved = None
57
62
  if "@" in value:
@@ -60,7 +65,7 @@ async def set_active_instance(
60
65
  return {
61
66
  "success": False,
62
67
  "error": f"Instance '{value}' not found. "
63
- "Use unity://instances to copy an exact Name@hash."
68
+ "Use mcpforunity://instances to copy an exact Name@hash."
64
69
  }
65
70
  else:
66
71
  lookup = value.lower()
@@ -75,7 +80,7 @@ async def set_active_instance(
75
80
  return {
76
81
  "success": False,
77
82
  "error": f"Instance hash '{value}' does not match any running Unity editors. "
78
- "Use unity://instances to confirm the available hashes."
83
+ "Use mcpforunity://instances to confirm the available hashes."
79
84
  }
80
85
  if len(matches) > 1:
81
86
  matching_ids = ", ".join(
@@ -84,10 +89,10 @@ async def set_active_instance(
84
89
  return {
85
90
  "success": False,
86
91
  "error": f"Instance hash '{value}' is ambiguous ({matching_ids}). "
87
- "Provide the full Name@hash from unity://instances."
92
+ "Provide the full Name@hash from mcpforunity://instances."
88
93
  }
89
94
  resolved = matches[0]
90
-
95
+
91
96
  if resolved is None:
92
97
  # Should be unreachable due to logic above, but satisfies static analysis
93
98
  return {
@@ -101,7 +106,7 @@ async def set_active_instance(
101
106
  # The session key is an internal detail but useful for debugging response.
102
107
  middleware.set_active_instance(ctx, resolved.id)
103
108
  session_key = middleware.get_session_key(ctx)
104
-
109
+
105
110
  return {
106
111
  "success": True,
107
112
  "message": f"Active instance set to {resolved.id}",
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:
@@ -233,14 +233,21 @@ class UnityConnection:
233
233
  logger.error(f"Error during receive: {str(e)}")
234
234
  raise
235
235
 
236
- def send_command(self, command_type: str, params: dict[str, Any] = None) -> dict[str, Any]:
237
- """Send a command with retry/backoff and port rediscovery. Pings only when requested."""
236
+ def send_command(self, command_type: str, params: dict[str, Any] = None, max_attempts: int | None = None) -> dict[str, Any]:
237
+ """Send a command with retry/backoff and port rediscovery. Pings only when requested.
238
+
239
+ Args:
240
+ command_type: The Unity command to send
241
+ params: Command parameters
242
+ max_attempts: Maximum retry attempts (None = use config default, 0 = no retries)
243
+ """
238
244
  # Defensive guard: catch empty/placeholder invocations early
239
245
  if not command_type:
240
246
  raise ValueError("MCP call missing command_type")
241
247
  if params is None:
242
248
  return MCPResponse(success=False, error="MCP call received with no parameters (client placeholder?)")
243
- attempts = max(config.max_retries, 5)
249
+ attempts = max(config.max_retries,
250
+ 5) if max_attempts is None else max_attempts
244
251
  base_backoff = max(0.5, config.retry_delay)
245
252
 
246
253
  def read_status_file(target_hash: str | None = None) -> dict | None:
@@ -584,7 +591,7 @@ class UnityConnectionPool:
584
591
  raise ConnectionError(
585
592
  f"Unity instance '{identifier}' not found. "
586
593
  f"Available instances: {available_ids}. "
587
- f"Check unity://instances resource for all instances."
594
+ f"Check mcpforunity://instances resource for all instances."
588
595
  )
589
596
 
590
597
  def get_connection(self, instance_identifier: str | None = None) -> UnityConnection:
@@ -686,28 +693,46 @@ def get_unity_connection(instance_identifier: str | None = None) -> UnityConnect
686
693
  # Centralized retry helpers
687
694
  # -----------------------------
688
695
 
689
- def _is_reloading_response(resp: object) -> bool:
690
- """Return True if the Unity response indicates the editor is reloading.
696
+ def _extract_response_reason(resp: object) -> str | None:
697
+ """Extract a normalized (lowercase) reason string from a response.
691
698
 
692
- Supports both raw dict payloads from Unity and MCPResponse objects returned
693
- by preflight checks or transport helpers.
699
+ Returns lowercase reason values to enable case-insensitive comparisons
700
+ by callers (e.g. _is_reloading_response, refresh_unity).
694
701
  """
695
- # Structured MCPResponse from preflight/transport
696
702
  if isinstance(resp, MCPResponse):
697
- # Explicit "please retry" hint from preflight
698
- if getattr(resp, "hint", None) == "retry":
699
- return True
703
+ data = getattr(resp, "data", None)
704
+ if isinstance(data, dict):
705
+ reason = data.get("reason")
706
+ if isinstance(reason, str):
707
+ return reason.lower()
700
708
  message_text = f"{resp.message or ''} {resp.error or ''}".lower()
701
- return "reload" in message_text
709
+ if "reload" in message_text:
710
+ return "reloading"
711
+ return None
702
712
 
703
- # Raw Unity payloads
704
713
  if isinstance(resp, dict):
705
714
  if resp.get("state") == "reloading":
706
- return True
715
+ return "reloading"
716
+ data = resp.get("data")
717
+ if isinstance(data, dict):
718
+ reason = data.get("reason")
719
+ if isinstance(reason, str):
720
+ return reason.lower()
707
721
  message_text = (resp.get("message") or resp.get("error") or "").lower()
708
- return "reload" in message_text
722
+ if "reload" in message_text:
723
+ return "reloading"
724
+ return None
709
725
 
710
- return False
726
+ return None
727
+
728
+
729
+ def _is_reloading_response(resp: object) -> bool:
730
+ """Return True if the Unity response indicates the editor is reloading.
731
+
732
+ Supports both raw dict payloads from Unity and MCPResponse objects returned
733
+ by preflight checks or transport helpers.
734
+ """
735
+ return _extract_response_reason(resp) == "reloading"
711
736
 
712
737
 
713
738
  def send_command_with_retry(
@@ -716,7 +741,8 @@ def send_command_with_retry(
716
741
  *,
717
742
  instance_id: str | None = None,
718
743
  max_retries: int | None = None,
719
- retry_ms: int | None = None
744
+ retry_ms: int | None = None,
745
+ retry_on_reload: bool = True
720
746
  ) -> dict[str, Any] | MCPResponse:
721
747
  """Send a command to a Unity instance, waiting politely through Unity reloads.
722
748
 
@@ -726,6 +752,8 @@ def send_command_with_retry(
726
752
  instance_id: Optional Unity instance identifier (name, hash, name@hash, etc.)
727
753
  max_retries: Maximum number of retries for reload states
728
754
  retry_ms: Delay between retries in milliseconds
755
+ retry_on_reload: If False, don't retry when Unity is reloading (for commands
756
+ that trigger compilation/reload and shouldn't be re-sent)
729
757
 
730
758
  Returns:
731
759
  Response dictionary or MCPResponse from Unity
@@ -738,15 +766,87 @@ def send_command_with_retry(
738
766
  max_retries = getattr(config, "reload_max_retries", 40)
739
767
  if retry_ms is None:
740
768
  retry_ms = getattr(config, "reload_retry_ms", 250)
741
-
742
- response = conn.send_command(command_type, params)
769
+ try:
770
+ max_wait_s = float(os.environ.get(
771
+ "UNITY_MCP_RELOAD_MAX_WAIT_S", "2.0"))
772
+ except ValueError as e:
773
+ raw_val = os.environ.get("UNITY_MCP_RELOAD_MAX_WAIT_S", "2.0")
774
+ logger.warning(
775
+ "Invalid UNITY_MCP_RELOAD_MAX_WAIT_S=%r, using default 2.0: %s",
776
+ raw_val, e)
777
+ max_wait_s = 2.0
778
+ # Clamp to [0, 30] to prevent misconfiguration from causing excessive waits
779
+ max_wait_s = max(0.0, min(max_wait_s, 30.0))
780
+
781
+ # If retry_on_reload=False, disable connection-level retries too (issue #577)
782
+ # Commands that trigger compilation/reload shouldn't retry on disconnect
783
+ send_max_attempts = None if retry_on_reload else 0
784
+
785
+ response = conn.send_command(
786
+ command_type, params, max_attempts=send_max_attempts)
743
787
  retries = 0
744
- 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))
788
+ wait_started = None
789
+ reason = _extract_response_reason(response)
790
+ while retry_on_reload and _is_reloading_response(response) and retries < max_retries:
791
+ if wait_started is None:
792
+ wait_started = time.monotonic()
793
+ logger.debug(
794
+ "Unity reload wait started: command=%s instance=%s reason=%s max_wait_s=%.2f",
795
+ command_type,
796
+ instance_id or "default",
797
+ reason or "reloading",
798
+ max_wait_s,
799
+ )
800
+ if max_wait_s <= 0:
801
+ break
802
+ elapsed = time.monotonic() - wait_started
803
+ if elapsed >= max_wait_s:
804
+ break
805
+ delay_ms = retry_ms
806
+ if isinstance(response, dict):
807
+ retry_after = response.get("retry_after_ms")
808
+ if retry_after is None and isinstance(response.get("data"), dict):
809
+ retry_after = response["data"].get("retry_after_ms")
810
+ if retry_after is not None:
811
+ delay_ms = int(retry_after)
812
+ sleep_ms = max(50, min(int(delay_ms), 250))
813
+ logger.debug(
814
+ "Unity reload wait retry: command=%s instance=%s reason=%s retry_after_ms=%s sleep_ms=%s",
815
+ command_type,
816
+ instance_id or "default",
817
+ reason or "reloading",
818
+ delay_ms,
819
+ sleep_ms,
820
+ )
821
+ time.sleep(max(0.0, sleep_ms / 1000.0))
748
822
  retries += 1
749
823
  response = conn.send_command(command_type, params)
824
+ reason = _extract_response_reason(response)
825
+
826
+ if wait_started is not None:
827
+ waited = time.monotonic() - wait_started
828
+ if _is_reloading_response(response):
829
+ logger.debug(
830
+ "Unity reload wait exceeded budget: command=%s instance=%s waited_s=%.3f",
831
+ command_type,
832
+ instance_id or "default",
833
+ waited,
834
+ )
835
+ return MCPResponse(
836
+ success=False,
837
+ error="Unity is reloading; please retry",
838
+ hint="retry",
839
+ data={
840
+ "reason": "reloading",
841
+ "retry_after_ms": min(250, max(50, retry_ms)),
842
+ },
843
+ )
844
+ logger.debug(
845
+ "Unity reload wait completed: command=%s instance=%s waited_s=%.3f",
846
+ command_type,
847
+ instance_id or "default",
848
+ waited,
849
+ )
750
850
  return response
751
851
 
752
852
 
@@ -757,7 +857,8 @@ async def async_send_command_with_retry(
757
857
  instance_id: str | None = None,
758
858
  loop=None,
759
859
  max_retries: int | None = None,
760
- retry_ms: int | None = None
860
+ retry_ms: int | None = None,
861
+ retry_on_reload: bool = True
761
862
  ) -> dict[str, Any] | MCPResponse:
762
863
  """Async wrapper that runs the blocking retry helper in a thread pool.
763
864
 
@@ -768,6 +869,7 @@ async def async_send_command_with_retry(
768
869
  loop: Optional asyncio event loop
769
870
  max_retries: Maximum number of retries for reload states
770
871
  retry_ms: Delay between retries in milliseconds
872
+ retry_on_reload: If False, don't retry when Unity is reloading
771
873
 
772
874
  Returns:
773
875
  Response dictionary or MCPResponse on error
@@ -779,7 +881,8 @@ async def async_send_command_with_retry(
779
881
  return await loop.run_in_executor(
780
882
  None,
781
883
  lambda: send_command_with_retry(
782
- command_type, params, instance_id=instance_id, max_retries=max_retries, retry_ms=retry_ms),
884
+ command_type, params, instance_id=instance_id, max_retries=max_retries,
885
+ retry_ms=retry_ms, retry_on_reload=retry_on_reload),
783
886
  )
784
887
  except Exception as e:
785
888
  return MCPResponse(success=False, error=str(e))