mcpforunityserver 9.3.0b20260129104751__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.
- cli/__init__.py +3 -0
- cli/commands/__init__.py +3 -0
- cli/commands/animation.py +84 -0
- cli/commands/asset.py +280 -0
- cli/commands/audio.py +125 -0
- cli/commands/batch.py +171 -0
- cli/commands/code.py +182 -0
- cli/commands/component.py +190 -0
- cli/commands/editor.py +447 -0
- cli/commands/gameobject.py +487 -0
- cli/commands/instance.py +93 -0
- cli/commands/lighting.py +123 -0
- cli/commands/material.py +239 -0
- cli/commands/prefab.py +248 -0
- cli/commands/scene.py +231 -0
- cli/commands/script.py +222 -0
- cli/commands/shader.py +226 -0
- cli/commands/texture.py +540 -0
- cli/commands/tool.py +58 -0
- cli/commands/ui.py +258 -0
- cli/commands/vfx.py +421 -0
- cli/main.py +281 -0
- cli/utils/__init__.py +31 -0
- cli/utils/config.py +58 -0
- cli/utils/confirmation.py +37 -0
- cli/utils/connection.py +258 -0
- cli/utils/constants.py +23 -0
- cli/utils/output.py +195 -0
- cli/utils/parsers.py +112 -0
- cli/utils/suggestions.py +34 -0
- core/__init__.py +0 -0
- core/config.py +52 -0
- core/logging_decorator.py +37 -0
- core/telemetry.py +551 -0
- core/telemetry_decorator.py +164 -0
- main.py +713 -0
- mcpforunityserver-9.3.0b20260129104751.dist-info/METADATA +216 -0
- mcpforunityserver-9.3.0b20260129104751.dist-info/RECORD +103 -0
- mcpforunityserver-9.3.0b20260129104751.dist-info/WHEEL +5 -0
- mcpforunityserver-9.3.0b20260129104751.dist-info/entry_points.txt +3 -0
- mcpforunityserver-9.3.0b20260129104751.dist-info/licenses/LICENSE +21 -0
- mcpforunityserver-9.3.0b20260129104751.dist-info/top_level.txt +7 -0
- models/__init__.py +4 -0
- models/models.py +56 -0
- models/unity_response.py +47 -0
- services/__init__.py +0 -0
- services/custom_tool_service.py +499 -0
- services/registry/__init__.py +22 -0
- services/registry/resource_registry.py +53 -0
- services/registry/tool_registry.py +51 -0
- services/resources/__init__.py +86 -0
- services/resources/active_tool.py +47 -0
- services/resources/custom_tools.py +57 -0
- services/resources/editor_state.py +304 -0
- services/resources/gameobject.py +243 -0
- services/resources/layers.py +29 -0
- services/resources/menu_items.py +34 -0
- services/resources/prefab.py +191 -0
- services/resources/prefab_stage.py +39 -0
- services/resources/project_info.py +39 -0
- services/resources/selection.py +55 -0
- services/resources/tags.py +30 -0
- services/resources/tests.py +87 -0
- services/resources/unity_instances.py +122 -0
- services/resources/windows.py +47 -0
- services/state/external_changes_scanner.py +245 -0
- services/tools/__init__.py +83 -0
- services/tools/batch_execute.py +93 -0
- services/tools/debug_request_context.py +86 -0
- services/tools/execute_custom_tool.py +43 -0
- services/tools/execute_menu_item.py +32 -0
- services/tools/find_gameobjects.py +110 -0
- services/tools/find_in_file.py +181 -0
- services/tools/manage_asset.py +119 -0
- services/tools/manage_components.py +131 -0
- services/tools/manage_editor.py +64 -0
- services/tools/manage_gameobject.py +260 -0
- services/tools/manage_material.py +111 -0
- services/tools/manage_prefabs.py +174 -0
- services/tools/manage_scene.py +111 -0
- services/tools/manage_script.py +645 -0
- services/tools/manage_scriptable_object.py +87 -0
- services/tools/manage_shader.py +71 -0
- services/tools/manage_texture.py +581 -0
- services/tools/manage_vfx.py +120 -0
- services/tools/preflight.py +110 -0
- services/tools/read_console.py +151 -0
- services/tools/refresh_unity.py +153 -0
- services/tools/run_tests.py +317 -0
- services/tools/script_apply_edits.py +1006 -0
- services/tools/set_active_instance.py +117 -0
- services/tools/utils.py +348 -0
- transport/__init__.py +0 -0
- transport/legacy/port_discovery.py +329 -0
- transport/legacy/stdio_port_registry.py +65 -0
- transport/legacy/unity_connection.py +888 -0
- transport/models.py +63 -0
- transport/plugin_hub.py +585 -0
- transport/plugin_registry.py +126 -0
- transport/unity_instance_middleware.py +232 -0
- transport/unity_transport.py +63 -0
- utils/focus_nudge.py +589 -0
- utils/module_discovery.py +55 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Port discovery utility for MCP for Unity Server.
|
|
3
|
+
|
|
4
|
+
What changed and why:
|
|
5
|
+
- Unity now writes a per-project port file named like
|
|
6
|
+
`~/.unity-mcp/unity-mcp-port-<hash>.json` to avoid projects overwriting
|
|
7
|
+
each other's saved port. The legacy file `unity-mcp-port.json` may still
|
|
8
|
+
exist.
|
|
9
|
+
- This module now scans for both patterns, prefers the most recently
|
|
10
|
+
modified file, and verifies that the port is actually a MCP for Unity listener
|
|
11
|
+
(quick socket connect + ping) before choosing it.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import glob
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
import socket
|
|
21
|
+
import struct
|
|
22
|
+
|
|
23
|
+
from models.models import UnityInstanceInfo
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger("mcp-for-unity-server")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PortDiscovery:
|
|
29
|
+
"""Handles port discovery from Unity Bridge registry"""
|
|
30
|
+
REGISTRY_FILE = "unity-mcp-port.json" # legacy single-project file
|
|
31
|
+
DEFAULT_PORT = 6400
|
|
32
|
+
CONNECT_TIMEOUT = 0.3 # seconds, keep this snappy during discovery
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def get_registry_path() -> Path:
|
|
36
|
+
"""Get the path to the port registry file"""
|
|
37
|
+
return PortDiscovery.get_registry_dir() / PortDiscovery.REGISTRY_FILE
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def get_registry_dir() -> Path:
|
|
41
|
+
env_dir = os.environ.get("UNITY_MCP_STATUS_DIR")
|
|
42
|
+
if env_dir:
|
|
43
|
+
return Path(env_dir)
|
|
44
|
+
return Path.home() / ".unity-mcp"
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def list_candidate_files() -> list[Path]:
|
|
48
|
+
"""Return candidate registry files, newest first.
|
|
49
|
+
Includes hashed per-project files and the legacy file (if present).
|
|
50
|
+
"""
|
|
51
|
+
base = PortDiscovery.get_registry_dir()
|
|
52
|
+
hashed = sorted(
|
|
53
|
+
(Path(p) for p in glob.glob(str(base / "unity-mcp-port-*.json"))),
|
|
54
|
+
key=lambda p: p.stat().st_mtime,
|
|
55
|
+
reverse=True,
|
|
56
|
+
)
|
|
57
|
+
legacy = PortDiscovery.get_registry_path()
|
|
58
|
+
if legacy.exists():
|
|
59
|
+
# Put legacy at the end so hashed, per-project files win
|
|
60
|
+
hashed.append(legacy)
|
|
61
|
+
return hashed
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def _try_probe_unity_mcp(port: int) -> bool:
|
|
65
|
+
"""Quickly check if a MCP for Unity listener is on this port.
|
|
66
|
+
Uses Unity's framed protocol: receives handshake, sends framed ping, expects framed pong.
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
with socket.create_connection(("127.0.0.1", port), PortDiscovery.CONNECT_TIMEOUT) as s:
|
|
70
|
+
s.settimeout(PortDiscovery.CONNECT_TIMEOUT)
|
|
71
|
+
try:
|
|
72
|
+
# 1. Receive handshake from Unity
|
|
73
|
+
handshake = s.recv(512)
|
|
74
|
+
if not handshake or b"FRAMING=1" not in handshake:
|
|
75
|
+
# Try legacy mode as fallback
|
|
76
|
+
s.sendall(b"ping")
|
|
77
|
+
data = s.recv(512)
|
|
78
|
+
return data and b'"message":"pong"' in data
|
|
79
|
+
|
|
80
|
+
# 2. Send framed ping command
|
|
81
|
+
# Frame format: 8-byte length header (big-endian uint64) + payload
|
|
82
|
+
payload = b"ping"
|
|
83
|
+
header = struct.pack('>Q', len(payload))
|
|
84
|
+
s.sendall(header + payload)
|
|
85
|
+
|
|
86
|
+
# 3. Receive framed response
|
|
87
|
+
# Helper to receive exact number of bytes
|
|
88
|
+
def _recv_exact(expected: int) -> bytes | None:
|
|
89
|
+
chunks = bytearray()
|
|
90
|
+
while len(chunks) < expected:
|
|
91
|
+
chunk = s.recv(expected - len(chunks))
|
|
92
|
+
if not chunk:
|
|
93
|
+
return None
|
|
94
|
+
chunks.extend(chunk)
|
|
95
|
+
return bytes(chunks)
|
|
96
|
+
|
|
97
|
+
response_header = _recv_exact(8)
|
|
98
|
+
if response_header is None:
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
response_length = struct.unpack('>Q', response_header)[0]
|
|
102
|
+
if response_length > 10000: # Sanity check
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
response = _recv_exact(response_length)
|
|
106
|
+
if response is None:
|
|
107
|
+
return False
|
|
108
|
+
return b'"message":"pong"' in response
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.debug(f"Port probe failed for {port}: {e}")
|
|
111
|
+
return False
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.debug(f"Connection failed for port {port}: {e}")
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def _read_latest_status() -> dict | None:
|
|
118
|
+
try:
|
|
119
|
+
base = PortDiscovery.get_registry_dir()
|
|
120
|
+
status_files = sorted(
|
|
121
|
+
(Path(p)
|
|
122
|
+
for p in glob.glob(str(base / "unity-mcp-status-*.json"))),
|
|
123
|
+
key=lambda p: p.stat().st_mtime,
|
|
124
|
+
reverse=True,
|
|
125
|
+
)
|
|
126
|
+
if not status_files:
|
|
127
|
+
return None
|
|
128
|
+
with status_files[0].open('r') as f:
|
|
129
|
+
return json.load(f)
|
|
130
|
+
except Exception:
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def discover_unity_port() -> int:
|
|
135
|
+
"""
|
|
136
|
+
Discover Unity port by scanning per-project and legacy registry files.
|
|
137
|
+
Prefer the newest file whose port responds; fall back to first parsed
|
|
138
|
+
value; finally default to 6400.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Port number to connect to
|
|
142
|
+
"""
|
|
143
|
+
# Prefer the latest heartbeat status if it points to a responsive port
|
|
144
|
+
status = PortDiscovery._read_latest_status()
|
|
145
|
+
if status:
|
|
146
|
+
port = status.get('unity_port')
|
|
147
|
+
if isinstance(port, int) and PortDiscovery._try_probe_unity_mcp(port):
|
|
148
|
+
logger.info(f"Using Unity port from status: {port}")
|
|
149
|
+
return port
|
|
150
|
+
|
|
151
|
+
candidates = PortDiscovery.list_candidate_files()
|
|
152
|
+
|
|
153
|
+
first_seen_port: int | None = None
|
|
154
|
+
|
|
155
|
+
for path in candidates:
|
|
156
|
+
try:
|
|
157
|
+
with open(path, 'r') as f:
|
|
158
|
+
cfg = json.load(f)
|
|
159
|
+
unity_port = cfg.get('unity_port')
|
|
160
|
+
if isinstance(unity_port, int):
|
|
161
|
+
if first_seen_port is None:
|
|
162
|
+
first_seen_port = unity_port
|
|
163
|
+
if PortDiscovery._try_probe_unity_mcp(unity_port):
|
|
164
|
+
logger.info(
|
|
165
|
+
f"Using Unity port from {path.name}: {unity_port}")
|
|
166
|
+
return unity_port
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.warning(f"Could not read port registry {path}: {e}")
|
|
169
|
+
|
|
170
|
+
if first_seen_port is not None:
|
|
171
|
+
logger.info(
|
|
172
|
+
f"No responsive port found; using first seen value {first_seen_port}")
|
|
173
|
+
return first_seen_port
|
|
174
|
+
|
|
175
|
+
# Fallback to default port
|
|
176
|
+
logger.info(
|
|
177
|
+
f"No port registry found; using default port {PortDiscovery.DEFAULT_PORT}")
|
|
178
|
+
return PortDiscovery.DEFAULT_PORT
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def get_port_config() -> dict | None:
|
|
182
|
+
"""
|
|
183
|
+
Get the most relevant port configuration from registry.
|
|
184
|
+
Returns the most recent hashed file's config if present,
|
|
185
|
+
otherwise the legacy file's config. Returns None if nothing exists.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Port configuration dict or None if not found
|
|
189
|
+
"""
|
|
190
|
+
candidates = PortDiscovery.list_candidate_files()
|
|
191
|
+
if not candidates:
|
|
192
|
+
return None
|
|
193
|
+
for path in candidates:
|
|
194
|
+
try:
|
|
195
|
+
with open(path, 'r') as f:
|
|
196
|
+
return json.load(f)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
logger.warning(
|
|
199
|
+
f"Could not read port configuration {path}: {e}")
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
def _extract_project_name(project_path: str) -> str:
|
|
204
|
+
"""Extract project name from Assets path.
|
|
205
|
+
|
|
206
|
+
Examples:
|
|
207
|
+
/Users/sakura/Projects/MyGame/Assets -> MyGame
|
|
208
|
+
C:\\Projects\\TestProject\\Assets -> TestProject
|
|
209
|
+
"""
|
|
210
|
+
if not project_path:
|
|
211
|
+
return "Unknown"
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
# Remove trailing /Assets or \Assets
|
|
215
|
+
path = project_path.rstrip('/\\')
|
|
216
|
+
if path.endswith('Assets'):
|
|
217
|
+
path = path[:-6].rstrip('/\\')
|
|
218
|
+
|
|
219
|
+
# Get the last directory name
|
|
220
|
+
name = os.path.basename(path)
|
|
221
|
+
return name if name else "Unknown"
|
|
222
|
+
except Exception:
|
|
223
|
+
return "Unknown"
|
|
224
|
+
|
|
225
|
+
@staticmethod
|
|
226
|
+
def discover_all_unity_instances() -> list[UnityInstanceInfo]:
|
|
227
|
+
"""
|
|
228
|
+
Discover all running Unity Editor instances by scanning status files.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
List of UnityInstanceInfo objects for all discovered instances
|
|
232
|
+
"""
|
|
233
|
+
instances_by_port: dict[int, tuple[UnityInstanceInfo, datetime]] = {}
|
|
234
|
+
base = PortDiscovery.get_registry_dir()
|
|
235
|
+
|
|
236
|
+
# Scan all status files
|
|
237
|
+
status_pattern = str(base / "unity-mcp-status-*.json")
|
|
238
|
+
status_files = glob.glob(status_pattern)
|
|
239
|
+
|
|
240
|
+
for status_file_path in status_files:
|
|
241
|
+
try:
|
|
242
|
+
status_path = Path(status_file_path)
|
|
243
|
+
file_mtime = datetime.fromtimestamp(
|
|
244
|
+
status_path.stat().st_mtime)
|
|
245
|
+
|
|
246
|
+
with status_path.open('r') as f:
|
|
247
|
+
data = json.load(f)
|
|
248
|
+
|
|
249
|
+
# Extract hash from filename: unity-mcp-status-{hash}.json
|
|
250
|
+
filename = os.path.basename(status_file_path)
|
|
251
|
+
hash_value = filename.replace(
|
|
252
|
+
'unity-mcp-status-', '').replace('.json', '')
|
|
253
|
+
|
|
254
|
+
# Extract information
|
|
255
|
+
project_path = data.get('project_path', '')
|
|
256
|
+
project_name = PortDiscovery._extract_project_name(
|
|
257
|
+
project_path)
|
|
258
|
+
port = data.get('unity_port')
|
|
259
|
+
is_reloading = data.get('reloading', False)
|
|
260
|
+
|
|
261
|
+
# Parse last_heartbeat
|
|
262
|
+
last_heartbeat = None
|
|
263
|
+
heartbeat_str = data.get('last_heartbeat')
|
|
264
|
+
if heartbeat_str:
|
|
265
|
+
try:
|
|
266
|
+
last_heartbeat = datetime.fromisoformat(
|
|
267
|
+
heartbeat_str.replace('Z', '+00:00'))
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
# Verify port is actually responding
|
|
272
|
+
is_alive = PortDiscovery._try_probe_unity_mcp(
|
|
273
|
+
port) if isinstance(port, int) else False
|
|
274
|
+
|
|
275
|
+
if not is_alive:
|
|
276
|
+
# If Unity says it's reloading and the status is fresh, don't drop the instance.
|
|
277
|
+
freshness = last_heartbeat or file_mtime
|
|
278
|
+
now = datetime.now()
|
|
279
|
+
if freshness.tzinfo:
|
|
280
|
+
from datetime import timezone
|
|
281
|
+
now = datetime.now(timezone.utc)
|
|
282
|
+
|
|
283
|
+
age_s = (now - freshness).total_seconds()
|
|
284
|
+
|
|
285
|
+
if is_reloading and age_s < 60:
|
|
286
|
+
pass # keep it, status="reloading"
|
|
287
|
+
else:
|
|
288
|
+
logger.debug(
|
|
289
|
+
f"Instance {project_name}@{hash_value} has heartbeat but port {port} not responding")
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
freshness = last_heartbeat or file_mtime
|
|
293
|
+
|
|
294
|
+
existing = instances_by_port.get(port)
|
|
295
|
+
if existing:
|
|
296
|
+
_, existing_time = existing
|
|
297
|
+
if existing_time >= freshness:
|
|
298
|
+
logger.debug(
|
|
299
|
+
f"Skipping stale status entry {status_path.name} in favor of more recent data for port {port}")
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
# Create instance info
|
|
303
|
+
instance = UnityInstanceInfo(
|
|
304
|
+
id=f"{project_name}@{hash_value}",
|
|
305
|
+
name=project_name,
|
|
306
|
+
path=project_path,
|
|
307
|
+
hash=hash_value,
|
|
308
|
+
port=port,
|
|
309
|
+
status="reloading" if is_reloading else "running",
|
|
310
|
+
last_heartbeat=last_heartbeat,
|
|
311
|
+
# May not be available in current version
|
|
312
|
+
unity_version=data.get('unity_version')
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
instances_by_port[port] = (instance, freshness)
|
|
316
|
+
logger.debug(
|
|
317
|
+
f"Discovered Unity instance: {instance.id} on port {instance.port}")
|
|
318
|
+
|
|
319
|
+
except Exception as e:
|
|
320
|
+
logger.debug(
|
|
321
|
+
f"Failed to parse status file {status_file_path}: {e}")
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
deduped_instances = [entry[0] for entry in sorted(
|
|
325
|
+
instances_by_port.values(), key=lambda item: item[1], reverse=True)]
|
|
326
|
+
|
|
327
|
+
logger.info(
|
|
328
|
+
f"Discovered {len(deduped_instances)} Unity instances (after de-duplication by port)")
|
|
329
|
+
return deduped_instances
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Caching registry for STDIO Unity instance discovery."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from core.config import config
|
|
10
|
+
from models.models import UnityInstanceInfo
|
|
11
|
+
from transport.legacy.port_discovery import PortDiscovery
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("mcp-for-unity-server")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StdioPortRegistry:
|
|
17
|
+
"""Caches Unity instance discovery results for STDIO transport."""
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
self._lock = threading.RLock()
|
|
21
|
+
self._instances: dict[str, UnityInstanceInfo] = {}
|
|
22
|
+
self._last_refresh: float = 0.0
|
|
23
|
+
|
|
24
|
+
def _refresh_locked(self) -> None:
|
|
25
|
+
instances = PortDiscovery.discover_all_unity_instances()
|
|
26
|
+
self._instances = {inst.id: inst for inst in instances}
|
|
27
|
+
self._last_refresh = time.time()
|
|
28
|
+
logger.debug(
|
|
29
|
+
f"STDIO port registry refreshed with {len(instances)} instance(s)")
|
|
30
|
+
|
|
31
|
+
def get_instances(self, *, force_refresh: bool = False) -> list[UnityInstanceInfo]:
|
|
32
|
+
ttl = getattr(config, "port_registry_ttl", 5.0)
|
|
33
|
+
with self._lock:
|
|
34
|
+
now = time.time()
|
|
35
|
+
if not force_refresh and self._instances and (now - self._last_refresh) < ttl:
|
|
36
|
+
return list(self._instances.values())
|
|
37
|
+
self._refresh_locked()
|
|
38
|
+
return list(self._instances.values())
|
|
39
|
+
|
|
40
|
+
def get_instance(self, instance_id: str | None) -> UnityInstanceInfo | None:
|
|
41
|
+
instances = self.get_instances()
|
|
42
|
+
if instance_id:
|
|
43
|
+
return next((inst for inst in instances if inst.id == instance_id), None)
|
|
44
|
+
if not instances:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
def _instance_sort_key(inst: UnityInstanceInfo) -> tuple[float, int]:
|
|
48
|
+
heartbeat = inst.last_heartbeat.timestamp() if inst.last_heartbeat else 0.0
|
|
49
|
+
return heartbeat, inst.port or 0
|
|
50
|
+
|
|
51
|
+
return max(instances, key=_instance_sort_key)
|
|
52
|
+
|
|
53
|
+
def get_port(self, instance_id: str | None = None) -> int:
|
|
54
|
+
instance = self.get_instance(instance_id)
|
|
55
|
+
if instance and isinstance(instance.port, int):
|
|
56
|
+
return instance.port
|
|
57
|
+
return PortDiscovery.discover_unity_port()
|
|
58
|
+
|
|
59
|
+
def clear(self) -> None:
|
|
60
|
+
with self._lock:
|
|
61
|
+
self._instances.clear()
|
|
62
|
+
self._last_refresh = 0.0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
stdio_port_registry = StdioPortRegistry()
|