mcpforunityserver 9.3.0b20260129104751__py3-none-any.whl → 9.3.0b20260131003150__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/utils/connection.py +28 -32
- core/config.py +15 -0
- core/constants.py +4 -0
- main.py +306 -174
- {mcpforunityserver-9.3.0b20260129104751.dist-info → mcpforunityserver-9.3.0b20260131003150.dist-info}/METADATA +117 -5
- {mcpforunityserver-9.3.0b20260129104751.dist-info → mcpforunityserver-9.3.0b20260131003150.dist-info}/RECORD +30 -28
- models/__init__.py +2 -2
- models/unity_response.py +24 -1
- services/api_key_service.py +235 -0
- services/resources/active_tool.py +2 -1
- services/resources/editor_state.py +7 -7
- services/resources/layers.py +2 -1
- services/resources/menu_items.py +2 -1
- services/resources/prefab_stage.py +2 -1
- services/resources/project_info.py +2 -1
- services/resources/selection.py +2 -1
- services/resources/tags.py +2 -1
- services/resources/tests.py +3 -2
- services/resources/unity_instances.py +6 -3
- services/resources/windows.py +2 -1
- services/tools/manage_prefabs.py +35 -0
- services/tools/set_active_instance.py +6 -3
- transport/plugin_hub.py +124 -24
- transport/plugin_registry.py +75 -19
- transport/unity_instance_middleware.py +38 -9
- transport/unity_transport.py +41 -10
- {mcpforunityserver-9.3.0b20260129104751.dist-info → mcpforunityserver-9.3.0b20260131003150.dist-info}/WHEEL +0 -0
- {mcpforunityserver-9.3.0b20260129104751.dist-info → mcpforunityserver-9.3.0b20260131003150.dist-info}/entry_points.txt +0 -0
- {mcpforunityserver-9.3.0b20260129104751.dist-info → mcpforunityserver-9.3.0b20260131003150.dist-info}/licenses/LICENSE +0 -0
- {mcpforunityserver-9.3.0b20260129104751.dist-info → mcpforunityserver-9.3.0b20260131003150.dist-info}/top_level.txt +0 -0
|
@@ -9,6 +9,7 @@ import logging
|
|
|
9
9
|
|
|
10
10
|
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
11
11
|
|
|
12
|
+
from core.config import config
|
|
12
13
|
from transport.plugin_hub import PluginHub
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger("mcp-for-unity-server")
|
|
@@ -32,7 +33,12 @@ def get_unity_instance_middleware() -> 'UnityInstanceMiddleware':
|
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
def set_unity_instance_middleware(middleware: 'UnityInstanceMiddleware') -> None:
|
|
35
|
-
"""
|
|
36
|
+
"""Replace the global middleware instance.
|
|
37
|
+
|
|
38
|
+
This is a test seam: production code uses ``get_unity_instance_middleware()``
|
|
39
|
+
which lazy-initialises the singleton. Tests call this function to inject a
|
|
40
|
+
mock or pre-configured middleware before exercising tool/resource code.
|
|
41
|
+
"""
|
|
36
42
|
global _unity_instance_middleware
|
|
37
43
|
_unity_instance_middleware = middleware
|
|
38
44
|
|
|
@@ -55,13 +61,18 @@ class UnityInstanceMiddleware(Middleware):
|
|
|
55
61
|
Derive a stable key for the calling session.
|
|
56
62
|
|
|
57
63
|
Prioritizes client_id for stability.
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
In remote-hosted mode, falls back to user_id for session isolation.
|
|
65
|
+
Otherwise falls back to 'global' (assuming single-user local mode).
|
|
60
66
|
"""
|
|
61
67
|
client_id = getattr(ctx, "client_id", None)
|
|
62
68
|
if isinstance(client_id, str) and client_id:
|
|
63
69
|
return client_id
|
|
64
70
|
|
|
71
|
+
# In remote-hosted mode, use user_id so different users get isolated instance selections
|
|
72
|
+
user_id = ctx.get_state("user_id")
|
|
73
|
+
if isinstance(user_id, str) and user_id:
|
|
74
|
+
return f"user:{user_id}"
|
|
75
|
+
|
|
65
76
|
# Fallback to global for local dev stability
|
|
66
77
|
return "global"
|
|
67
78
|
|
|
@@ -92,10 +103,10 @@ class UnityInstanceMiddleware(Middleware):
|
|
|
92
103
|
to stick for subsequent tool/resource calls in the same session.
|
|
93
104
|
"""
|
|
94
105
|
try:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
106
|
+
transport = (config.transport_mode or "stdio").lower()
|
|
107
|
+
# This implicit behavior works well for solo-users, but is dangerous for multi-user setups
|
|
108
|
+
if transport == "http" and config.http_remote_hosted:
|
|
109
|
+
return None
|
|
99
110
|
if PluginHub.is_configured():
|
|
100
111
|
try:
|
|
101
112
|
sessions_data = await PluginHub.get_sessions()
|
|
@@ -172,10 +183,27 @@ class UnityInstanceMiddleware(Middleware):
|
|
|
172
183
|
|
|
173
184
|
return None
|
|
174
185
|
|
|
186
|
+
async def _resolve_user_id(self) -> str | None:
|
|
187
|
+
"""Extract user_id from the current HTTP request's API key."""
|
|
188
|
+
if not config.http_remote_hosted:
|
|
189
|
+
return None
|
|
190
|
+
# Lazy import to avoid circular dependencies (same pattern as _maybe_autoselect_instance).
|
|
191
|
+
from transport.unity_transport import _resolve_user_id_from_request
|
|
192
|
+
return await _resolve_user_id_from_request()
|
|
193
|
+
|
|
175
194
|
async def _inject_unity_instance(self, context: MiddlewareContext) -> None:
|
|
176
|
-
"""Inject active Unity instance into context if available."""
|
|
195
|
+
"""Inject active Unity instance and user_id into context if available."""
|
|
177
196
|
ctx = context.fastmcp_context
|
|
178
197
|
|
|
198
|
+
# Resolve user_id from the HTTP request's API key header
|
|
199
|
+
user_id = await self._resolve_user_id()
|
|
200
|
+
if config.http_remote_hosted and user_id is None:
|
|
201
|
+
raise RuntimeError(
|
|
202
|
+
"API key authentication required. Provide a valid X-API-Key header."
|
|
203
|
+
)
|
|
204
|
+
if user_id:
|
|
205
|
+
ctx.set_state("user_id", user_id)
|
|
206
|
+
|
|
179
207
|
active_instance = self.get_active_instance(ctx)
|
|
180
208
|
if not active_instance:
|
|
181
209
|
active_instance = await self._maybe_autoselect_instance(ctx)
|
|
@@ -193,7 +221,8 @@ class UnityInstanceMiddleware(Middleware):
|
|
|
193
221
|
# resolving session_id might fail if the plugin disconnected
|
|
194
222
|
# We only need session_id for HTTP transport routing.
|
|
195
223
|
# For stdio, we just need the instance ID.
|
|
196
|
-
|
|
224
|
+
# Pass user_id for remote-hosted mode session isolation
|
|
225
|
+
session_id = await PluginHub._resolve_session_id(active_instance, user_id=user_id)
|
|
197
226
|
except (ConnectionError, ValueError, KeyError, TimeoutError) as exc:
|
|
198
227
|
# If resolution fails, it means the Unity instance is not reachable via HTTP/WS.
|
|
199
228
|
# If we are in stdio mode, this might still be fine if the user is just setting state?
|
transport/unity_transport.py
CHANGED
|
@@ -1,34 +1,49 @@
|
|
|
1
1
|
"""Transport helpers for routing commands to Unity."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
import inspect
|
|
6
|
-
import os
|
|
4
|
+
import logging
|
|
7
5
|
from typing import Awaitable, Callable, TypeVar
|
|
8
6
|
|
|
9
|
-
from fastmcp import Context
|
|
10
|
-
|
|
11
7
|
from transport.plugin_hub import PluginHub
|
|
8
|
+
from core.config import config
|
|
9
|
+
from core.constants import API_KEY_HEADER
|
|
10
|
+
from services.api_key_service import ApiKeyService
|
|
12
11
|
from models.models import MCPResponse
|
|
13
12
|
from models.unity_response import normalize_unity_response
|
|
14
|
-
from services.tools import get_unity_instance_from_context
|
|
15
13
|
|
|
16
14
|
T = TypeVar("T")
|
|
15
|
+
logger = logging.getLogger("mcp-for-unity-server")
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
def _is_http_transport() -> bool:
|
|
20
|
-
return
|
|
19
|
+
return config.transport_mode.lower() == "http"
|
|
21
20
|
|
|
22
21
|
|
|
23
|
-
def
|
|
24
|
-
"""
|
|
25
|
-
|
|
22
|
+
async def _resolve_user_id_from_request() -> str | None:
|
|
23
|
+
"""Extract user_id from the current HTTP request's API key header."""
|
|
24
|
+
if not config.http_remote_hosted:
|
|
25
|
+
return None
|
|
26
|
+
if not ApiKeyService.is_initialized():
|
|
27
|
+
return None
|
|
28
|
+
try:
|
|
29
|
+
from fastmcp.server.dependencies import get_http_headers
|
|
30
|
+
headers = get_http_headers(include_all=True)
|
|
31
|
+
api_key = headers.get(API_KEY_HEADER.lower())
|
|
32
|
+
if not api_key:
|
|
33
|
+
return None
|
|
34
|
+
service = ApiKeyService.get_instance()
|
|
35
|
+
result = await service.validate(api_key)
|
|
36
|
+
return result.user_id if result.valid else None
|
|
37
|
+
except Exception as e:
|
|
38
|
+
logger.debug("Failed to resolve user_id from HTTP request: %s", e)
|
|
39
|
+
return None
|
|
26
40
|
|
|
27
41
|
|
|
28
42
|
async def send_with_unity_instance(
|
|
29
43
|
send_fn: Callable[..., Awaitable[T]],
|
|
30
44
|
unity_instance: str | None,
|
|
31
45
|
*args,
|
|
46
|
+
user_id: str | None = None,
|
|
32
47
|
**kwargs,
|
|
33
48
|
) -> T:
|
|
34
49
|
if _is_http_transport():
|
|
@@ -41,11 +56,27 @@ async def send_with_unity_instance(
|
|
|
41
56
|
if not isinstance(params, dict):
|
|
42
57
|
raise TypeError(
|
|
43
58
|
"Command parameters must be a dict for HTTP transport")
|
|
59
|
+
|
|
60
|
+
# Auto-resolve user_id from HTTP request API key (remote-hosted mode)
|
|
61
|
+
if user_id is None:
|
|
62
|
+
user_id = await _resolve_user_id_from_request()
|
|
63
|
+
|
|
64
|
+
# Auth check
|
|
65
|
+
if config.http_remote_hosted and not user_id:
|
|
66
|
+
return normalize_unity_response(
|
|
67
|
+
MCPResponse(
|
|
68
|
+
success=False,
|
|
69
|
+
error="auth_required",
|
|
70
|
+
message="API key required",
|
|
71
|
+
).model_dump()
|
|
72
|
+
)
|
|
73
|
+
|
|
44
74
|
try:
|
|
45
75
|
raw = await PluginHub.send_command_for_instance(
|
|
46
76
|
unity_instance,
|
|
47
77
|
command_type,
|
|
48
78
|
params,
|
|
79
|
+
user_id=user_id,
|
|
49
80
|
)
|
|
50
81
|
return normalize_unity_response(raw)
|
|
51
82
|
except Exception as exc:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|