mcpforunityserver 9.3.0b20260129121506__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.0b20260129121506.dist-info → mcpforunityserver-9.3.0b20260131003150.dist-info}/METADATA +117 -5
- {mcpforunityserver-9.3.0b20260129121506.dist-info → mcpforunityserver-9.3.0b20260131003150.dist-info}/RECORD +29 -27
- 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/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.0b20260129121506.dist-info → mcpforunityserver-9.3.0b20260131003150.dist-info}/WHEEL +0 -0
- {mcpforunityserver-9.3.0b20260129121506.dist-info → mcpforunityserver-9.3.0b20260131003150.dist-info}/entry_points.txt +0 -0
- {mcpforunityserver-9.3.0b20260129121506.dist-info → mcpforunityserver-9.3.0b20260131003150.dist-info}/licenses/LICENSE +0 -0
- {mcpforunityserver-9.3.0b20260129121506.dist-info → mcpforunityserver-9.3.0b20260131003150.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcpforunityserver
|
|
3
|
-
Version: 9.3.
|
|
3
|
+
Version: 9.3.0b20260131003150
|
|
4
4
|
Summary: MCP for Unity Server: A Unity package for Unity Editor integration via the Model Context Protocol (MCP).
|
|
5
5
|
Author-email: Marcus Sanatan <msanatan@gmail.com>, David Sarno <david.sarno@gmail.com>, Wu Shutong <martinwfire@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -154,12 +154,124 @@ uv run src/main.py --transport stdio
|
|
|
154
154
|
|
|
155
155
|
## Configuration
|
|
156
156
|
|
|
157
|
-
The server connects to Unity Editor automatically when both are running.
|
|
157
|
+
The server connects to Unity Editor automatically when both are running. Most users do not need to change any settings.
|
|
158
|
+
|
|
159
|
+
### CLI options
|
|
160
|
+
|
|
161
|
+
These options apply to the `mcp-for-unity` command (whether run via `uvx`, Docker, or `python src/main.py`).
|
|
162
|
+
|
|
163
|
+
- `--transport {stdio,http}` - Transport protocol (default: `stdio`)
|
|
164
|
+
- `--http-url URL` - Base URL used to derive host/port defaults (default: `http://localhost:8080`)
|
|
165
|
+
- `--http-host HOST` - Override HTTP bind host (overrides URL host)
|
|
166
|
+
- `--http-port PORT` - Override HTTP bind port (overrides URL port)
|
|
167
|
+
- `--http-remote-hosted` - Treat HTTP transport as remotely hosted
|
|
168
|
+
- Requires API key authentication (see below)
|
|
169
|
+
- Disables local/CLI-only HTTP routes (`/api/command`, `/api/instances`, `/api/custom-tools`)
|
|
170
|
+
- Forces explicit Unity instance selection for MCP tool/resource calls
|
|
171
|
+
- Isolates Unity sessions per user
|
|
172
|
+
- `--api-key-validation-url URL` - External endpoint to validate API keys (required when `--http-remote-hosted` is set)
|
|
173
|
+
- `--api-key-login-url URL` - URL where users can obtain/manage API keys (served by `/api/auth/login-url`)
|
|
174
|
+
- `--api-key-cache-ttl SECONDS` - Cache duration for validated keys (default: `300`)
|
|
175
|
+
- `--api-key-service-token-header HEADER` - Header name for server-to-auth-service authentication (e.g. `X-Service-Token`)
|
|
176
|
+
- `--api-key-service-token TOKEN` - Token value sent to the auth service for server authentication
|
|
177
|
+
- `--default-instance INSTANCE` - Default Unity instance to target (project name, hash, or `Name@hash`)
|
|
178
|
+
- `--project-scoped-tools` - Keep custom tools scoped to the active Unity project and enable the custom tools resource
|
|
179
|
+
- `--unity-instance-token TOKEN` - Optional per-launch token set by Unity for deterministic lifecycle management
|
|
180
|
+
- `--pidfile PATH` - Optional path where the server writes its PID on startup (used by Unity-managed terminal launches)
|
|
181
|
+
|
|
182
|
+
### Environment variables
|
|
183
|
+
|
|
184
|
+
- `UNITY_MCP_TRANSPORT` - Transport protocol: `stdio` or `http`
|
|
185
|
+
- `UNITY_MCP_HTTP_URL` - HTTP server URL (default: `http://localhost:8080`)
|
|
186
|
+
- `UNITY_MCP_HTTP_HOST` - HTTP bind host (overrides URL host)
|
|
187
|
+
- `UNITY_MCP_HTTP_PORT` - HTTP bind port (overrides URL port)
|
|
188
|
+
- `UNITY_MCP_HTTP_REMOTE_HOSTED` - Enable remote-hosted mode (`true`, `1`, or `yes`)
|
|
189
|
+
- `UNITY_MCP_DEFAULT_INSTANCE` - Default Unity instance to target (project name, hash, or `Name@hash`)
|
|
190
|
+
- `UNITY_MCP_SKIP_STARTUP_CONNECT=1` - Skip initial Unity connection attempt on startup
|
|
191
|
+
|
|
192
|
+
API key authentication (remote-hosted mode):
|
|
193
|
+
|
|
194
|
+
- `UNITY_MCP_API_KEY_VALIDATION_URL` - External endpoint to validate API keys
|
|
195
|
+
- `UNITY_MCP_API_KEY_LOGIN_URL` - URL where users can obtain/manage API keys
|
|
196
|
+
- `UNITY_MCP_API_KEY_CACHE_TTL` - Cache TTL for validated keys in seconds (default: `300`)
|
|
197
|
+
- `UNITY_MCP_API_KEY_SERVICE_TOKEN_HEADER` - Header name for server-to-auth-service authentication
|
|
198
|
+
- `UNITY_MCP_API_KEY_SERVICE_TOKEN` - Token value sent to the auth service for server authentication
|
|
199
|
+
|
|
200
|
+
Telemetry:
|
|
201
|
+
|
|
202
|
+
- `DISABLE_TELEMETRY=1` - Disable anonymous telemetry (opt-out)
|
|
203
|
+
- `UNITY_MCP_DISABLE_TELEMETRY=1` - Same as `DISABLE_TELEMETRY`
|
|
204
|
+
- `MCP_DISABLE_TELEMETRY=1` - Same as `DISABLE_TELEMETRY`
|
|
205
|
+
- `UNITY_MCP_TELEMETRY_ENDPOINT` - Override telemetry endpoint URL
|
|
206
|
+
- `UNITY_MCP_TELEMETRY_TIMEOUT` - Override telemetry request timeout (seconds)
|
|
207
|
+
|
|
208
|
+
### Examples
|
|
209
|
+
|
|
210
|
+
**Stdio (default):**
|
|
158
211
|
|
|
159
|
-
|
|
212
|
+
```bash
|
|
213
|
+
uvx --from mcpforunityserver mcp-for-unity --transport stdio
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**HTTP (local):**
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
uvx --from mcpforunityserver mcp-for-unity --transport http --http-host 127.0.0.1 --http-port 8080
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**HTTP (remote-hosted with API key auth):**
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
uvx --from mcpforunityserver mcp-for-unity \
|
|
226
|
+
--transport http \
|
|
227
|
+
--http-host 0.0.0.0 \
|
|
228
|
+
--http-port 8080 \
|
|
229
|
+
--http-remote-hosted \
|
|
230
|
+
--api-key-validation-url https://auth.example.com/api/validate-key \
|
|
231
|
+
--api-key-login-url https://app.example.com/api-keys
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Disable telemetry:**
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
DISABLE_TELEMETRY=1 uvx --from mcpforunityserver mcp-for-unity --transport stdio
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Remote-Hosted Mode
|
|
243
|
+
|
|
244
|
+
When deploying the server as a shared remote service (e.g. for a team or Asset Store users), enable `--http-remote-hosted` to activate API key authentication and per-user session isolation.
|
|
245
|
+
|
|
246
|
+
**Requirements:**
|
|
247
|
+
|
|
248
|
+
- An external HTTP endpoint that validates API keys. The server POSTs `{"api_key": "..."}` and expects `{"valid": true, "user_id": "..."}` or `{"valid": false}` in response.
|
|
249
|
+
- `--api-key-validation-url` must be provided (or `UNITY_MCP_API_KEY_VALIDATION_URL`). The server exits with code 1 if this is missing.
|
|
250
|
+
|
|
251
|
+
**What changes in remote-hosted mode:**
|
|
252
|
+
|
|
253
|
+
- All MCP tool/resource calls and Unity plugin WebSocket connections require a valid `X-API-Key` header.
|
|
254
|
+
- Each user only sees Unity instances that connected with their API key (session isolation).
|
|
255
|
+
- Auto-selection of a sole Unity instance is disabled; users must explicitly call `set_active_instance`.
|
|
256
|
+
- CLI REST routes (`/api/command`, `/api/instances`, `/api/custom-tools`) are disabled.
|
|
257
|
+
- `/health` and `/api/auth/login-url` remain accessible without authentication.
|
|
258
|
+
|
|
259
|
+
**MCP client config with API key:**
|
|
260
|
+
|
|
261
|
+
```json
|
|
262
|
+
{
|
|
263
|
+
"mcpServers": {
|
|
264
|
+
"UnityMCP": {
|
|
265
|
+
"url": "http://remote-server:8080/mcp",
|
|
266
|
+
"headers": {
|
|
267
|
+
"X-API-Key": "<your-api-key>"
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
160
273
|
|
|
161
|
-
|
|
162
|
-
- `LOG_LEVEL=DEBUG` - Enable detailed logging (default: INFO)
|
|
274
|
+
For full details, see [Remote Server Auth Guide](../docs/guides/REMOTE_SERVER_AUTH.md) and [Architecture Reference](../docs/reference/REMOTE_SERVER_AUTH_ARCHITECTURE.md).
|
|
163
275
|
|
|
164
276
|
---
|
|
165
277
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
main.py,sha256=
|
|
1
|
+
main.py,sha256=xNuZrgxhsjVOYl1C-NBzpW-eb8YW3_m_E2Npz9a5vao,35040
|
|
2
2
|
cli/__init__.py,sha256=f2HjXqR9d8Uhibru211t9HPpdrb_1vdDC2v_NwF_eqA,63
|
|
3
3
|
cli/main.py,sha256=V_VFa8tA-CDHNv9J5NzNSLxRuEGjRVZWDe4xn6rYdog,8457
|
|
4
4
|
cli/commands/__init__.py,sha256=xQHf6o0afDV2HsU9gwSxjcrzS41cMCSGZyWYWxblPIk,69
|
|
@@ -24,40 +24,42 @@ cli/commands/vfx.py,sha256=gdx5a_N7Ulu960xjaJIV0E_1Ii422C211KaipoRO6nQ,14453
|
|
|
24
24
|
cli/utils/__init__.py,sha256=Gbm9hYC7UqwloFwdirXgo6z1iBktR9Y96o3bQcrYudc,613
|
|
25
25
|
cli/utils/config.py,sha256=_k3XAFmXG22sv8tYIb5JmO46kNl3T1sGqFptySAayfc,1550
|
|
26
26
|
cli/utils/confirmation.py,sha256=7NGu0I5ogowpdWRTUndn3g5nmWmJM9mV3e0wWMLJwA8,1234
|
|
27
|
-
cli/utils/connection.py,sha256=
|
|
27
|
+
cli/utils/connection.py,sha256=fx2o3J9AI6HdSC6rRDKh6KO8sPmBWkzR-ElY1dDkhes,8123
|
|
28
28
|
cli/utils/constants.py,sha256=xCyRMY1L3cc-sbCyl-TGyqkY5hMCOl5tU6L4ZVbN9w4,1046
|
|
29
29
|
cli/utils/output.py,sha256=96daU55ta_hl7UeOhNh5Iy7OJ4psbdR9Nfx1-q2k3xA,6370
|
|
30
30
|
cli/utils/parsers.py,sha256=mnpH2bhZn3L3Lyl8e7Sh7Zr2UW9Xzu3aUuCaLxpV2pk,3430
|
|
31
31
|
cli/utils/suggestions.py,sha256=n6KG3Mrvub28X9rPFYFLRTtZ6HePp3PhhAeojG2WOJw,929
|
|
32
32
|
core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
|
-
core/config.py,sha256=
|
|
33
|
+
core/config.py,sha256=VlCLUFPm73DfInCVZkJFq4_7KZKVqSFbDS-n0HMA8WA,2118
|
|
34
|
+
core/constants.py,sha256=PhZs926Nie7Muigju1xkVBynVaUktZlP9lJMHgu4_to,114
|
|
34
35
|
core/logging_decorator.py,sha256=D9CD7rFvQz-MBG-G4inizQj0Ivr6dfc9RBmTrw7q8mI,1383
|
|
35
36
|
core/telemetry.py,sha256=zIjmQKUNW0S822SSlkXyjjCIuX0ZpSTaZP4pAU0rCjw,20426
|
|
36
37
|
core/telemetry_decorator.py,sha256=ycSTrzVNCDQHSd-xmIWOpVfKFURPxpiZe_XkOQAGDAo,6705
|
|
37
|
-
mcpforunityserver-9.3.
|
|
38
|
-
models/__init__.py,sha256=
|
|
38
|
+
mcpforunityserver-9.3.0b20260131003150.dist-info/licenses/LICENSE,sha256=bv5lDJZQEqxBgjjc1rkRbkEwpSIHF-8N-1Od0VnEJFw,1066
|
|
39
|
+
models/__init__.py,sha256=J2ozraI5aDkqLb53KvXjTDaWgh_xVFxpWrcRMekOwPk,231
|
|
39
40
|
models/models.py,sha256=heXuvdBtdats1SGwW8wKFFHM0qR4hA6A7qETn5s9BZ0,1827
|
|
40
|
-
models/unity_response.py,sha256=
|
|
41
|
+
models/unity_response.py,sha256=xvgJhJ3pS8-ATQhnU5owijgO4141UW59f5dQm1ohVew,2278
|
|
41
42
|
services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
|
+
services/api_key_service.py,sha256=Wq8BJDHXurN0hSiotqI37ta7G-Q9QAxHCD5hHbokxYs,8755
|
|
42
44
|
services/custom_tool_service.py,sha256=WJxljL-hdJE5GMlAhVimHVhQwwnWHCd0StgWhWEFgaI,18592
|
|
43
45
|
services/registry/__init__.py,sha256=QCwcYThvGF0kBt3WR6DBskdyxkegJC7NymEChgJA-YM,470
|
|
44
46
|
services/registry/resource_registry.py,sha256=T_Kznqgvt5kKgV7mU85nb0LlFuB4rg-Tm4Cjhxt-IcI,1467
|
|
45
47
|
services/registry/tool_registry.py,sha256=9tMwOP07JE92QFYUS4KvoysO0qC9pkBD5B79kjRsSPw,1304
|
|
46
48
|
services/resources/__init__.py,sha256=G8uSEYJtiyX3yg0QsfoeGdDXOdbU89l5m0B5Anay1Fc,3054
|
|
47
|
-
services/resources/active_tool.py,sha256=
|
|
49
|
+
services/resources/active_tool.py,sha256=O2tKtId1-UCZXFAVigIN4WzuiihmyZe24sCGn1k16HA,1558
|
|
48
50
|
services/resources/custom_tools.py,sha256=uujlJEuqv5svCvSZLILgiY6hiiWZqWHVzTBJpTTL3as,1751
|
|
49
|
-
services/resources/editor_state.py,sha256=
|
|
51
|
+
services/resources/editor_state.py,sha256=OEPdb7VFFzk6LGN1r5FouBFv4sQdfKs-8sMKZxXCHwA,11004
|
|
50
52
|
services/resources/gameobject.py,sha256=cviektt_GHmwgria5pkvNrzvD5-6hzBi64Ogm0YGIv8,9356
|
|
51
|
-
services/resources/layers.py,sha256=
|
|
52
|
-
services/resources/menu_items.py,sha256=
|
|
53
|
+
services/resources/layers.py,sha256=6CjGfHH2XqLX6FYAoBLgePZH3srVO-9psZV8TJ93Okw,1160
|
|
54
|
+
services/resources/menu_items.py,sha256=tNyHlqC9mFkllN3JW9fyouecTvnV4YbnlSAysbAjVbQ,1091
|
|
53
55
|
services/resources/prefab.py,sha256=4TLEBsrlnQdi5FOKWZZ9eesiUCG5Ryxh7FZvd02FcTc,7434
|
|
54
|
-
services/resources/prefab_stage.py,sha256=
|
|
55
|
-
services/resources/project_info.py,sha256=
|
|
56
|
-
services/resources/selection.py,sha256=
|
|
57
|
-
services/resources/tags.py,sha256=
|
|
58
|
-
services/resources/tests.py,sha256=
|
|
59
|
-
services/resources/unity_instances.py,sha256=
|
|
60
|
-
services/resources/windows.py,sha256=
|
|
56
|
+
services/resources/prefab_stage.py,sha256=APVPbzxSO4vUR0YgYDi4WI_woywqoc0KcTyUwgbGmJQ,1467
|
|
57
|
+
services/resources/project_info.py,sha256=xGSFTxOs4y7ok7iO3XVdGhxxyjAXpg1czbapybqp-NI,1401
|
|
58
|
+
services/resources/selection.py,sha256=E2thI4vWqk2SE3gm6mcDvYWSG0UuRpC1tja2Y6J2BRc,1921
|
|
59
|
+
services/resources/tags.py,sha256=fROK2Bj9VBiyQSQt0jdXxYatX-3d4vtAH8i6Zhgj5Vw,1127
|
|
60
|
+
services/resources/tests.py,sha256=6aqhZWxO6YPMGBzaw_dxMUm1wCJMFRNLeu6Nf_wXieo,3474
|
|
61
|
+
services/resources/unity_instances.py,sha256=Ix6YtTWKnTLXZ4v4mrn_d0IhLr7gcjfe9d605zf2V5w,4564
|
|
62
|
+
services/resources/windows.py,sha256=UT8-CDstsBXyggzJ8A9pVFIqNr8jr9oP77rOEBsWQCk,1507
|
|
61
63
|
services/state/external_changes_scanner.py,sha256=ZiXu8ZcK5B-hv7CaJLmnEIa9JxzgOBpdmrsRDY2eK5I,9052
|
|
62
64
|
services/tools/__init__.py,sha256=J-woLzm3aLF0uPC1-VroqG7QV9xLXHDmVYee2ZXuCgk,2746
|
|
63
65
|
services/tools/batch_execute.py,sha256=hjh67kgWvQDHyGd2N-Tfezv9WAj5x_pWTt_Vybmmq7s,3501
|
|
@@ -83,21 +85,21 @@ services/tools/read_console.py,sha256=ps23debJcQkj3Ap-MqTYVhopYnKGspJs9QHLJHZAAk
|
|
|
83
85
|
services/tools/refresh_unity.py,sha256=KrRA8bmLkDLFO1XBv2NmagQAp1dmyaXdUAap567Hcv4,7100
|
|
84
86
|
services/tools/run_tests.py,sha256=Wd7hNZqy-OOZ9ZedonxUJ5bVlhta_aOEmD-2uxmrmyM,11743
|
|
85
87
|
services/tools/script_apply_edits.py,sha256=0f-SaP5NUYGuivl4CWHjR8F-CXUpt3-5qkHpf_edn1U,47677
|
|
86
|
-
services/tools/set_active_instance.py,sha256=
|
|
88
|
+
services/tools/set_active_instance.py,sha256=j_d0GhFwhBh-HwoNKzrice1haNyWPdK-RtVrNasYH7c,4408
|
|
87
89
|
services/tools/utils.py,sha256=ETCiNnWdMZEtnJcDD-CtPsCJ7TBp5x5sPsYuhufkxac,13962
|
|
88
90
|
transport/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
89
91
|
transport/models.py,sha256=2tnK0Wc-IzvlOS0a04VB3WtuSlAAHeLVFfOnNFHGnhw,1311
|
|
90
|
-
transport/plugin_hub.py,sha256=
|
|
91
|
-
transport/plugin_registry.py,sha256=
|
|
92
|
-
transport/unity_instance_middleware.py,sha256=
|
|
93
|
-
transport/unity_transport.py,sha256=
|
|
92
|
+
transport/plugin_hub.py,sha256=Xe0Vn1Df7Ks4KmRs3_waheUWuaQnT_DMLELA1tCnqHs,28449
|
|
93
|
+
transport/plugin_registry.py,sha256=Zn2QaDbpDj9Ad-oXs3q7CXxnZz3mc5SJ61GzIToEBPI,7160
|
|
94
|
+
transport/unity_instance_middleware.py,sha256=vdbX28eU6ddxEiPdgtv275Q1qcIJzuyYxQnNE-KXXnc,11837
|
|
95
|
+
transport/unity_transport.py,sha256=MK7eG3JU5zEGF2w5ntIBqxCvLy7cv-E5tlymqcQC8UA,3319
|
|
94
96
|
transport/legacy/port_discovery.py,sha256=JDSCqXLodfTT7fOsE0DFC1jJ3QsU6hVaYQb7x7FgdxY,12728
|
|
95
97
|
transport/legacy/stdio_port_registry.py,sha256=j4iARuP6wetppNDG8qKeuvo1bJKcSlgEhZvSyl_uf0A,2313
|
|
96
98
|
transport/legacy/unity_connection.py,sha256=FE9ZQfYMhHvIxBycr_DjI3BKvuEdORXuABnCE5Q2tjQ,36733
|
|
97
99
|
utils/focus_nudge.py,sha256=0MCOms-SxUW7sN2hT3syy1epMdli2zc-6UHBICAfBSM,21330
|
|
98
100
|
utils/module_discovery.py,sha256=My48ofB1BUqxiBoAZAGbEaLQYdsrDhMm8MayBP_bUSQ,2005
|
|
99
|
-
mcpforunityserver-9.3.
|
|
100
|
-
mcpforunityserver-9.3.
|
|
101
|
-
mcpforunityserver-9.3.
|
|
102
|
-
mcpforunityserver-9.3.
|
|
103
|
-
mcpforunityserver-9.3.
|
|
101
|
+
mcpforunityserver-9.3.0b20260131003150.dist-info/METADATA,sha256=9JlYrKIHsH0CAaGJUXtHikB_kUs9rWYjmJoPDYmvlkQ,11877
|
|
102
|
+
mcpforunityserver-9.3.0b20260131003150.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
103
|
+
mcpforunityserver-9.3.0b20260131003150.dist-info/entry_points.txt,sha256=pPm70RXQvkt3uBhPOtViDa47ZTA03RaQ6rwXvyi8oiI,70
|
|
104
|
+
mcpforunityserver-9.3.0b20260131003150.dist-info/top_level.txt,sha256=3-A65WsmBO6UZYH8O5mINdyhhZ63SDssr8LncRd1PSQ,46
|
|
105
|
+
mcpforunityserver-9.3.0b20260131003150.dist-info/RECORD,,
|
models/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
from .models import MCPResponse, UnityInstanceInfo
|
|
2
|
-
from .unity_response import normalize_unity_response
|
|
2
|
+
from .unity_response import normalize_unity_response, parse_resource_response
|
|
3
3
|
|
|
4
|
-
__all__ = ['MCPResponse', 'UnityInstanceInfo', 'normalize_unity_response']
|
|
4
|
+
__all__ = ['MCPResponse', 'UnityInstanceInfo', 'normalize_unity_response', 'parse_resource_response']
|
models/unity_response.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""Utilities for normalizing Unity transport responses."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any, Type
|
|
5
|
+
|
|
6
|
+
from models.models import MCPResponse
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
def normalize_unity_response(response: Any) -> Any:
|
|
@@ -45,3 +47,24 @@ def normalize_unity_response(response: Any) -> Any:
|
|
|
45
47
|
normalized["error"] = message or "Unity command failed"
|
|
46
48
|
|
|
47
49
|
return normalized
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_resource_response(response: Any, typed_cls: Type[MCPResponse]) -> MCPResponse:
|
|
53
|
+
"""Parse a Unity response into a typed response class.
|
|
54
|
+
|
|
55
|
+
Returns a base ``MCPResponse`` for error responses so that typed subclasses
|
|
56
|
+
with strict ``data`` fields (e.g. ``list[str]``) don't raise Pydantic
|
|
57
|
+
validation errors when ``data`` is ``None``.
|
|
58
|
+
"""
|
|
59
|
+
if not isinstance(response, dict):
|
|
60
|
+
return response
|
|
61
|
+
|
|
62
|
+
# Detect errors from both normalized (success=False) and raw (status="error") shapes.
|
|
63
|
+
if response.get("success") is False or response.get("status") == "error":
|
|
64
|
+
return MCPResponse(
|
|
65
|
+
success=False,
|
|
66
|
+
error=response.get("error"),
|
|
67
|
+
message=response.get("message"),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return typed_cls(**response)
|
|
@@ -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"]
|
|
@@ -2,6 +2,7 @@ from pydantic import BaseModel
|
|
|
2
2
|
from fastmcp import Context
|
|
3
3
|
|
|
4
4
|
from models import MCPResponse
|
|
5
|
+
from models.unity_response import parse_resource_response
|
|
5
6
|
from services.registry import mcp_for_unity_resource
|
|
6
7
|
from services.tools import get_unity_instance_from_context
|
|
7
8
|
from transport.unity_transport import send_with_unity_instance
|
|
@@ -44,4 +45,4 @@ async def get_active_tool(ctx: Context) -> ActiveToolResponse | MCPResponse:
|
|
|
44
45
|
"get_active_tool",
|
|
45
46
|
{}
|
|
46
47
|
)
|
|
47
|
-
return
|
|
48
|
+
return parse_resource_response(response, ActiveToolResponse)
|
|
@@ -5,12 +5,14 @@ from typing import Any
|
|
|
5
5
|
from fastmcp import Context
|
|
6
6
|
from pydantic import BaseModel
|
|
7
7
|
|
|
8
|
+
from core.config import config
|
|
8
9
|
from models import MCPResponse
|
|
9
10
|
from services.registry import mcp_for_unity_resource
|
|
10
11
|
from services.tools import get_unity_instance_from_context
|
|
11
12
|
from services.state.external_changes_scanner import external_changes_scanner
|
|
12
13
|
import transport.unity_transport as unity_transport
|
|
13
14
|
from transport.legacy.unity_connection import async_send_command_with_retry
|
|
15
|
+
from transport.plugin_hub import PluginHub
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
class EditorStateUnity(BaseModel):
|
|
@@ -132,17 +134,15 @@ async def infer_single_instance_id(ctx: Context) -> str | None:
|
|
|
132
134
|
"""
|
|
133
135
|
await ctx.info("If exactly one Unity instance is connected, return its Name@hash id.")
|
|
134
136
|
|
|
135
|
-
|
|
136
|
-
transport = unity_transport._current_transport()
|
|
137
|
-
except Exception:
|
|
138
|
-
transport = None
|
|
137
|
+
transport = (config.transport_mode or "stdio").lower()
|
|
139
138
|
|
|
140
139
|
if transport == "http":
|
|
141
140
|
# HTTP/WebSocket transport: derive from PluginHub sessions.
|
|
142
141
|
try:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
# In remote-hosted mode, filter sessions by user_id
|
|
143
|
+
user_id = ctx.get_state(
|
|
144
|
+
"user_id") if config.http_remote_hosted else None
|
|
145
|
+
sessions_data = await PluginHub.get_sessions(user_id=user_id)
|
|
146
146
|
sessions = sessions_data.sessions if hasattr(
|
|
147
147
|
sessions_data, "sessions") else {}
|
|
148
148
|
if isinstance(sessions, dict) and len(sessions) == 1:
|
services/resources/layers.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from fastmcp import Context
|
|
2
2
|
|
|
3
3
|
from models import MCPResponse
|
|
4
|
+
from models.unity_response import parse_resource_response
|
|
4
5
|
from services.registry import mcp_for_unity_resource
|
|
5
6
|
from services.tools import get_unity_instance_from_context
|
|
6
7
|
from transport.unity_transport import send_with_unity_instance
|
|
@@ -26,4 +27,4 @@ async def get_layers(ctx: Context) -> LayersResponse | MCPResponse:
|
|
|
26
27
|
"get_layers",
|
|
27
28
|
{}
|
|
28
29
|
)
|
|
29
|
-
return
|
|
30
|
+
return parse_resource_response(response, LayersResponse)
|
services/resources/menu_items.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from fastmcp import Context
|
|
2
2
|
|
|
3
3
|
from models import MCPResponse
|
|
4
|
+
from models.unity_response import parse_resource_response
|
|
4
5
|
from services.registry import mcp_for_unity_resource
|
|
5
6
|
from services.tools import get_unity_instance_from_context
|
|
6
7
|
from transport.unity_transport import send_with_unity_instance
|
|
@@ -31,4 +32,4 @@ async def get_menu_items(ctx: Context) -> GetMenuItemsResponse | MCPResponse:
|
|
|
31
32
|
"get_menu_items",
|
|
32
33
|
params,
|
|
33
34
|
)
|
|
34
|
-
return
|
|
35
|
+
return parse_resource_response(response, GetMenuItemsResponse)
|
|
@@ -2,6 +2,7 @@ from pydantic import BaseModel
|
|
|
2
2
|
from fastmcp import Context
|
|
3
3
|
|
|
4
4
|
from models import MCPResponse
|
|
5
|
+
from models.unity_response import parse_resource_response
|
|
5
6
|
from services.registry import mcp_for_unity_resource
|
|
6
7
|
from services.tools import get_unity_instance_from_context
|
|
7
8
|
from transport.unity_transport import send_with_unity_instance
|
|
@@ -36,4 +37,4 @@ async def get_prefab_stage(ctx: Context) -> PrefabStageResponse | MCPResponse:
|
|
|
36
37
|
"get_prefab_stage",
|
|
37
38
|
{}
|
|
38
39
|
)
|
|
39
|
-
return
|
|
40
|
+
return parse_resource_response(response, PrefabStageResponse)
|
|
@@ -2,6 +2,7 @@ from pydantic import BaseModel
|
|
|
2
2
|
from fastmcp import Context
|
|
3
3
|
|
|
4
4
|
from models import MCPResponse
|
|
5
|
+
from models.unity_response import parse_resource_response
|
|
5
6
|
from services.registry import mcp_for_unity_resource
|
|
6
7
|
from services.tools import get_unity_instance_from_context
|
|
7
8
|
from transport.unity_transport import send_with_unity_instance
|
|
@@ -36,4 +37,4 @@ async def get_project_info(ctx: Context) -> ProjectInfoResponse | MCPResponse:
|
|
|
36
37
|
"get_project_info",
|
|
37
38
|
{}
|
|
38
39
|
)
|
|
39
|
-
return
|
|
40
|
+
return parse_resource_response(response, ProjectInfoResponse)
|
services/resources/selection.py
CHANGED
|
@@ -2,6 +2,7 @@ from pydantic import BaseModel
|
|
|
2
2
|
from fastmcp import Context
|
|
3
3
|
|
|
4
4
|
from models import MCPResponse
|
|
5
|
+
from models.unity_response import parse_resource_response
|
|
5
6
|
from services.registry import mcp_for_unity_resource
|
|
6
7
|
from services.tools import get_unity_instance_from_context
|
|
7
8
|
from transport.unity_transport import send_with_unity_instance
|
|
@@ -52,4 +53,4 @@ async def get_selection(ctx: Context) -> SelectionResponse | MCPResponse:
|
|
|
52
53
|
"get_selection",
|
|
53
54
|
{}
|
|
54
55
|
)
|
|
55
|
-
return
|
|
56
|
+
return parse_resource_response(response, SelectionResponse)
|
services/resources/tags.py
CHANGED
|
@@ -2,6 +2,7 @@ from pydantic import Field
|
|
|
2
2
|
from fastmcp import Context
|
|
3
3
|
|
|
4
4
|
from models import MCPResponse
|
|
5
|
+
from models.unity_response import parse_resource_response
|
|
5
6
|
from services.registry import mcp_for_unity_resource
|
|
6
7
|
from services.tools import get_unity_instance_from_context
|
|
7
8
|
from transport.unity_transport import send_with_unity_instance
|
|
@@ -27,4 +28,4 @@ async def get_tags(ctx: Context) -> TagsResponse | MCPResponse:
|
|
|
27
28
|
"get_tags",
|
|
28
29
|
{}
|
|
29
30
|
)
|
|
30
|
-
return
|
|
31
|
+
return parse_resource_response(response, TagsResponse)
|