mcpforunityserver 9.4.0b20260203025228__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 +254 -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 +67 -0
- core/constants.py +4 -0
- core/logging_decorator.py +37 -0
- core/telemetry.py +551 -0
- core/telemetry_decorator.py +164 -0
- main.py +845 -0
- mcpforunityserver-9.4.0b20260203025228.dist-info/METADATA +328 -0
- mcpforunityserver-9.4.0b20260203025228.dist-info/RECORD +105 -0
- mcpforunityserver-9.4.0b20260203025228.dist-info/WHEEL +5 -0
- mcpforunityserver-9.4.0b20260203025228.dist-info/entry_points.txt +3 -0
- mcpforunityserver-9.4.0b20260203025228.dist-info/licenses/LICENSE +21 -0
- mcpforunityserver-9.4.0b20260203025228.dist-info/top_level.txt +7 -0
- models/__init__.py +4 -0
- models/models.py +56 -0
- models/unity_response.py +70 -0
- services/__init__.py +0 -0
- services/api_key_service.py +235 -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 +48 -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 +30 -0
- services/resources/menu_items.py +35 -0
- services/resources/prefab.py +191 -0
- services/resources/prefab_stage.py +40 -0
- services/resources/project_info.py +40 -0
- services/resources/selection.py +56 -0
- services/resources/tags.py +31 -0
- services/resources/tests.py +88 -0
- services/resources/unity_instances.py +125 -0
- services/resources/windows.py +48 -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 +209 -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 +120 -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 +910 -0
- transport/models.py +68 -0
- transport/plugin_hub.py +787 -0
- transport/plugin_registry.py +182 -0
- transport/unity_instance_middleware.py +262 -0
- transport/unity_transport.py +94 -0
- utils/focus_nudge.py +589 -0
- utils/module_discovery.py +55 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""API Key validation service for remote-hosted mode."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("mcp-for-unity-server")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ValidationResult:
|
|
18
|
+
"""Result of an API key validation."""
|
|
19
|
+
valid: bool
|
|
20
|
+
user_id: str | None = None
|
|
21
|
+
metadata: dict[str, Any] | None = None
|
|
22
|
+
error: str | None = None
|
|
23
|
+
cacheable: bool = True
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ApiKeyService:
|
|
27
|
+
"""Service for validating API keys against an external auth endpoint.
|
|
28
|
+
|
|
29
|
+
Follows the class-level singleton pattern for global access by MCP tools.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
_instance: "ApiKeyService | None" = None
|
|
33
|
+
|
|
34
|
+
# Request defaults (sensible hardening)
|
|
35
|
+
REQUEST_TIMEOUT: float = 5.0
|
|
36
|
+
MAX_RETRIES: int = 1
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
validation_url: str,
|
|
41
|
+
cache_ttl: float = 300.0,
|
|
42
|
+
service_token_header: str | None = None,
|
|
43
|
+
service_token: str | None = None,
|
|
44
|
+
):
|
|
45
|
+
"""Initialize the API key service.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
validation_url: External URL to validate API keys (POST with {"api_key": "..."})
|
|
49
|
+
cache_ttl: Cache TTL for validated keys in seconds (default: 300)
|
|
50
|
+
service_token_header: Optional header name for service authentication (e.g. "X-Service-Token")
|
|
51
|
+
service_token: Optional token value for service authentication
|
|
52
|
+
"""
|
|
53
|
+
self._validation_url = validation_url
|
|
54
|
+
self._cache_ttl = cache_ttl
|
|
55
|
+
self._service_token_header = service_token_header
|
|
56
|
+
self._service_token = service_token
|
|
57
|
+
# Cache: api_key -> (valid, user_id, metadata, expires_at)
|
|
58
|
+
self._cache: dict[str, tuple[bool, str |
|
|
59
|
+
None, dict[str, Any] | None, float]] = {}
|
|
60
|
+
self._cache_lock = asyncio.Lock()
|
|
61
|
+
ApiKeyService._instance = self
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def get_instance(cls) -> "ApiKeyService":
|
|
65
|
+
"""Get the singleton instance.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
RuntimeError: If the service has not been initialized.
|
|
69
|
+
"""
|
|
70
|
+
if cls._instance is None:
|
|
71
|
+
raise RuntimeError("ApiKeyService not initialized")
|
|
72
|
+
return cls._instance
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def is_initialized(cls) -> bool:
|
|
76
|
+
"""Check if the service has been initialized."""
|
|
77
|
+
return cls._instance is not None
|
|
78
|
+
|
|
79
|
+
async def validate(self, api_key: str) -> ValidationResult:
|
|
80
|
+
"""Validate an API key.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
ValidationResult with valid=True and user_id if valid,
|
|
84
|
+
or valid=False with error message if invalid.
|
|
85
|
+
"""
|
|
86
|
+
if not api_key:
|
|
87
|
+
return ValidationResult(valid=False, error="API key required")
|
|
88
|
+
|
|
89
|
+
# Check cache first
|
|
90
|
+
async with self._cache_lock:
|
|
91
|
+
cached = self._cache.get(api_key)
|
|
92
|
+
if cached is not None:
|
|
93
|
+
valid, user_id, metadata, expires_at = cached
|
|
94
|
+
if time.time() < expires_at:
|
|
95
|
+
if valid:
|
|
96
|
+
return ValidationResult(valid=True, user_id=user_id, metadata=metadata)
|
|
97
|
+
else:
|
|
98
|
+
return ValidationResult(valid=False, error="Invalid API key")
|
|
99
|
+
else:
|
|
100
|
+
# Expired, remove from cache
|
|
101
|
+
del self._cache[api_key]
|
|
102
|
+
|
|
103
|
+
# Call external validation URL
|
|
104
|
+
result = await self._validate_external(api_key)
|
|
105
|
+
|
|
106
|
+
# Only cache definitive results (valid keys and confirmed-invalid keys).
|
|
107
|
+
# Transient failures (auth service unavailable, timeouts, etc.) should
|
|
108
|
+
# not be cached to avoid locking out users during service outages.
|
|
109
|
+
if result.cacheable:
|
|
110
|
+
async with self._cache_lock:
|
|
111
|
+
expires_at = time.time() + self._cache_ttl
|
|
112
|
+
self._cache[api_key] = (
|
|
113
|
+
result.valid,
|
|
114
|
+
result.user_id,
|
|
115
|
+
result.metadata,
|
|
116
|
+
expires_at,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return result
|
|
120
|
+
|
|
121
|
+
async def _validate_external(self, api_key: str) -> ValidationResult:
|
|
122
|
+
"""Call external validation endpoint.
|
|
123
|
+
|
|
124
|
+
Failure mode: fail closed (treat as invalid on errors).
|
|
125
|
+
"""
|
|
126
|
+
# Redact API key from logs
|
|
127
|
+
redacted_key = f"{api_key[:4]}...{api_key[-4:]}" if len(
|
|
128
|
+
api_key) > 8 else "***"
|
|
129
|
+
|
|
130
|
+
for attempt in range(self.MAX_RETRIES + 1):
|
|
131
|
+
try:
|
|
132
|
+
async with httpx.AsyncClient(timeout=self.REQUEST_TIMEOUT) as client:
|
|
133
|
+
# Build request headers
|
|
134
|
+
headers = {"Content-Type": "application/json"}
|
|
135
|
+
if self._service_token_header and self._service_token:
|
|
136
|
+
headers[self._service_token_header] = self._service_token
|
|
137
|
+
|
|
138
|
+
response = await client.post(
|
|
139
|
+
self._validation_url,
|
|
140
|
+
json={"api_key": api_key},
|
|
141
|
+
headers=headers,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if response.status_code == 200:
|
|
145
|
+
data = response.json()
|
|
146
|
+
if data.get("valid"):
|
|
147
|
+
return ValidationResult(
|
|
148
|
+
valid=True,
|
|
149
|
+
user_id=data.get("user_id"),
|
|
150
|
+
metadata=data.get("metadata"),
|
|
151
|
+
)
|
|
152
|
+
else:
|
|
153
|
+
return ValidationResult(
|
|
154
|
+
valid=False,
|
|
155
|
+
error=data.get("error", "Invalid API key"),
|
|
156
|
+
)
|
|
157
|
+
elif response.status_code == 401:
|
|
158
|
+
return ValidationResult(valid=False, error="Invalid API key")
|
|
159
|
+
else:
|
|
160
|
+
logger.warning(
|
|
161
|
+
"API key validation returned status %d for key %s",
|
|
162
|
+
response.status_code,
|
|
163
|
+
redacted_key,
|
|
164
|
+
)
|
|
165
|
+
# Fail closed but don't cache (transient service error)
|
|
166
|
+
return ValidationResult(
|
|
167
|
+
valid=False,
|
|
168
|
+
error=f"Auth service error (status {response.status_code})",
|
|
169
|
+
cacheable=False,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
except httpx.TimeoutException:
|
|
173
|
+
if attempt < self.MAX_RETRIES:
|
|
174
|
+
logger.debug(
|
|
175
|
+
"API key validation timeout for key %s, retrying...",
|
|
176
|
+
redacted_key,
|
|
177
|
+
)
|
|
178
|
+
await asyncio.sleep(0.1 * (attempt + 1))
|
|
179
|
+
continue
|
|
180
|
+
logger.warning(
|
|
181
|
+
"API key validation timeout for key %s after %d attempts",
|
|
182
|
+
redacted_key,
|
|
183
|
+
attempt + 1,
|
|
184
|
+
)
|
|
185
|
+
return ValidationResult(
|
|
186
|
+
valid=False,
|
|
187
|
+
error="Auth service timeout",
|
|
188
|
+
cacheable=False,
|
|
189
|
+
)
|
|
190
|
+
except httpx.RequestError as exc:
|
|
191
|
+
if attempt < self.MAX_RETRIES:
|
|
192
|
+
logger.debug(
|
|
193
|
+
"API key validation request error for key %s: %s, retrying...",
|
|
194
|
+
redacted_key,
|
|
195
|
+
exc,
|
|
196
|
+
)
|
|
197
|
+
await asyncio.sleep(0.1 * (attempt + 1))
|
|
198
|
+
continue
|
|
199
|
+
logger.warning(
|
|
200
|
+
"API key validation request error for key %s: %s",
|
|
201
|
+
redacted_key,
|
|
202
|
+
exc,
|
|
203
|
+
)
|
|
204
|
+
return ValidationResult(
|
|
205
|
+
valid=False,
|
|
206
|
+
error="Auth service unavailable",
|
|
207
|
+
cacheable=False,
|
|
208
|
+
)
|
|
209
|
+
except Exception as exc:
|
|
210
|
+
logger.error(
|
|
211
|
+
"Unexpected error validating API key %s: %s",
|
|
212
|
+
redacted_key,
|
|
213
|
+
exc,
|
|
214
|
+
)
|
|
215
|
+
return ValidationResult(
|
|
216
|
+
valid=False,
|
|
217
|
+
error="Auth service error",
|
|
218
|
+
cacheable=False,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Should not reach here, but fail closed
|
|
222
|
+
return ValidationResult(valid=False, error="Auth service error", cacheable=False)
|
|
223
|
+
|
|
224
|
+
async def invalidate_cache(self, api_key: str) -> None:
|
|
225
|
+
"""Remove an API key from the cache."""
|
|
226
|
+
async with self._cache_lock:
|
|
227
|
+
self._cache.pop(api_key, None)
|
|
228
|
+
|
|
229
|
+
async def clear_cache(self) -> None:
|
|
230
|
+
"""Clear all cached validations."""
|
|
231
|
+
async with self._cache_lock:
|
|
232
|
+
self._cache.clear()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
__all__ = ["ApiKeyService", "ValidationResult"]
|