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.
Files changed (105) hide show
  1. cli/__init__.py +3 -0
  2. cli/commands/__init__.py +3 -0
  3. cli/commands/animation.py +84 -0
  4. cli/commands/asset.py +280 -0
  5. cli/commands/audio.py +125 -0
  6. cli/commands/batch.py +171 -0
  7. cli/commands/code.py +182 -0
  8. cli/commands/component.py +190 -0
  9. cli/commands/editor.py +447 -0
  10. cli/commands/gameobject.py +487 -0
  11. cli/commands/instance.py +93 -0
  12. cli/commands/lighting.py +123 -0
  13. cli/commands/material.py +239 -0
  14. cli/commands/prefab.py +248 -0
  15. cli/commands/scene.py +231 -0
  16. cli/commands/script.py +222 -0
  17. cli/commands/shader.py +226 -0
  18. cli/commands/texture.py +540 -0
  19. cli/commands/tool.py +58 -0
  20. cli/commands/ui.py +258 -0
  21. cli/commands/vfx.py +421 -0
  22. cli/main.py +281 -0
  23. cli/utils/__init__.py +31 -0
  24. cli/utils/config.py +58 -0
  25. cli/utils/confirmation.py +37 -0
  26. cli/utils/connection.py +254 -0
  27. cli/utils/constants.py +23 -0
  28. cli/utils/output.py +195 -0
  29. cli/utils/parsers.py +112 -0
  30. cli/utils/suggestions.py +34 -0
  31. core/__init__.py +0 -0
  32. core/config.py +67 -0
  33. core/constants.py +4 -0
  34. core/logging_decorator.py +37 -0
  35. core/telemetry.py +551 -0
  36. core/telemetry_decorator.py +164 -0
  37. main.py +845 -0
  38. mcpforunityserver-9.4.0b20260203025228.dist-info/METADATA +328 -0
  39. mcpforunityserver-9.4.0b20260203025228.dist-info/RECORD +105 -0
  40. mcpforunityserver-9.4.0b20260203025228.dist-info/WHEEL +5 -0
  41. mcpforunityserver-9.4.0b20260203025228.dist-info/entry_points.txt +3 -0
  42. mcpforunityserver-9.4.0b20260203025228.dist-info/licenses/LICENSE +21 -0
  43. mcpforunityserver-9.4.0b20260203025228.dist-info/top_level.txt +7 -0
  44. models/__init__.py +4 -0
  45. models/models.py +56 -0
  46. models/unity_response.py +70 -0
  47. services/__init__.py +0 -0
  48. services/api_key_service.py +235 -0
  49. services/custom_tool_service.py +499 -0
  50. services/registry/__init__.py +22 -0
  51. services/registry/resource_registry.py +53 -0
  52. services/registry/tool_registry.py +51 -0
  53. services/resources/__init__.py +86 -0
  54. services/resources/active_tool.py +48 -0
  55. services/resources/custom_tools.py +57 -0
  56. services/resources/editor_state.py +304 -0
  57. services/resources/gameobject.py +243 -0
  58. services/resources/layers.py +30 -0
  59. services/resources/menu_items.py +35 -0
  60. services/resources/prefab.py +191 -0
  61. services/resources/prefab_stage.py +40 -0
  62. services/resources/project_info.py +40 -0
  63. services/resources/selection.py +56 -0
  64. services/resources/tags.py +31 -0
  65. services/resources/tests.py +88 -0
  66. services/resources/unity_instances.py +125 -0
  67. services/resources/windows.py +48 -0
  68. services/state/external_changes_scanner.py +245 -0
  69. services/tools/__init__.py +83 -0
  70. services/tools/batch_execute.py +93 -0
  71. services/tools/debug_request_context.py +86 -0
  72. services/tools/execute_custom_tool.py +43 -0
  73. services/tools/execute_menu_item.py +32 -0
  74. services/tools/find_gameobjects.py +110 -0
  75. services/tools/find_in_file.py +181 -0
  76. services/tools/manage_asset.py +119 -0
  77. services/tools/manage_components.py +131 -0
  78. services/tools/manage_editor.py +64 -0
  79. services/tools/manage_gameobject.py +260 -0
  80. services/tools/manage_material.py +111 -0
  81. services/tools/manage_prefabs.py +209 -0
  82. services/tools/manage_scene.py +111 -0
  83. services/tools/manage_script.py +645 -0
  84. services/tools/manage_scriptable_object.py +87 -0
  85. services/tools/manage_shader.py +71 -0
  86. services/tools/manage_texture.py +581 -0
  87. services/tools/manage_vfx.py +120 -0
  88. services/tools/preflight.py +110 -0
  89. services/tools/read_console.py +151 -0
  90. services/tools/refresh_unity.py +153 -0
  91. services/tools/run_tests.py +317 -0
  92. services/tools/script_apply_edits.py +1006 -0
  93. services/tools/set_active_instance.py +120 -0
  94. services/tools/utils.py +348 -0
  95. transport/__init__.py +0 -0
  96. transport/legacy/port_discovery.py +329 -0
  97. transport/legacy/stdio_port_registry.py +65 -0
  98. transport/legacy/unity_connection.py +910 -0
  99. transport/models.py +68 -0
  100. transport/plugin_hub.py +787 -0
  101. transport/plugin_registry.py +182 -0
  102. transport/unity_instance_middleware.py +262 -0
  103. transport/unity_transport.py +94 -0
  104. utils/focus_nudge.py +589 -0
  105. 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"]