Skip to content

Commit 73f8206

Browse files
sakurachanclaude
andcommitted
Fix CodeRabbit review feedback
- Fix partial framed response handling in port discovery Add _recv_exact() helper to ensure complete frame reading Prevents healthy Unity instances from being misidentified as offline - Remove unused default_conn variables in server.py (2 files) Fixes Ruff F841 lint error that would block CI/CD - Preserve sync/async nature of resources in wrapper Check if original function is coroutine before wrapping Prevents 'dict object is not awaitable' runtime errors - Fix reconnection to preserve instance_id Add instance_id tracking to UnityConnection dataclass Reconnection now targets the same Unity instance instead of any available one Prevents operations from being applied to wrong project - Add instance logging to manage_asset for debugging Helps troubleshoot multi-instance scenarios 🤖 Generated with Claude Code Co-Authored-By: Claude <[email protected]>
1 parent 83b96ac commit 73f8206

File tree

10 files changed

+114
-30
lines changed

10 files changed

+114
-30
lines changed

MCPForUnity/UnityMcpServer~/src/port_discovery.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,27 @@ def _try_probe_unity_mcp(port: int) -> bool:
8282
s.sendall(header + payload)
8383

8484
# 3. Receive framed response
85-
response_header = s.recv(8)
86-
if len(response_header) != 8:
85+
# Helper to receive exact number of bytes
86+
def _recv_exact(expected: int) -> bytes | None:
87+
chunks = bytearray()
88+
while len(chunks) < expected:
89+
chunk = s.recv(expected - len(chunks))
90+
if not chunk:
91+
return None
92+
chunks.extend(chunk)
93+
return bytes(chunks)
94+
95+
response_header = _recv_exact(8)
96+
if response_header is None:
8797
return False
8898

8999
response_length = struct.unpack('>Q', response_header)[0]
90100
if response_length > 10000: # Sanity check
91101
return False
92102

93-
response = s.recv(response_length)
103+
response = _recv_exact(response_length)
104+
if response is None:
105+
return False
94106
return b'"message":"pong"' in response
95107
except Exception as e:
96108
logger.debug(f"Port probe failed for {port}: {e}")

MCPForUnity/UnityMcpServer~/src/resources/__init__.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,24 @@
2121
def _create_fixed_wrapper(original_func, has_other_params):
2222
"""
2323
Factory function to create a wrapper that calls original_func with unity_instance=None.
24-
This avoids closure issues in loops.
24+
This avoids closure issues in loops and preserves sync/async nature of the original function.
2525
"""
26+
is_async = inspect.iscoroutinefunction(original_func)
27+
2628
if has_other_params:
27-
# Has other parameters (like mode)
28-
async def fixed_wrapper(*args, **kwargs):
29-
return await original_func(*args, **kwargs, unity_instance=None)
29+
if is_async:
30+
async def fixed_wrapper(*args, **kwargs):
31+
return await original_func(*args, **kwargs, unity_instance=None)
32+
else:
33+
def fixed_wrapper(*args, **kwargs):
34+
return original_func(*args, **kwargs, unity_instance=None)
3035
else:
31-
# No other parameters, just unity_instance
32-
async def fixed_wrapper():
33-
return await original_func(unity_instance=None)
36+
if is_async:
37+
async def fixed_wrapper():
38+
return await original_func(unity_instance=None)
39+
else:
40+
def fixed_wrapper():
41+
return original_func(unity_instance=None)
3442

3543
return fixed_wrapper
3644

MCPForUnity/UnityMcpServer~/src/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def _emit_startup():
111111

112112
# Try to connect to default instance
113113
try:
114-
default_conn = _unity_connection_pool.get_connection()
114+
_unity_connection_pool.get_connection()
115115
logger.info("Connected to default Unity instance on startup")
116116

117117
# Record successful Unity connection (deferred)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ async def manage_asset(
3535
unity_instance: Annotated[str,
3636
"Target Unity instance (project name, hash, or 'Name@hash'). If not specified, uses default instance."] | None = None
3737
) -> dict[str, Any]:
38-
ctx.info(f"Processing manage_asset: {action}")
38+
ctx.info(f"Processing manage_asset: {action} (unity_instance={unity_instance or 'default'})")
3939
# Coerce 'properties' from JSON string to dict for client compatibility
4040
if isinstance(properties, str):
4141
try:

