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.
@@ -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),
@@ -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
- raw = await PluginHub.send_command_for_instance(
95
- unity_instance,
96
- command_type,
97
- params,
98
- )
99
- return normalize_unity_response(raw)
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)