mcpforunityserver 8.2.3__py3-none-any.whl → 8.7.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.
- main.py +68 -0
- {mcpforunityserver-8.2.3.dist-info → mcpforunityserver-8.7.0.dist-info}/METADATA +38 -66
- {mcpforunityserver-8.2.3.dist-info → mcpforunityserver-8.7.0.dist-info}/RECORD +24 -18
- services/resources/editor_state.py +10 -1
- services/resources/editor_state_v2.py +270 -0
- services/state/external_changes_scanner.py +246 -0
- services/tools/debug_request_context.py +9 -0
- services/tools/manage_asset.py +46 -25
- services/tools/manage_gameobject.py +20 -1
- services/tools/manage_scene.py +37 -18
- services/tools/manage_scriptable_object.py +75 -0
- services/tools/preflight.py +107 -0
- services/tools/read_console.py +13 -30
- services/tools/refresh_unity.py +90 -0
- services/tools/run_tests.py +32 -20
- services/tools/test_jobs.py +94 -0
- services/tools/utils.py +17 -0
- transport/plugin_hub.py +118 -7
- transport/unity_instance_middleware.py +90 -0
- transport/unity_transport.py +16 -6
- {mcpforunityserver-8.2.3.dist-info → mcpforunityserver-8.7.0.dist-info}/WHEEL +0 -0
- {mcpforunityserver-8.2.3.dist-info → mcpforunityserver-8.7.0.dist-info}/entry_points.txt +0 -0
- {mcpforunityserver-8.2.3.dist-info → mcpforunityserver-8.7.0.dist-info}/licenses/LICENSE +0 -0
- {mcpforunityserver-8.2.3.dist-info → mcpforunityserver-8.7.0.dist-info}/top_level.txt +0 -0
|
@@ -83,11 +83,101 @@ class UnityInstanceMiddleware(Middleware):
|
|
|
83
83
|
with self._lock:
|
|
84
84
|
self._active_by_key.pop(key, None)
|
|
85
85
|
|
|
86
|
+
async def _maybe_autoselect_instance(self, ctx) -> str | None:
|
|
87
|
+
"""
|
|
88
|
+
Auto-select the sole Unity instance when no active instance is set.
|
|
89
|
+
|
|
90
|
+
Note: This method both *discovers* and *persists* the selection via
|
|
91
|
+
`set_active_instance` as a side-effect, since callers expect the selection
|
|
92
|
+
to stick for subsequent tool/resource calls in the same session.
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
# Import here to avoid circular dependencies / optional transport modules.
|
|
96
|
+
from transport.unity_transport import _current_transport
|
|
97
|
+
|
|
98
|
+
transport = _current_transport()
|
|
99
|
+
if PluginHub.is_configured():
|
|
100
|
+
try:
|
|
101
|
+
sessions_data = await PluginHub.get_sessions()
|
|
102
|
+
sessions = sessions_data.sessions or {}
|
|
103
|
+
ids: list[str] = []
|
|
104
|
+
for session_info in sessions.values():
|
|
105
|
+
project = getattr(session_info, "project", None) or "Unknown"
|
|
106
|
+
hash_value = getattr(session_info, "hash", None)
|
|
107
|
+
if hash_value:
|
|
108
|
+
ids.append(f"{project}@{hash_value}")
|
|
109
|
+
if len(ids) == 1:
|
|
110
|
+
chosen = ids[0]
|
|
111
|
+
self.set_active_instance(ctx, chosen)
|
|
112
|
+
logger.info(
|
|
113
|
+
"Auto-selected sole Unity instance via PluginHub: %s",
|
|
114
|
+
chosen,
|
|
115
|
+
)
|
|
116
|
+
return chosen
|
|
117
|
+
except (ConnectionError, ValueError, KeyError, TimeoutError, AttributeError) as exc:
|
|
118
|
+
logger.debug(
|
|
119
|
+
"PluginHub auto-select probe failed (%s); falling back to stdio",
|
|
120
|
+
type(exc).__name__,
|
|
121
|
+
exc_info=True,
|
|
122
|
+
)
|
|
123
|
+
except Exception as exc:
|
|
124
|
+
if isinstance(exc, (SystemExit, KeyboardInterrupt)):
|
|
125
|
+
raise
|
|
126
|
+
logger.debug(
|
|
127
|
+
"PluginHub auto-select probe failed with unexpected error (%s); falling back to stdio",
|
|
128
|
+
type(exc).__name__,
|
|
129
|
+
exc_info=True,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if transport != "http":
|
|
133
|
+
try:
|
|
134
|
+
# Import here to avoid circular imports in legacy transport paths.
|
|
135
|
+
from transport.legacy.unity_connection import get_unity_connection_pool
|
|
136
|
+
|
|
137
|
+
pool = get_unity_connection_pool()
|
|
138
|
+
instances = pool.discover_all_instances(force_refresh=True)
|
|
139
|
+
ids = [getattr(inst, "id", None) for inst in instances]
|
|
140
|
+
ids = [inst_id for inst_id in ids if inst_id]
|
|
141
|
+
if len(ids) == 1:
|
|
142
|
+
chosen = ids[0]
|
|
143
|
+
self.set_active_instance(ctx, chosen)
|
|
144
|
+
logger.info(
|
|
145
|
+
"Auto-selected sole Unity instance via stdio discovery: %s",
|
|
146
|
+
chosen,
|
|
147
|
+
)
|
|
148
|
+
return chosen
|
|
149
|
+
except (ConnectionError, ValueError, KeyError, TimeoutError, AttributeError) as exc:
|
|
150
|
+
logger.debug(
|
|
151
|
+
"Stdio auto-select probe failed (%s)",
|
|
152
|
+
type(exc).__name__,
|
|
153
|
+
exc_info=True,
|
|
154
|
+
)
|
|
155
|
+
except Exception as exc:
|
|
156
|
+
if isinstance(exc, (SystemExit, KeyboardInterrupt)):
|
|
157
|
+
raise
|
|
158
|
+
logger.debug(
|
|
159
|
+
"Stdio auto-select probe failed with unexpected error (%s)",
|
|
160
|
+
type(exc).__name__,
|
|
161
|
+
exc_info=True,
|
|
162
|
+
)
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
if isinstance(exc, (SystemExit, KeyboardInterrupt)):
|
|
165
|
+
raise
|
|
166
|
+
logger.debug(
|
|
167
|
+
"Auto-select path encountered an unexpected error (%s)",
|
|
168
|
+
type(exc).__name__,
|
|
169
|
+
exc_info=True,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return None
|
|
173
|
+
|
|
86
174
|
async def _inject_unity_instance(self, context: MiddlewareContext) -> None:
|
|
87
175
|
"""Inject active Unity instance into context if available."""
|
|
88
176
|
ctx = context.fastmcp_context
|
|
89
177
|
|
|
90
178
|
active_instance = self.get_active_instance(ctx)
|
|
179
|
+
if not active_instance:
|
|
180
|
+
active_instance = await self._maybe_autoselect_instance(ctx)
|
|
91
181
|
if active_instance:
|
|
92
182
|
# If using HTTP transport (PluginHub configured), validate session
|
|
93
183
|
# But for stdio transport (no PluginHub needed or maybe partially configured),
|
transport/unity_transport.py
CHANGED
|
@@ -9,6 +9,7 @@ from typing import Awaitable, Callable, TypeVar
|
|
|
9
9
|
from fastmcp import Context
|
|
10
10
|
|
|
11
11
|
from transport.plugin_hub import PluginHub
|
|
12
|
+
from models.models import MCPResponse
|
|
12
13
|
from models.unity_response import normalize_unity_response
|
|
13
14
|
from services.tools import get_unity_instance_from_context
|
|
14
15
|
|
|
@@ -91,12 +92,21 @@ async def send_with_unity_instance(
|
|
|
91
92
|
if not isinstance(params, dict):
|
|
92
93
|
raise TypeError(
|
|
93
94
|
"Command parameters must be a dict for HTTP transport")
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
try:
|
|
96
|
+
raw = await PluginHub.send_command_for_instance(
|
|
97
|
+
unity_instance,
|
|
98
|
+
command_type,
|
|
99
|
+
params,
|
|
100
|
+
)
|
|
101
|
+
return normalize_unity_response(raw)
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
# NOTE: asyncio.TimeoutError has an empty str() by default, which is confusing for clients.
|
|
104
|
+
err = str(exc) or f"{type(exc).__name__}"
|
|
105
|
+
# Fail fast with a retry hint instead of hanging for COMMAND_TIMEOUT.
|
|
106
|
+
# The client can decide whether retrying is appropriate for the command.
|
|
107
|
+
return normalize_unity_response(
|
|
108
|
+
MCPResponse(success=False, error=err, hint="retry").model_dump()
|
|
109
|
+
)
|
|
100
110
|
|
|
101
111
|
if unity_instance:
|
|
102
112
|
kwargs.setdefault("instance_id", unity_instance)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|