MCPForUnity/UnityMcpServer~/src/unity_connection.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class UnityConnection:
3838
port: int = None # Will be set dynamically
3939
sock: socket.socket = None # Socket for Unity communication
4040
use_framing: bool = False # Negotiated per-connection
41+
instance_id: str | None = None # Instance identifier for reconnection
4142

4243
def __post_init__(self):
4344
"""Set port from discovery if not explicitly provided"""
@@ -329,9 +330,24 @@ def read_status_file() -> dict | None:
329330
finally:
330331
self.sock = None
331332

332-
# Re-discover port each time
333+
# Re-discover the port for this specific instance
333334
try:
334-
new_port = PortDiscovery.discover_unity_port()
335+
new_port: int | None = None
336+
if self.instance_id:
337+
# Try to rediscover the specific instance
338+
pool = get_unity_connection_pool()
339+
refreshed = pool.discover_all_instances(force_refresh=True)
340+
match = next((inst for inst in refreshed if inst.id == self.instance_id), None)
341+
if match:
342+
new_port = match.port
343+
logger.debug(f"Rediscovered instance {self.instance_id} on port {new_port}")
344+
else:
345+
logger.warning(f"Instance {self.instance_id} not found during reconnection")
346+
347+
# Fallback to generic port discovery if instance-specific discovery failed
348+
if new_port is None:
349+
new_port = PortDiscovery.discover_unity_port()
350+
335351
if new_port != self.port:
336352
logger.info(
337353
f"Unity port changed {self.port} -> {new_port}")
@@ -547,14 +563,20 @@ def get_connection(self, instance_identifier: Optional[str] = None) -> UnityConn
547563
with self._pool_lock:
548564
if target.id not in self._connections:
549565
logger.info(f"Creating new connection to Unity instance: {target.id} (port {target.port})")
550-
conn = UnityConnection(port=target.port)
566+
conn = UnityConnection(port=target.port, instance_id=target.id)
551567
if not conn.connect():
552568
raise ConnectionError(
553569
f"Failed to connect to Unity instance '{target.id}' on port {target.port}. "
554570
f"Ensure the Unity Editor is running."
555571
)
556572
self._connections[target.id] = conn
557573
else:
574+
# Update existing connection with instance_id and port if changed
575+
conn = self._connections[target.id]
576+
conn.instance_id = target.id
577+
if conn.port != target.port:
578+
logger.info(f"Updating cached port for {target.id}: {conn.port} -> {target.port}")
579+
conn.port = target.port
558580
logger.debug(f"Reusing existing connection to: {target.id}")
559581

560582
return self._connections[target.id]

Server/port_discovery.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,27 @@ def _try_probe_unity_mcp(port: int) -> bool:
8282
s.sendall(header + payload)
8383

8484
# 3. Receive framed response
85-
response_header = s.recv(8)
86-
if len(response_header) != 8:
85+
# Helper to receive exact number of bytes
86+
def _recv_exact(expected: int) -> bytes | None:
87+
chunks = bytearray()
88+
while len(chunks) < expected:
89+
chunk = s.recv(expected - len(chunks))
90+
if not chunk:
91+
return None
92+
chunks.extend(chunk)
93+
return bytes(chunks)
94+
95+
response_header = _recv_exact(8)
96+
if response_header is None:
8797
return False
8898

8999
response_length = struct.unpack('>Q', response_header)[0]
90100
if response_length > 10000: # Sanity check
91101
return False
92102

93-
response = s.recv(response_length)
103+
response = _recv_exact(response_length)
104+
if response is None:
105+
return False
94106
return b'"message":"pong"' in response
95107
except Exception as e:
96108
logger.debug(f"Port probe failed for {port}: {e}")

