Skip to content

Commit 1938756

Browse files
committed
server: centralize reload-aware retries and single-source retry_after_ms via config; increase default retry window (40 x 250ms); preserve structured reloading failures
1 parent b179ce1 commit 1938756

File tree

10 files changed

+104
-95
lines changed

10 files changed

+104
-95
lines changed

UnityMcpBridge/UnityMcpServer~/src/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ class ServerConfig:
2525
# Server settings
2626
max_retries: int = 10
2727
retry_delay: float = 0.25
28+
# Backoff hint returned to clients when Unity is reloading (milliseconds)
29+
reload_retry_ms: int = 250
30+
# Number of polite retries when Unity reports reloading
31+
# 40 × 250ms ≈ 10s default window
32+
reload_max_retries: int = 40
2833

2934
# Create a global config instance
3035
config = ServerConfig()

UnityMcpBridge/UnityMcpServer~/src/tools/execute_menu_item.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"""
44
from typing import Dict, Any
55
from mcp.server.fastmcp import FastMCP, Context
6-
from unity_connection import get_unity_connection # Import unity_connection module
6+
from unity_connection import get_unity_connection, send_command_with_retry # Import retry helper
7+
from config import config
78
import time
89

910
def register_execute_menu_item_tools(mcp: FastMCP):
@@ -43,15 +44,6 @@ async def execute_menu_item(
4344
if "parameters" not in params_dict:
4445
params_dict["parameters"] = {} # Ensure parameters dict exists
4546

46-
# Get Unity connection and send the command
47-
# We use the unity_connection module to communicate with Unity
48-
unity_conn = get_unity_connection()
49-
50-
# Send command to the ExecuteMenuItem C# handler
51-
# The command type should match what the Unity side expects
52-
resp = unity_conn.send_command("execute_menu_item", params_dict)
53-
if isinstance(resp, dict) and not resp.get("success", True) and resp.get("state") == "reloading":
54-
delay_ms = int(resp.get("retry_after_ms", 250))
55-
time.sleep(max(0.0, delay_ms / 1000.0))
56-
resp = unity_conn.send_command("execute_menu_item", params_dict)
57-
return resp
47+
# Use centralized retry helper
48+
resp = send_command_with_retry("execute_menu_item", params_dict)
49+
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}

UnityMcpBridge/UnityMcpServer~/src/tools/manage_asset.py

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from typing import Dict, Any
66
from mcp.server.fastmcp import FastMCP, Context
77
# from ..unity_connection import get_unity_connection # Original line that caused error
8-
from unity_connection import get_unity_connection # Use absolute import relative to Python dir
8+
from unity_connection import get_unity_connection, async_send_command_with_retry # Use centralized retry helper
9+
from config import config
910
import time
1011

1112
def register_manage_asset_tools(mcp: FastMCP):
@@ -72,22 +73,7 @@ async def manage_asset(
7273
# Get the Unity connection instance
7374
connection = get_unity_connection()
7475

75-
# Run the synchronous send_command in the default executor (thread pool)
76-
# This prevents blocking the main async event loop.
77-
result = await loop.run_in_executor(
78-
None, # Use default executor
79-
connection.send_command, # The function to call
80-
"manage_asset", # First argument for send_command
81-
params_dict # Second argument for send_command
82-
)
83-
if isinstance(result, dict) and not result.get("success", True) and result.get("state") == "reloading":
84-
delay_ms = int(result.get("retry_after_ms", 250))
85-
await asyncio.sleep(max(0.0, delay_ms / 1000.0))
86-
result = await loop.run_in_executor(
87-
None,
88-
connection.send_command,
89-
"manage_asset",
90-
params_dict
91-
)
76+
# Use centralized async retry helper to avoid blocking the event loop
77+
result = await async_send_command_with_retry("manage_asset", params_dict, loop=loop)
9278
# Return the result obtained from Unity
93-
return result
79+
return result if isinstance(result, dict) else {"success": False, "message": str(result)}

UnityMcpBridge/UnityMcpServer~/src/tools/manage_editor.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from mcp.server.fastmcp import FastMCP, Context
22
import time
33
from typing import Dict, Any
4-
from unity_connection import get_unity_connection
4+
from unity_connection import get_unity_connection, send_command_with_retry
5+
from config import config
56

67
def register_manage_editor_tools(mcp: FastMCP):
78
"""Register all editor management tools with the MCP server."""
@@ -41,18 +42,13 @@ def manage_editor(
4142
}
4243
params = {k: v for k, v in params.items() if v is not None}
4344

44-
# Send command to Unity (with a single polite retry if reloading)
45-
response = get_unity_connection().send_command("manage_editor", params)
46-
if isinstance(response, dict) and not response.get("success", True) and response.get("state") == "reloading":
47-
delay_ms = int(response.get("retry_after_ms", 250))
48-
time.sleep(max(0.0, delay_ms / 1000.0))
49-
response = get_unity_connection().send_command("manage_editor", params)
45+
# Send command using centralized retry helper
46+
response = send_command_with_retry("manage_editor", params)
5047

51-
# Process response
52-
if response.get("success"):
48+
# Preserve structured failure data; unwrap success into a friendlier shape
49+
if isinstance(response, dict) and response.get("success"):
5350
return {"success": True, "message": response.get("message", "Editor operation successful."), "data": response.get("data")}
54-
else:
55-
return {"success": False, "message": response.get("error", "An unknown error occurred during editor management.")}
51+
return response if isinstance(response, dict) else {"success": False, "message": str(response)}
5652

5753
except Exception as e:
5854
return {"success": False, "message": f"Python error managing editor: {str(e)}"}

UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from mcp.server.fastmcp import FastMCP, Context
22
from typing import Dict, Any, List
3-
from unity_connection import get_unity_connection
3+
from unity_connection import get_unity_connection, send_command_with_retry
4+
from config import config
45
import time
56

67
def register_manage_gameobject_tools(mcp: FastMCP):
@@ -123,21 +124,14 @@ def manage_gameobject(
123124
params.pop("prefab_folder", None)
124125
# --------------------------------
125126

126-
# Send the command to Unity via the established connection
127-
# Use the get_unity_connection function to retrieve the active connection instance
128-
# Changed "MANAGE_GAMEOBJECT" to "manage_gameobject" to potentially match Unity expectation
129-
response = get_unity_connection().send_command("manage_gameobject", params)
130-
if isinstance(response, dict) and not response.get("success", True) and response.get("state") == "reloading":
131-
delay_ms = int(response.get("retry_after_ms", 250))
132-
time.sleep(max(0.0, delay_ms / 1000.0))
133-
response = get_unity_connection().send_command("manage_gameobject", params)
127+
# Use centralized retry helper
128+
response = send_command_with_retry("manage_gameobject", params)
134129

135130
# Check if the response indicates success
136131
# If the response is not successful, raise an exception with the error message
137-
if response.get("success"):
132+
if isinstance(response, dict) and response.get("success"):
138133
return {"success": True, "message": response.get("message", "GameObject operation successful."), "data": response.get("data")}
139-
else:
140-
return {"success": False, "message": response.get("error", "An unknown error occurred during GameObject management.")}
134+
return response if isinstance(response, dict) else {"success": False, "message": str(response)}
141135

142136
except Exception as e:
143137
return {"success": False, "message": f"Python error managing GameObject: {str(e)}"}

UnityMcpBridge/UnityMcpServer~/src/tools/manage_scene.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from mcp.server.fastmcp import FastMCP, Context
22
from typing import Dict, Any
3-
from unity_connection import get_unity_connection
3+
from unity_connection import get_unity_connection, send_command_with_retry
4+
from config import config
45
import time
56

67
def register_manage_scene_tools(mcp: FastMCP):
@@ -35,18 +36,13 @@ def manage_scene(
3536
}
3637
params = {k: v for k, v in params.items() if v is not None}
3738

38-
# Send command to Unity (with a single polite retry if reloading)
39-
response = get_unity_connection().send_command("manage_scene", params)
40-
if isinstance(response, dict) and not response.get("success", True) and response.get("state") == "reloading":
41-
delay_ms = int(response.get("retry_after_ms", 250))
42-
time.sleep(max(0.0, delay_ms / 1000.0))
43-
response = get_unity_connection().send_command("manage_scene", params)
39+
# Use centralized retry helper
40+
response = send_command_with_retry("manage_scene", params)
4441

45-
# Process response
46-
if response.get("success"):
42+
# Preserve structured failure data; unwrap success into a friendlier shape
43+
if isinstance(response, dict) and response.get("success"):
4744
return {"success": True, "message": response.get("message", "Scene operation successful."), "data": response.get("data")}
48-
else:
49-
return {"success": False, "message": response.get("error", "An unknown error occurred during scene management.")}
45+
return response if isinstance(response, dict) else {"success": False, "message": str(response)}
5046

5147
except Exception as e:
5248
return {"success": False, "message": f"Python error managing scene: {str(e)}"}

UnityMcpBridge/UnityMcpServer~/src/tools/manage_script.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from mcp.server.fastmcp import FastMCP, Context
22
from typing import Dict, Any
3-
from unity_connection import get_unity_connection
3+
from unity_connection import get_unity_connection, send_command_with_retry
4+
from config import config
45
import time
56
import os
67
import base64
@@ -54,15 +55,11 @@ def manage_script(
5455
# Remove None values so they don't get sent as null
5556
params = {k: v for k, v in params.items() if v is not None}
5657

57-
# Send command to Unity (with single polite retry if reloading)
58-
response = get_unity_connection().send_command("manage_script", params)
59-
if isinstance(response, dict) and not response.get("success", True) and response.get("state") == "reloading":
60-
delay_ms = int(response.get("retry_after_ms", 250))
61-
time.sleep(max(0.0, delay_ms / 1000.0))
62-
response = get_unity_connection().send_command("manage_script", params)
58+
# Send command via centralized retry helper
59+
response = send_command_with_retry("manage_script", params)
6360

6461
# Process response from Unity
65-
if response.get("success"):
62+
if isinstance(response, dict) and response.get("success"):
6663
# If the response contains base64 encoded content, decode it
6764
if response.get("data", {}).get("contentsEncoded"):
6865
decoded_contents = base64.b64decode(response["data"]["encodedContents"]).decode('utf-8')
@@ -71,8 +68,7 @@ def manage_script(
7168
del response["data"]["contentsEncoded"]
7269

7370
return {"success": True, "message": response.get("message", "Operation successful."), "data": response.get("data")}
74-
else:
75-
return {"success": False, "message": response.get("error", "An unknown error occurred.")}
71+
return response if isinstance(response, dict) else {"success": False, "message": str(response)}
7672

7773
except Exception as e:
7874
# Handle Python-side errors (e.g., connection issues)

UnityMcpBridge/UnityMcpServer~/src/tools/manage_shader.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from mcp.server.fastmcp import FastMCP, Context
22
from typing import Dict, Any
3-
from unity_connection import get_unity_connection
3+
from unity_connection import get_unity_connection, send_command_with_retry
4+
from config import config
45
import time
56
import os
67
import base64
@@ -47,15 +48,11 @@ def manage_shader(
4748
# Remove None values so they don't get sent as null
4849
params = {k: v for k, v in params.items() if v is not None}
4950

50-
# Send command to Unity
51-
response = get_unity_connection().send_command("manage_shader", params)
52-
if isinstance(response, dict) and not response.get("success", True) and response.get("state") == "reloading":
53-
delay_ms = int(response.get("retry_after_ms", 250))
54-
time.sleep(max(0.0, delay_ms / 1000.0))
55-
response = get_unity_connection().send_command("manage_shader", params)
51+
# Send command via centralized retry helper
52+
response = send_command_with_retry("manage_shader", params)
5653

5754
# Process response from Unity
58-
if response.get("success"):
55+
if isinstance(response, dict) and response.get("success"):
5956
# If the response contains base64 encoded content, decode it
6057
if response.get("data", {}).get("contentsEncoded"):
6158
decoded_contents = base64.b64decode(response["data"]["encodedContents"]).decode('utf-8')
@@ -64,8 +61,7 @@ def manage_shader(
6461
del response["data"]["contentsEncoded"]
6562

6663
return {"success": True, "message": response.get("message", "Operation successful."), "data": response.get("data")}
67-
else:
68-
return {"success": False, "message": response.get("error", "An unknown error occurred.")}
64+
return response if isinstance(response, dict) else {"success": False, "message": str(response)}
6965

7066
except Exception as e:
7167
# Handle Python-side errors (e.g., connection issues)

UnityMcpBridge/UnityMcpServer~/src/tools/read_console.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
from typing import List, Dict, Any
55
import time
66
from mcp.server.fastmcp import FastMCP, Context
7-
from unity_connection import get_unity_connection
7+
from unity_connection import get_unity_connection, send_command_with_retry
8+
from config import config
89

910
def register_read_console_tools(mcp: FastMCP):
1011
"""Registers the read_console tool with the MCP server."""
@@ -67,10 +68,6 @@ def read_console(
6768
if 'count' not in params_dict:
6869
params_dict['count'] = None
6970

70-
# Forward the command using the bridge's send_command method (with a single polite retry on reload)
71-
resp = bridge.send_command("read_console", params_dict)
72-
if isinstance(resp, dict) and not resp.get("success", True) and resp.get("state") == "reloading":
73-
delay_ms = int(resp.get("retry_after_ms", 250))
74-
time.sleep(max(0.0, delay_ms / 1000.0))
75-
resp = bridge.send_command("read_console", params_dict)
76-
return resp
71+
# Use centralized retry helper
72+
resp = send_command_with_retry("read_console", params_dict)
73+
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}

UnityMcpBridge/UnityMcpServer~/src/unity_connection.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def read_status_file() -> dict | None:
139139
return {
140140
"success": False,
141141
"state": "reloading",
142-
"retry_after_ms": int(250),
142+
"retry_after_ms": int(config.reload_retry_ms),
143143
"error": "Unity domain reload in progress",
144144
"message": "Unity is reloading scripts; please retry shortly"
145145
}
@@ -278,3 +278,54 @@ def get_unity_connection() -> UnityConnection:
278278
pass
279279
_unity_connection = None
280280
raise ConnectionError(f"Could not establish valid Unity connection: {str(e)}")
281+
282+
283+
# -----------------------------
284+
# Centralized retry helpers
285+
# -----------------------------
286+
287+
def _is_reloading_response(resp: dict) -> bool:
288+
"""Return True if the Unity response indicates the editor is reloading."""
289+
if not isinstance(resp, dict):
290+
return False
291+
if resp.get("state") == "reloading":
292+
return True
293+
message_text = (resp.get("message") or resp.get("error") or "").lower()
294+
return "reload" in message_text
295+
296+
297+
def send_command_with_retry(command_type: str, params: Dict[str, Any], *, max_retries: int | None = None, retry_ms: int | None = None) -> Dict[str, Any]:
298+
"""Send a command via the shared connection, waiting politely through Unity reloads.
299+
300+
Uses config.reload_retry_ms and config.reload_max_retries by default. Preserves the
301+
structured failure if retries are exhausted.
302+
"""
303+
conn = get_unity_connection()
304+
if max_retries is None:
305+
max_retries = getattr(config, "reload_max_retries", 40)
306+
if retry_ms is None:
307+
retry_ms = getattr(config, "reload_retry_ms", 250)
308+
309+
response = conn.send_command(command_type, params)
310+
retries = 0
311+
while _is_reloading_response(response) and retries < max_retries:
312+
delay_ms = int(response.get("retry_after_ms", retry_ms)) if isinstance(response, dict) else retry_ms
313+
time.sleep(max(0.0, delay_ms / 1000.0))
314+
retries += 1
315+
response = conn.send_command(command_type, params)
316+
return response
317+
318+
319+
async def async_send_command_with_retry(command_type: str, params: Dict[str, Any], *, loop=None, max_retries: int | None = None, retry_ms: int | None = None) -> Dict[str, Any]:
320+
"""Async wrapper that runs the blocking retry helper in a thread pool."""
321+
try:
322+
import asyncio # local import to avoid mandatory asyncio dependency for sync callers
323+
if loop is None:
324+
loop = asyncio.get_running_loop()
325+
return await loop.run_in_executor(
326+
None,
327+
lambda: send_command_with_retry(command_type, params, max_retries=max_retries, retry_ms=retry_ms),
328+
)
329+
except Exception as e:
330+
# Return a structured error dict for consistency with other responses
331+
return {"success": False, "error": f"Python async retry helper failed: {str(e)}"}

0 commit comments

Comments
 (0)