Server/resources/__init__.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,24 @@
2121
def _create_fixed_wrapper(original_func, has_other_params):
2222
"""
2323
Factory function to create a wrapper that calls original_func with unity_instance=None.
24-
This avoids closure issues in loops.
24+
This avoids closure issues in loops and preserves sync/async nature of the original function.
2525
"""
26+
is_async = inspect.iscoroutinefunction(original_func)
27+
2628
if has_other_params:
27-
# Has other parameters (like mode)
28-
async def fixed_wrapper(*args, **kwargs):
29-
return await original_func(*args, **kwargs, unity_instance=None)
29+
if is_async:
30+
async def fixed_wrapper(*args, **kwargs):
31+
return await original_func(*args, **kwargs, unity_instance=None)
32+
else:
33+
def fixed_wrapper(*args, **kwargs):
34+
return original_func(*args, **kwargs, unity_instance=None)
3035
else:
31-
# No other parameters, just unity_instance
32-
async def fixed_wrapper():
33-
return await original_func(unity_instance=None)
36+
if is_async:
37+
async def fixed_wrapper():
38+
return await original_func(unity_instance=None)
39+
else:
40+
def fixed_wrapper():
41+
return original_func(unity_instance=None)
3442

3543
return fixed_wrapper
3644

Server/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def _emit_startup():
111111

112112
# Try to connect to default instance
113113
try:
114-
default_conn = _unity_connection_pool.get_connection()
114+
_unity_connection_pool.get_connection()
115115
logger.info("Connected to default Unity instance on startup")
116116

117117
# Record successful Unity connection (deferred)

Server/tools/manage_asset.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ async def manage_asset(
3535
unity_instance: Annotated[str,
3636
"Target Unity instance (project name, hash, or 'Name@hash'). If not specified, uses default instance."] | None = None
3737
) -> dict[str, Any]:
38-
ctx.info(f"Processing manage_asset: {action}")
38+
ctx.info(f"Processing manage_asset: {action} (unity_instance={unity_instance or 'default'})")
3939
# Coerce 'properties' from JSON string to dict for client compatibility
4040
if isinstance(properties, str):
4141
try:

Server/unity_connection.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class UnityConnection:
3838
port: int = None # Will be set dynamically
3939
sock: socket.socket = None # Socket for Unity communication
4040
use_framing: bool = False # Negotiated per-connection
41+
instance_id: str | None = None # Instance identifier for reconnection
4142

4243
def __post_init__(self):
4344
"""Set port from discovery if not explicitly provided"""
@@ -329,9 +330,24 @@ def read_status_file() -> dict | None:
329330
finally:
330331
self.sock = None
331332

332-
# Re-discover port each time
333+
# Re-discover the port for this specific instance
333334
try:
334-
new_port = PortDiscovery.discover_unity_port()
335+
new_port: int | None = None
336+
if self.instance_id:
337+
# Try to rediscover the specific instance
338+
pool = get_unity_connection_pool()
339+
refreshed = pool.discover_all_instances(force_refresh=True)
340+
match = next((inst for inst in refreshed if inst.id == self.instance_id), None)
341+
if match:
342+
new_port = match.port
343+
logger.debug(f"Rediscovered instance {self.instance_id} on port {new_port}")
344+
else:
345+
logger.warning(f"Instance {self.instance_id} not found during reconnection")
346+
347+
# Fallback to generic port discovery if instance-specific discovery failed
348+
if new_port is None:
349+
new_port = PortDiscovery.discover_unity_port()
350+
335351
if new_port != self.port:
336352
logger.info(
337353
f"Unity port changed {self.port} -> {new_port}")
@@ -547,14 +563,20 @@ def get_connection(self, instance_identifier: Optional[str] = None) -> UnityConn
547563
with self._pool_lock:
548564
if target.id not in self._connections:
549565
logger.info(f"Creating new connection to Unity instance: {target.id} (port {target.port})")
550-
conn = UnityConnection(port=target.port)
566+
conn = UnityConnection(port=target.port, instance_id=target.id)
551567
if not conn.connect():
552568
raise ConnectionError(
553569
f"Failed to connect to Unity instance '{target.id}' on port {target.port}. "
554570
f"Ensure the Unity Editor is running."
555571
)
556572
self._connections[target.id] = conn
557573
else:
574+
# Update existing connection with instance_id and port if changed
575+
conn = self._connections[target.id]
576+
conn.instance_id = target.id
577+
if conn.port != target.port:
578+
logger.info(f"Updating cached port for {target.id}: {conn.port} -> {target.port}")
579+
conn.port = target.port
558580
logger.debug(f"Reusing existing connection to: {target.id}")
559581

560582
return self._connections[target.id]

0 commit comments

Comments
 (0)