mcpforunityserver 9.3.0b20260128055651__py3-none-any.whl → 9.3.0b20260129121506__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 (61) hide show
  1. cli/commands/animation.py +6 -9
  2. cli/commands/asset.py +50 -80
  3. cli/commands/audio.py +14 -22
  4. cli/commands/batch.py +20 -33
  5. cli/commands/code.py +63 -70
  6. cli/commands/component.py +33 -55
  7. cli/commands/editor.py +122 -188
  8. cli/commands/gameobject.py +60 -83
  9. cli/commands/instance.py +28 -36
  10. cli/commands/lighting.py +54 -59
  11. cli/commands/material.py +39 -68
  12. cli/commands/prefab.py +63 -81
  13. cli/commands/scene.py +30 -54
  14. cli/commands/script.py +32 -50
  15. cli/commands/shader.py +43 -55
  16. cli/commands/texture.py +53 -51
  17. cli/commands/tool.py +24 -27
  18. cli/commands/ui.py +125 -130
  19. cli/commands/vfx.py +84 -138
  20. cli/utils/confirmation.py +37 -0
  21. cli/utils/connection.py +32 -2
  22. cli/utils/constants.py +23 -0
  23. cli/utils/parsers.py +112 -0
  24. core/config.py +0 -4
  25. core/telemetry.py +20 -2
  26. {mcpforunityserver-9.3.0b20260128055651.dist-info → mcpforunityserver-9.3.0b20260129121506.dist-info}/METADATA +21 -1
  27. mcpforunityserver-9.3.0b20260129121506.dist-info/RECORD +103 -0
  28. services/resources/active_tool.py +1 -1
  29. services/resources/custom_tools.py +1 -1
  30. services/resources/editor_state.py +1 -1
  31. services/resources/gameobject.py +4 -4
  32. services/resources/layers.py +1 -1
  33. services/resources/menu_items.py +1 -1
  34. services/resources/prefab.py +3 -3
  35. services/resources/prefab_stage.py +1 -1
  36. services/resources/project_info.py +1 -1
  37. services/resources/selection.py +1 -1
  38. services/resources/tags.py +1 -1
  39. services/resources/tests.py +40 -8
  40. services/resources/unity_instances.py +1 -1
  41. services/resources/windows.py +1 -1
  42. services/tools/__init__.py +3 -1
  43. services/tools/find_gameobjects.py +32 -11
  44. services/tools/manage_gameobject.py +11 -66
  45. services/tools/manage_material.py +4 -37
  46. services/tools/manage_prefabs.py +51 -7
  47. services/tools/manage_script.py +1 -1
  48. services/tools/manage_texture.py +10 -96
  49. services/tools/run_tests.py +67 -4
  50. services/tools/utils.py +217 -0
  51. transport/models.py +1 -0
  52. transport/plugin_hub.py +2 -1
  53. transport/plugin_registry.py +3 -0
  54. transport/unity_transport.py +0 -51
  55. utils/focus_nudge.py +291 -23
  56. mcpforunityserver-9.3.0b20260128055651.dist-info/RECORD +0 -101
  57. utils/reload_sentinel.py +0 -9
  58. {mcpforunityserver-9.3.0b20260128055651.dist-info → mcpforunityserver-9.3.0b20260129121506.dist-info}/WHEEL +0 -0
  59. {mcpforunityserver-9.3.0b20260128055651.dist-info → mcpforunityserver-9.3.0b20260129121506.dist-info}/entry_points.txt +0 -0
  60. {mcpforunityserver-9.3.0b20260128055651.dist-info → mcpforunityserver-9.3.0b20260129121506.dist-info}/licenses/LICENSE +0 -0
  61. {mcpforunityserver-9.3.0b20260128055651.dist-info → mcpforunityserver-9.3.0b20260129121506.dist-info}/top_level.txt +0 -0
core/telemetry.py CHANGED
@@ -249,6 +249,7 @@ class TelemetryCollector:
249
249
  self._lock: threading.Lock = threading.Lock()
250
250
  # Bounded queue with single background worker (records only; no context propagation)
251
251
  self._queue: "queue.Queue[TelemetryRecord]" = queue.Queue(maxsize=1000)
252
+ self._shutdown: bool = False
252
253
  # Load persistent data before starting worker so first events have UUID
253
254
  self._load_persistent_data()
254
255
  self._worker: threading.Thread = threading.Thread(
@@ -349,8 +350,11 @@ class TelemetryCollector:
349
350
 
350
351
  def _worker_loop(self):
351
352
  """Background worker that serializes telemetry sends."""
352
- while True:
353
- rec = self._queue.get()
353
+ while not self._shutdown:
354
+ try:
355
+ rec = self._queue.get(timeout=0.5)
356
+ except queue.Empty:
357
+ continue
354
358
  try:
355
359
  # Run sender directly; do not reuse caller context/thread-locals
356
360
  self._send_telemetry(rec)
@@ -360,6 +364,12 @@ class TelemetryCollector:
360
364
  with contextlib.suppress(Exception):
361
365
  self._queue.task_done()
362
366
 
367
+ def shutdown(self):
368
+ """Shutdown the telemetry collector and worker thread."""
369
+ self._shutdown = True
370
+ if self._worker and self._worker.is_alive():
371
+ self._worker.join(timeout=2.0)
372
+
363
373
  def _send_telemetry(self, record: TelemetryRecord):
364
374
  """Send telemetry data to endpoint"""
365
375
  try:
@@ -440,6 +450,14 @@ def get_telemetry() -> TelemetryCollector:
440
450
  return _telemetry_collector
441
451
 
442
452
 
453
+ def reset_telemetry():
454
+ """Reset the global telemetry collector. For testing only."""
455
+ global _telemetry_collector
456
+ if _telemetry_collector is not None:
457
+ _telemetry_collector.shutdown()
458
+ _telemetry_collector = None
459
+
460
+
443
461
  def record_telemetry(record_type: RecordType,
444
462
  data: dict[str, Any],
445
463
  milestone: MilestoneType | None = None):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcpforunityserver
3
- Version: 9.3.0b20260128055651
3
+ Version: 9.3.0b20260129121506
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
@@ -163,6 +163,26 @@ The server connects to Unity Editor automatically when both are running. No addi
163
163
 
164
164
  ---
165
165
 
166
+ ## MCP Resources
167
+
168
+ The server provides read-only MCP resources for querying Unity Editor state. Resources provide up-to-date information about your Unity project without modifying it.
169
+
170
+ **Accessing Resources:**
171
+
172
+ Resources are accessed by their URI (not their name). Always use `ListMcpResources` to get the correct URI format.
173
+
174
+ **Example URIs:**
175
+ - `mcpforunity://editor/state` - Editor readiness snapshot
176
+ - `mcpforunity://project/tags` - All project tags
177
+ - `mcpforunity://scene/gameobject/{instance_id}` - GameObject details by ID
178
+ - `mcpforunity://prefab/{encoded_path}` - Prefab info by asset path
179
+
180
+ **Important:** Resource names use underscores (e.g., `editor_state`) but URIs use slashes/hyphens (e.g., `mcpforunity://editor/state`). Always use the URI from `ListMcpResources()` when reading resources.
181
+
182
+ **All resource descriptions now include their URI** for easy reference. List available resources to see the complete catalog with URIs.
183
+
184
+ ---
185
+
166
186
  ## Example Prompts
167
187
 
168
188
  Once connected, try these commands in your AI assistant:
@@ -0,0 +1,103 @@
1
+ main.py,sha256=EoHA0upWjtQzuoOgN5BfNmGL6bIVnFQjRW5PHO5EmjY,29265
2
+ cli/__init__.py,sha256=f2HjXqR9d8Uhibru211t9HPpdrb_1vdDC2v_NwF_eqA,63
3
+ cli/main.py,sha256=V_VFa8tA-CDHNv9J5NzNSLxRuEGjRVZWDe4xn6rYdog,8457
4
+ cli/commands/__init__.py,sha256=xQHf6o0afDV2HsU9gwSxjcrzS41cMCSGZyWYWxblPIk,69
5
+ cli/commands/animation.py,sha256=a7ZqTkP7CRou9wL7MB8b3TSB7v0PFG_1LcjdxW5_zQ0,2388
6
+ cli/commands/asset.py,sha256=Cm18PFLGLisTLRhycTbxaGT8XU3WnJWcPOwsWiB5E2s,7164
7
+ cli/commands/audio.py,sha256=gTW4F6uutgQezAwDqL6_-i8vM588V-AVwxAaJ4LzGN8,3088
8
+ cli/commands/batch.py,sha256=eo2DD62FzRPDk0mQ5WhFVAXS0qXf8_znT_5kdN0BoG4,5388
9
+ cli/commands/code.py,sha256=N6MaBjH7QkYjjCP4wEhAoAQSwvboh2ub3rrI6qYv8AM,5226
10
+ cli/commands/component.py,sha256=OqjuNOyA2n5UOy-0-vMeiIRCQHOq_OnhVqW4ZzMgqrE,5736
11
+ cli/commands/editor.py,sha256=F_KOfQiE-uuwGHXZQ9o9BF5nURG1nKcWuiL-9hjAFHc,12704
12
+ cli/commands/gameobject.py,sha256=D4h8xuMHjOoc_mdyZToNSzYCGrJKjeOpwrMWCXR0OCU,12881
13
+ cli/commands/instance.py,sha256=5qYJtptOFGCzfhXEMIPoEYcJtpkKlfM76dq36i_ZK5s,2780
14
+ cli/commands/lighting.py,sha256=4OpbzPvR8pesTTUVYmKV554SwbTyCVBRwDwTGlnmh2w,3558
15
+ cli/commands/material.py,sha256=_9atAdD0Wo_mEpZ2FP9Opl54ZBi2ak96euydHD_xGa4,6999
16
+ cli/commands/prefab.py,sha256=ef8mFF5pcBDH8X3zWANuD4Hq23sT3gK1mAXsHSbP1iM,7480
17
+ cli/commands/scene.py,sha256=HRDOcOUMpS6RhqL3FNF4JphjseCZFhTuDrJ0l-MOSRc,5652
18
+ cli/commands/script.py,sha256=70i3I6IuKWq5f49QvHIpTzC9fafYmJrWfynJl4ZAAMU,5966
19
+ cli/commands/shader.py,sha256=w_n8g6GJcFmqqZMulPdJGbvWWOirbfzHr6_vUx_2nVE,5985
20
+ cli/commands/texture.py,sha256=lLiPNMfcWNNcfHbyeNA1ZN16tKv1ljHkzhmr7SM5e50,18595
21
+ cli/commands/tool.py,sha256=A-jC9qFAs-LkWY0rav-vrye0yytgfKxQ7y2FraIAzZ4,1605
22
+ cli/commands/ui.py,sha256=u9rpeF_eT7qxdlYmaA2G4ARSe3a16Y4yt-3if7CKmxY,7225
23
+ cli/commands/vfx.py,sha256=gdx5a_N7Ulu960xjaJIV0E_1Ii422C211KaipoRO6nQ,14453
24
+ cli/utils/__init__.py,sha256=Gbm9hYC7UqwloFwdirXgo6z1iBktR9Y96o3bQcrYudc,613
25
+ cli/utils/config.py,sha256=_k3XAFmXG22sv8tYIb5JmO46kNl3T1sGqFptySAayfc,1550
26
+ cli/utils/confirmation.py,sha256=7NGu0I5ogowpdWRTUndn3g5nmWmJM9mV3e0wWMLJwA8,1234
27
+ cli/utils/connection.py,sha256=RBSOK7WotKgokj-wewsqnKT5rwS0_DhsqXqZrFZWUXo,8534
28
+ cli/utils/constants.py,sha256=xCyRMY1L3cc-sbCyl-TGyqkY5hMCOl5tU6L4ZVbN9w4,1046
29
+ cli/utils/output.py,sha256=96daU55ta_hl7UeOhNh5Iy7OJ4psbdR9Nfx1-q2k3xA,6370
30
+ cli/utils/parsers.py,sha256=mnpH2bhZn3L3Lyl8e7Sh7Zr2UW9Xzu3aUuCaLxpV2pk,3430
31
+ cli/utils/suggestions.py,sha256=n6KG3Mrvub28X9rPFYFLRTtZ6HePp3PhhAeojG2WOJw,929
32
+ core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ core/config.py,sha256=kE7yg_89QQck3zScWNjvrq7CnfYt6uLS0VdxK7AJ70o,1453
34
+ core/logging_decorator.py,sha256=D9CD7rFvQz-MBG-G4inizQj0Ivr6dfc9RBmTrw7q8mI,1383
35
+ core/telemetry.py,sha256=zIjmQKUNW0S822SSlkXyjjCIuX0ZpSTaZP4pAU0rCjw,20426
36
+ core/telemetry_decorator.py,sha256=ycSTrzVNCDQHSd-xmIWOpVfKFURPxpiZe_XkOQAGDAo,6705
37
+ mcpforunityserver-9.3.0b20260129121506.dist-info/licenses/LICENSE,sha256=bv5lDJZQEqxBgjjc1rkRbkEwpSIHF-8N-1Od0VnEJFw,1066
38
+ models/__init__.py,sha256=JlscZkGWE9TRmSoBi99v_LSl8OAFNGmr8463PYkXin4,179
39
+ models/models.py,sha256=heXuvdBtdats1SGwW8wKFFHM0qR4hA6A7qETn5s9BZ0,1827
40
+ models/unity_response.py,sha256=oJ1PTsnNc5VBC-9OgM59C0C-R9N-GdmEdmz_yph4GSU,1454
41
+ services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
+ services/custom_tool_service.py,sha256=WJxljL-hdJE5GMlAhVimHVhQwwnWHCd0StgWhWEFgaI,18592
43
+ services/registry/__init__.py,sha256=QCwcYThvGF0kBt3WR6DBskdyxkegJC7NymEChgJA-YM,470
44
+ services/registry/resource_registry.py,sha256=T_Kznqgvt5kKgV7mU85nb0LlFuB4rg-Tm4Cjhxt-IcI,1467
45
+ services/registry/tool_registry.py,sha256=9tMwOP07JE92QFYUS4KvoysO0qC9pkBD5B79kjRsSPw,1304
46
+ services/resources/__init__.py,sha256=G8uSEYJtiyX3yg0QsfoeGdDXOdbU89l5m0B5Anay1Fc,3054
47
+ services/resources/active_tool.py,sha256=6m65iPCD1Iqmp4xqpDKvPErfOJg7Gl3XU5vMyAVcdj8,1521
48
+ services/resources/custom_tools.py,sha256=uujlJEuqv5svCvSZLILgiY6hiiWZqWHVzTBJpTTL3as,1751
49
+ services/resources/editor_state.py,sha256=LZLmljdHQC7nQcA-YYtoRnpy1xDiiAzo4_iU69TgsH8,10858
50
+ services/resources/gameobject.py,sha256=cviektt_GHmwgria5pkvNrzvD5-6hzBi64Ogm0YGIv8,9356
51
+ services/resources/layers.py,sha256=e3bDhfJ0ZjtUagHhraI35GrzrDnzXrRHnDeobmnbniw,1123
52
+ services/resources/menu_items.py,sha256=01LIVHA96bswYGXyLZzGFw3suDqftsBanhy_lkUB2H0,1054
53
+ services/resources/prefab.py,sha256=4TLEBsrlnQdi5FOKWZZ9eesiUCG5Ryxh7FZvd02FcTc,7434
54
+ services/resources/prefab_stage.py,sha256=YALZrfZh4zpV_tEfQI7sUR6ETLnz8qeTN--gwYxk_y8,1430
55
+ services/resources/project_info.py,sha256=NaHb9v5n-2vi3woMxioMXbgpy1tBcmh2bh0RZOTxtC4,1364
56
+ services/resources/selection.py,sha256=HxmpN0RAb6Op0r4kqqRsizahP5L3Hj8V1baRa3uaKpU,1884
57
+ services/resources/tags.py,sha256=f50DzILLNQsUJ44ooomwIGasrcntbKJgT7boqrU4b5s,1090
58
+ services/resources/tests.py,sha256=KOm1vFcDPAXi21rDRQ1hQmifs1T9Xnd0zUp-BcBrBxo,3458
59
+ services/resources/unity_instances.py,sha256=8vLA5oR2VpeLRjuxHk-_J3eopLhuNpGEoJyCvyUAOXg,4386
60
+ services/resources/windows.py,sha256=inD7zMB3R4Tip_92pCDtKGD7clzsGKOGkdmv4rKmygA,1470
61
+ services/state/external_changes_scanner.py,sha256=ZiXu8ZcK5B-hv7CaJLmnEIa9JxzgOBpdmrsRDY2eK5I,9052
62
+ services/tools/__init__.py,sha256=J-woLzm3aLF0uPC1-VroqG7QV9xLXHDmVYee2ZXuCgk,2746
63
+ services/tools/batch_execute.py,sha256=hjh67kgWvQDHyGd2N-Tfezv9WAj5x_pWTt_Vybmmq7s,3501
64
+ services/tools/debug_request_context.py,sha256=Duq5xiuSmRO5GdvWAlZhCfOfmrwvK7gGkRC4wYnXmXk,2907
65
+ services/tools/execute_custom_tool.py,sha256=hiZbm2A9t84f92jitzvkE2G4CMOIUiDVm7u5B8K-RbU,1527
66
+ services/tools/execute_menu_item.py,sha256=k4J89LlXmEGyo9z3NK8Q0vREIzr11ucF_9tN_JeQq9M,1248
67
+ services/tools/find_gameobjects.py,sha256=Qyls8HxBgXjA5OtdJfXuIX0qNGMleQSt3NOvQwjP25E,3963
68
+ services/tools/find_in_file.py,sha256=SxhMeo8lRrt0OiGApGZSFUnq671bxVfK8qgAsHxLua8,6493
69
+ services/tools/manage_asset.py,sha256=St_iWQWg9icztnRthU78t6JNhJN0AlC6ELiZhn-SNZU,5990
70
+ services/tools/manage_components.py,sha256=2_nKPk9iPAf5VyYiXuRxSkN8U76VNQbMtE68UTPngrw,5061
71
+ services/tools/manage_editor.py,sha256=ShvlSBQRfoNQ0DvqBWak_Hi3MB7tv2WkMKEhrKQipk0,3279
72
+ services/tools/manage_gameobject.py,sha256=SP-y3_7Ckw12JO6bLJy01Jrx303JXF-7RMROaQuA1kU,14154
73
+ services/tools/manage_material.py,sha256=LSn9Kp-cSMZ5caU6Ux0M_OSMghCtZgOKkmvwf0xLTFE,4311
74
+ services/tools/manage_prefabs.py,sha256=7K-6kTavqu2-oxbA49Ug-vTh1Jt39ua02lWcijwM1DI,10163
75
+ services/tools/manage_scene.py,sha256=-ARtRuj7ZNk_14lmMSORnQs0qTAYKBTPtUfk0sNDo6A,5370
76
+ services/tools/manage_script.py,sha256=tT8JmhTtAYgW8CQla71cfn6IjiUw-tiPjBWVd4ipuCE,28551
77
+ services/tools/manage_scriptable_object.py,sha256=tezG_mbGzPLNpL3F7l5JJLyyjJN3rJi1thGMU8cpOC4,3659
78
+ services/tools/manage_shader.py,sha256=bucRKzQww7opy6DK5nf6isVaEECWWqJ-DVkFulp8CV8,3185
79
+ services/tools/manage_texture.py,sha256=uUicuxKl_uSnP2jBrMbYAhAEWnh7RETusgJfnBI1P8c,22538
80
+ services/tools/manage_vfx.py,sha256=7KFbRohF8EzaD0m7vVIEwjUz-QwC7NEXS5cVcU6Die0,4710
81
+ services/tools/preflight.py,sha256=0nvo0BmZMdIGop1Ha_vypkjn2VLiRvskF0uxh_SlZgE,4162
82
+ services/tools/read_console.py,sha256=ps23debJcQkj3Ap-MqTYVhopYnKGspJs9QHLJHZAAkE,6826
83
+ services/tools/refresh_unity.py,sha256=KrRA8bmLkDLFO1XBv2NmagQAp1dmyaXdUAap567Hcv4,7100
84
+ services/tools/run_tests.py,sha256=Wd7hNZqy-OOZ9ZedonxUJ5bVlhta_aOEmD-2uxmrmyM,11743
85
+ services/tools/script_apply_edits.py,sha256=0f-SaP5NUYGuivl4CWHjR8F-CXUpt3-5qkHpf_edn1U,47677
86
+ services/tools/set_active_instance.py,sha256=pdmC1SxFijyzzjeEyC2N1bXk-GNMu_iXsbCieIpa-R4,4242
87
+ services/tools/utils.py,sha256=ETCiNnWdMZEtnJcDD-CtPsCJ7TBp5x5sPsYuhufkxac,13962
88
+ transport/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
89
+ transport/models.py,sha256=2tnK0Wc-IzvlOS0a04VB3WtuSlAAHeLVFfOnNFHGnhw,1311
90
+ transport/plugin_hub.py,sha256=Kykqnydk_KkCSICaJGE8XQulr7sZeNcIgPOHjtdbjHM,23783
91
+ transport/plugin_registry.py,sha256=L6xl7Ok9rtSW9xwSSKTFjWS7Duu1g4c-qfgfnn6y-sQ,4528
92
+ transport/unity_instance_middleware.py,sha256=DD8gs-peMRmRJz9CYwaHEh4m75LTYPDjVuKuw9sArBw,10438
93
+ transport/unity_transport.py,sha256=RKMH0NYPqTU3rpjXUPUkzZsdWhJqyVaHmkLswab4bGg,2135
94
+ transport/legacy/port_discovery.py,sha256=JDSCqXLodfTT7fOsE0DFC1jJ3QsU6hVaYQb7x7FgdxY,12728
95
+ transport/legacy/stdio_port_registry.py,sha256=j4iARuP6wetppNDG8qKeuvo1bJKcSlgEhZvSyl_uf0A,2313
96
+ transport/legacy/unity_connection.py,sha256=FE9ZQfYMhHvIxBycr_DjI3BKvuEdORXuABnCE5Q2tjQ,36733
97
+ utils/focus_nudge.py,sha256=0MCOms-SxUW7sN2hT3syy1epMdli2zc-6UHBICAfBSM,21330
98
+ utils/module_discovery.py,sha256=My48ofB1BUqxiBoAZAGbEaLQYdsrDhMm8MayBP_bUSQ,2005
99
+ mcpforunityserver-9.3.0b20260129121506.dist-info/METADATA,sha256=NhUC2AYe0dyaYPjuAkHQN1wksQiDGTHHM_8k8IvcEw4,6750
100
+ mcpforunityserver-9.3.0b20260129121506.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
101
+ mcpforunityserver-9.3.0b20260129121506.dist-info/entry_points.txt,sha256=pPm70RXQvkt3uBhPOtViDa47ZTA03RaQ6rwXvyi8oiI,70
102
+ mcpforunityserver-9.3.0b20260129121506.dist-info/top_level.txt,sha256=3-A65WsmBO6UZYH8O5mINdyhhZ63SDssr8LncRd1PSQ,46
103
+ mcpforunityserver-9.3.0b20260129121506.dist-info/RECORD,,
@@ -33,7 +33,7 @@ class ActiveToolResponse(MCPResponse):
33
33
  @mcp_for_unity_resource(
34
34
  uri="mcpforunity://editor/active-tool",
35
35
  name="editor_active_tool",
36
- description="Currently active editor tool (Move, Rotate, Scale, etc.) and transform handle settings."
36
+ description="Currently active editor tool (Move, Rotate, Scale, etc.) and transform handle settings.\n\nURI: mcpforunity://editor/active-tool"
37
37
  )
38
38
  async def get_active_tool(ctx: Context) -> ActiveToolResponse | MCPResponse:
39
39
  """Get active editor tool information."""
@@ -24,7 +24,7 @@ class CustomToolsResourceResponse(MCPResponse):
24
24
  @mcp_for_unity_resource(
25
25
  uri="mcpforunity://custom-tools",
26
26
  name="custom_tools",
27
- description="Lists custom tools available for the active Unity project.",
27
+ description="Lists custom tools available for the active Unity project.\n\nURI: mcpforunity://custom-tools",
28
28
  )
29
29
  async def get_custom_tools(ctx: Context) -> CustomToolsResourceResponse | MCPResponse:
30
30
  unity_instance = get_unity_instance_from_context(ctx)
@@ -214,7 +214,7 @@ def _enrich_advice_and_staleness(state_v2: dict[str, Any]) -> dict[str, Any]:
214
214
  @mcp_for_unity_resource(
215
215
  uri="mcpforunity://editor/state",
216
216
  name="editor_state",
217
- description="Canonical editor readiness snapshot. Includes advice and server-computed staleness.",
217
+ description="Canonical editor readiness snapshot. Includes advice and server-computed staleness.\n\nURI: mcpforunity://editor/state",
218
218
  )
219
219
  async def get_editor_state(ctx: Context) -> MCPResponse:
220
220
  unity_instance = get_unity_instance_from_context(ctx)
@@ -42,7 +42,7 @@ def _validate_instance_id(instance_id: str) -> tuple[int | None, MCPResponse | N
42
42
  @mcp_for_unity_resource(
43
43
  uri="mcpforunity://scene/gameobject-api",
44
44
  name="gameobject_api",
45
- description="Documentation for GameObject resources. Use find_gameobjects tool to get instance IDs, then access resources below."
45
+ description="Documentation for GameObject resources. Use find_gameobjects tool to get instance IDs, then access resources below.\n\nURI: mcpforunity://scene/gameobject-api"
46
46
  )
47
47
  async def get_gameobject_api_docs(_ctx: Context) -> MCPResponse:
48
48
  """
@@ -129,7 +129,7 @@ class GameObjectResponse(MCPResponse):
129
129
  @mcp_for_unity_resource(
130
130
  uri="mcpforunity://scene/gameobject/{instance_id}",
131
131
  name="gameobject",
132
- description="Get detailed information about a single GameObject by instance ID. Returns name, tag, layer, active state, transform data, parent/children IDs, and component type list (no full component properties)."
132
+ description="Get detailed information about a single GameObject by instance ID. Returns name, tag, layer, active state, transform data, parent/children IDs, and component type list (no full component properties).\n\nURI: mcpforunity://scene/gameobject/{instance_id}"
133
133
  )
134
134
  async def get_gameobject(ctx: Context, instance_id: str) -> MCPResponse:
135
135
  """Get GameObject data by instance ID."""
@@ -170,7 +170,7 @@ class ComponentsResponse(MCPResponse):
170
170
  @mcp_for_unity_resource(
171
171
  uri="mcpforunity://scene/gameobject/{instance_id}/components",
172
172
  name="gameobject_components",
173
- description="Get all components on a GameObject with full property serialization. Supports pagination with pageSize and cursor parameters."
173
+ description="Get all components on a GameObject with full property serialization. Supports pagination with pageSize and cursor parameters.\n\nURI: mcpforunity://scene/gameobject/{instance_id}/components"
174
174
  )
175
175
  async def get_gameobject_components(
176
176
  ctx: Context,
@@ -216,7 +216,7 @@ class SingleComponentResponse(MCPResponse):
216
216
  @mcp_for_unity_resource(
217
217
  uri="mcpforunity://scene/gameobject/{instance_id}/component/{component_name}",
218
218
  name="gameobject_component",
219
- description="Get a specific component on a GameObject by type name. Returns the fully serialized component with all properties."
219
+ description="Get a specific component on a GameObject by type name. Returns the fully serialized component with all properties.\n\nURI: mcpforunity://scene/gameobject/{instance_id}/component/{component_name}"
220
220
  )
221
221
  async def get_gameobject_component(
222
222
  ctx: Context,
@@ -15,7 +15,7 @@ class LayersResponse(MCPResponse):
15
15
  @mcp_for_unity_resource(
16
16
  uri="mcpforunity://project/layers",
17
17
  name="project_layers",
18
- description="All layers defined in the project's TagManager with their indices (0-31). Read this before using add_layer or remove_layer tools."
18
+ description="All layers defined in the project's TagManager with their indices (0-31). Read this before using add_layer or remove_layer tools.\n\nURI: mcpforunity://project/layers"
19
19
  )
20
20
  async def get_layers(ctx: Context) -> LayersResponse | MCPResponse:
21
21
  """Get all project layers with their indices."""
@@ -14,7 +14,7 @@ class GetMenuItemsResponse(MCPResponse):
14
14
  @mcp_for_unity_resource(
15
15
  uri="mcpforunity://menu-items",
16
16
  name="menu_items",
17
- description="Provides a list of all menu items."
17
+ description="Provides a list of all menu items.\n\nURI: mcpforunity://menu-items"
18
18
  )
19
19
  async def get_menu_items(ctx: Context) -> GetMenuItemsResponse | MCPResponse:
20
20
  """Provides a list of all menu items.
@@ -43,7 +43,7 @@ def _decode_prefab_path(encoded_path: str) -> str:
43
43
  @mcp_for_unity_resource(
44
44
  uri="mcpforunity://prefab-api",
45
45
  name="prefab_api",
46
- description="Documentation for Prefab resources. Use manage_asset action=search filterType=Prefab to find prefabs, then access resources below."
46
+ description="Documentation for Prefab resources. Use manage_asset action=search filterType=Prefab to find prefabs, then access resources below.\n\nURI: mcpforunity://prefab-api"
47
47
  )
48
48
  async def get_prefab_api_docs(_ctx: Context) -> MCPResponse:
49
49
  """
@@ -117,7 +117,7 @@ class PrefabInfoResponse(MCPResponse):
117
117
  @mcp_for_unity_resource(
118
118
  uri="mcpforunity://prefab/{encoded_path}",
119
119
  name="prefab_info",
120
- description="Get detailed information about a prefab asset by URL-encoded path. Returns prefab type, root object name, component types, child count, and variant info."
120
+ description="Get detailed information about a prefab asset by URL-encoded path. Returns prefab type, root object name, component types, child count, and variant info.\n\nURI: mcpforunity://prefab/{encoded_path}"
121
121
  )
122
122
  async def get_prefab_info(ctx: Context, encoded_path: str) -> MCPResponse:
123
123
  """Get prefab asset info by path."""
@@ -169,7 +169,7 @@ class PrefabHierarchyResponse(MCPResponse):
169
169
  @mcp_for_unity_resource(
170
170
  uri="mcpforunity://prefab/{encoded_path}/hierarchy",
171
171
  name="prefab_hierarchy",
172
- description="Get the full hierarchy of a prefab with nested prefab information. Returns all GameObjects with their components and nesting depth."
172
+ description="Get the full hierarchy of a prefab with nested prefab information. Returns all GameObjects with their components and nesting depth.\n\nURI: mcpforunity://prefab/{encoded_path}/hierarchy"
173
173
  )
174
174
  async def get_prefab_hierarchy(ctx: Context, encoded_path: str) -> MCPResponse:
175
175
  """Get prefab hierarchy by path."""
@@ -25,7 +25,7 @@ class PrefabStageResponse(MCPResponse):
25
25
  @mcp_for_unity_resource(
26
26
  uri="mcpforunity://editor/prefab-stage",
27
27
  name="editor_prefab_stage",
28
- description="Current prefab editing context if a prefab is open in isolation mode. Returns isOpen=false if no prefab is being edited."
28
+ description="Current prefab editing context if a prefab is open in isolation mode. Returns isOpen=false if no prefab is being edited.\n\nURI: mcpforunity://editor/prefab-stage"
29
29
  )
30
30
  async def get_prefab_stage(ctx: Context) -> PrefabStageResponse | MCPResponse:
31
31
  """Get current prefab stage information."""
@@ -25,7 +25,7 @@ class ProjectInfoResponse(MCPResponse):
25
25
  @mcp_for_unity_resource(
26
26
  uri="mcpforunity://project/info",
27
27
  name="project_info",
28
- description="Static project information including root path, Unity version, and platform. This data rarely changes."
28
+ description="Static project information including root path, Unity version, and platform. This data rarely changes.\n\nURI: mcpforunity://project/info"
29
29
  )
30
30
  async def get_project_info(ctx: Context) -> ProjectInfoResponse | MCPResponse:
31
31
  """Get static project configuration information."""
@@ -41,7 +41,7 @@ class SelectionResponse(MCPResponse):
41
41
  @mcp_for_unity_resource(
42
42
  uri="mcpforunity://editor/selection",
43
43
  name="editor_selection",
44
- description="Detailed information about currently selected objects in the editor, including GameObjects, assets, and their properties."
44
+ description="Detailed information about currently selected objects in the editor, including GameObjects, assets, and their properties.\n\nURI: mcpforunity://editor/selection"
45
45
  )
46
46
  async def get_selection(ctx: Context) -> SelectionResponse | MCPResponse:
47
47
  """Get detailed editor selection information."""
@@ -16,7 +16,7 @@ class TagsResponse(MCPResponse):
16
16
  @mcp_for_unity_resource(
17
17
  uri="mcpforunity://project/tags",
18
18
  name="project_tags",
19
- description="All tags defined in the project's TagManager. Read this before using add_tag or remove_tag tools."
19
+ description="All tags defined in the project's TagManager. Read this before using add_tag or remove_tag tools.\n\nURI: mcpforunity://project/tags"
20
20
  )
21
21
  async def get_tags(ctx: Context) -> TagsResponse | MCPResponse:
22
22
  """Get all project tags."""
@@ -1,4 +1,4 @@
1
- from typing import Annotated, Literal
1
+ from typing import Annotated, Literal, Optional
2
2
  from pydantic import BaseModel, Field
3
3
 
4
4
  from fastmcp import Context
@@ -17,15 +17,36 @@ class TestItem(BaseModel):
17
17
  Field(description="The mode the test is for.")]
18
18
 
19
19
 
20
+ class PaginatedTestsData(BaseModel):
21
+ """Paginated test results."""
22
+ items: list[TestItem] = Field(description="Tests on current page")
23
+ cursor: int = Field(description="Current page cursor (0-based)")
24
+ nextCursor: Optional[int] = Field(None, description="Next page cursor, null if last page")
25
+ totalCount: int = Field(description="Total number of tests across all pages")
26
+ pageSize: int = Field(description="Number of items per page")
27
+ hasMore: bool = Field(description="Whether there are more items after this page")
28
+
29
+
20
30
  class GetTestsResponse(MCPResponse):
21
- data: list[TestItem] = []
31
+ """Response containing paginated test data."""
32
+ data: PaginatedTestsData = Field(description="Paginated test data")
22
33
 
23
34
 
24
- @mcp_for_unity_resource(uri="mcpforunity://tests", name="get_tests", description="Provides a list of all tests.")
35
+ @mcp_for_unity_resource(
36
+ uri="mcpforunity://tests",
37
+ name="get_tests",
38
+ description="Provides the first page of Unity tests (default 50 items). "
39
+ "For filtering or pagination, use the run_tests tool instead.\n\nURI: mcpforunity://tests"
40
+ )
25
41
  async def get_tests(ctx: Context) -> GetTestsResponse | MCPResponse:
26
- """Provides a list of all tests.
42
+ """Provides a paginated list of all Unity tests.
43
+
44
+ Returns the first page of tests using Unity's default pagination (50 items).
45
+ For advanced filtering or pagination control, use the run_tests tool which
46
+ accepts mode, filter, page_size, and cursor parameters.
27
47
  """
28
48
  unity_instance = get_unity_instance_from_context(ctx)
49
+
29
50
  response = await send_with_unity_instance(
30
51
  async_send_command_with_retry,
31
52
  unity_instance,
@@ -35,17 +56,28 @@ async def get_tests(ctx: Context) -> GetTestsResponse | MCPResponse:
35
56
  return GetTestsResponse(**response) if isinstance(response, dict) else response
36
57
 
37
58
 
38
- @mcp_for_unity_resource(uri="mcpforunity://tests/{mode}", name="get_tests_for_mode", description="Provides a list of tests for a specific mode.")
59
+ @mcp_for_unity_resource(
60
+ uri="mcpforunity://tests/{mode}",
61
+ name="get_tests_for_mode",
62
+ description="Provides the first page of tests for a specific mode (EditMode or PlayMode). "
63
+ "For filtering or pagination, use the run_tests tool instead.\n\nURI: mcpforunity://tests/{mode}"
64
+ )
39
65
  async def get_tests_for_mode(
40
66
  ctx: Context,
41
- mode: Annotated[Literal["EditMode", "PlayMode"], Field(description="The mode to filter tests by.")],
67
+ mode: Annotated[Literal["EditMode", "PlayMode"], Field(
68
+ description="The mode to filter tests by (EditMode or PlayMode)."
69
+ )],
42
70
  ) -> GetTestsResponse | MCPResponse:
43
- """Provides a list of tests for a specific mode.
71
+ """Provides the first page of tests for a specific mode.
44
72
 
45
73
  Args:
46
- mode: The test mode to filter by (EditMode or PlayMode).
74
+ mode: The test mode to filter by (EditMode or PlayMode)
75
+
76
+ Returns the first page of tests using Unity's default pagination (50 items).
77
+ For advanced filtering or pagination control, use the run_tests tool.
47
78
  """
48
79
  unity_instance = get_unity_instance_from_context(ctx)
80
+
49
81
  response = await send_with_unity_instance(
50
82
  async_send_command_with_retry,
51
83
  unity_instance,
@@ -13,7 +13,7 @@ from transport.unity_transport import _current_transport
13
13
  @mcp_for_unity_resource(
14
14
  uri="mcpforunity://instances",
15
15
  name="unity_instances",
16
- description="Lists all running Unity Editor instances with their details."
16
+ description="Lists all running Unity Editor instances with their details.\n\nURI: mcpforunity://instances"
17
17
  )
18
18
  async def unity_instances(ctx: Context) -> dict[str, Any]:
19
19
  """
@@ -33,7 +33,7 @@ class WindowsResponse(MCPResponse):
33
33
  @mcp_for_unity_resource(
34
34
  uri="mcpforunity://editor/windows",
35
35
  name="editor_windows",
36
- description="All currently open editor windows with their titles, types, positions, and focus state."
36
+ description="All currently open editor windows with their titles, types, positions, and focus state.\n\nURI: mcpforunity://editor/windows"
37
37
  )
38
38
  async def get_windows(ctx: Context) -> WindowsResponse | MCPResponse:
39
39
  """Get all open editor windows."""
@@ -51,7 +51,9 @@ def register_all_tools(mcp: FastMCP, *, project_scoped_tools: bool = True):
51
51
  "Skipping execute_custom_tool registration (project-scoped tools disabled)")
52
52
  continue
53
53
 
54
- # Apply the @mcp.tool decorator, telemetry, and logging
54
+ # Apply decorators: logging -> telemetry -> mcp.tool
55
+ # Note: Parameter normalization (camelCase -> snake_case) is handled by
56
+ # ParamNormalizerMiddleware before FastMCP validation
55
57
  wrapped = log_execution(tool_name, "Tool")(func)
56
58
  wrapped = telemetry_tool(tool_name)(wrapped)
57
59
  wrapped = mcp.tool(
@@ -5,6 +5,7 @@ Returns only instance IDs with pagination support for efficient searches.
5
5
  from typing import Annotated, Any, Literal
6
6
 
7
7
  from fastmcp import Context
8
+ from pydantic import Field
8
9
  from services.registry import mcp_for_unity_tool
9
10
  from services.tools import get_unity_instance_from_context
10
11
  from transport.unity_transport import send_with_unity_instance
@@ -14,22 +15,42 @@ from services.tools.preflight import preflight
14
15
 
15
16
 
16
17
  @mcp_for_unity_tool(
17
- description="Search for GameObjects in the scene. Returns instance IDs only (paginated) for efficient lookups. Use mcpforunity://scene/gameobject/{id} resource to get full GameObject data."
18
+ description="Search for GameObjects in the scene. Requires search_term (name, tag, layer name, component type, or path). Returns instance IDs only (paginated). Use mcpforunity://scene/gameobject/{id} resource to get full GameObject data."
18
19
  )
19
20
  async def find_gameobjects(
20
21
  ctx: Context,
21
- search_term: Annotated[str, "The value to search for (name, tag, layer name, component type, or path)"],
22
+ search_term: Annotated[
23
+ str,
24
+ Field(description="The value to search for (name, tag, layer name, component type, or path)")
25
+ ],
22
26
  search_method: Annotated[
23
- Literal["by_name", "by_tag", "by_layer",
24
- "by_component", "by_path", "by_id"],
25
- "How to search for GameObjects"
27
+ Literal["by_name", "by_tag", "by_layer", "by_component", "by_path", "by_id"],
28
+ Field(
29
+ default="by_name",
30
+ description="How to search for GameObjects"
31
+ )
26
32
  ] = "by_name",
27
- include_inactive: Annotated[bool | str,
28
- "Include inactive GameObjects in search"] | None = None,
29
- page_size: Annotated[int | str,
30
- "Number of results per page (default: 50, max: 500)"] | None = None,
31
- cursor: Annotated[int | str,
32
- "Pagination cursor (offset for next page)"] | None = None,
33
+ include_inactive: Annotated[
34
+ bool | str | None,
35
+ Field(
36
+ default=None,
37
+ description="Include inactive GameObjects in search"
38
+ )
39
+ ] = None,
40
+ page_size: Annotated[
41
+ int | str | None,
42
+ Field(
43
+ default=None,
44
+ description="Number of results per page (default: 50, max: 500)"
45
+ )
46
+ ] = None,
47
+ cursor: Annotated[
48
+ int | str | None,
49
+ Field(
50
+ default=None,
51
+ description="Pagination cursor (offset for next page)"
52
+ )
53
+ ] = None,
33
54
  ) -> dict[str, Any]:
34
55
  """
35
56
  Search for GameObjects and return their instance IDs.
@@ -1,5 +1,4 @@
1
1
  import json
2
- import math
3
2
  from typing import Annotated, Any, Literal
4
3
 
5
4
  from fastmcp import Context
@@ -9,64 +8,10 @@ from services.registry import mcp_for_unity_tool
9
8
  from services.tools import get_unity_instance_from_context
10
9
  from transport.unity_transport import send_with_unity_instance
11
10
  from transport.legacy.unity_connection import async_send_command_with_retry
12
- from services.tools.utils import coerce_bool, parse_json_payload, coerce_int
11
+ from services.tools.utils import coerce_bool, parse_json_payload, coerce_int, normalize_vector3
13
12
  from services.tools.preflight import preflight
14
13
 
15
14
 
16
- def _normalize_vector(value: Any, param_name: str = "vector") -> tuple[list[float] | None, str | None]:
17
- """
18
- Robustly normalize a vector parameter to [x, y, z] format.
19
- Handles: list, tuple, JSON string, comma-separated string.
20
- Returns (parsed_vector, error_message). If error_message is set, parsed_vector is None.
21
- """
22
- if value is None:
23
- return None, None
24
-
25
- # If already a list/tuple with 3 elements, convert to floats
26
- if isinstance(value, (list, tuple)) and len(value) == 3:
27
- try:
28
- vec = [float(value[0]), float(value[1]), float(value[2])]
29
- if all(math.isfinite(n) for n in vec):
30
- return vec, None
31
- return None, f"{param_name} values must be finite numbers, got {value}"
32
- except (ValueError, TypeError):
33
- return None, f"{param_name} values must be numbers, got {value}"
34
-
35
- # Try parsing as JSON string
36
- if isinstance(value, str):
37
- # Check for obviously invalid values
38
- if value in ("[object Object]", "undefined", "null", ""):
39
- return None, f"{param_name} received invalid value: '{value}'. Expected [x, y, z] array (list or JSON string)"
40
-
41
- parsed = parse_json_payload(value)
42
- if isinstance(parsed, list) and len(parsed) == 3:
43
- try:
44
- vec = [float(parsed[0]), float(parsed[1]), float(parsed[2])]
45
- if all(math.isfinite(n) for n in vec):
46
- return vec, None
47
- return None, f"{param_name} values must be finite numbers, got {parsed}"
48
- except (ValueError, TypeError):
49
- return None, f"{param_name} values must be numbers, got {parsed}"
50
-
51
- # Handle legacy comma-separated strings "1,2,3" or "[1,2,3]"
52
- s = value.strip()
53
- if s.startswith("[") and s.endswith("]"):
54
- s = s[1:-1]
55
- parts = [p.strip() for p in (s.split(",") if "," in s else s.split())]
56
- if len(parts) == 3:
57
- try:
58
- vec = [float(parts[0]), float(parts[1]), float(parts[2])]
59
- if all(math.isfinite(n) for n in vec):
60
- return vec, None
61
- return None, f"{param_name} values must be finite numbers, got {value}"
62
- except (ValueError, TypeError):
63
- return None, f"{param_name} values must be numbers, got {value}"
64
-
65
- return None, f"{param_name} must be a [x, y, z] array (list or JSON string), got: {value}"
66
-
67
- return None, f"{param_name} must be a list or JSON string, got {type(value).__name__}"
68
-
69
-
70
15
  def _normalize_component_properties(value: Any) -> tuple[dict[str, dict[str, Any]] | None, str | None]:
71
16
  """
72
17
  Robustly normalize component_properties to a dict.
@@ -115,12 +60,12 @@ async def manage_gameobject(
115
60
  "Tag name - used for both 'create' (initial tag) and 'modify' (change tag)"] | None = None,
116
61
  parent: Annotated[str,
117
62
  "Parent GameObject reference - used for both 'create' (initial parent) and 'modify' (change parent)"] | None = None,
118
- position: Annotated[list[float] | str,
119
- "Position as [x, y, z] array (list or JSON string)"] | None = None,
120
- rotation: Annotated[list[float] | str,
121
- "Rotation as [x, y, z] euler angles array (list or JSON string)"] | None = None,
122
- scale: Annotated[list[float] | str,
123
- "Scale as [x, y, z] array (list or JSON string)"] | None = None,
63
+ position: Annotated[list[float] | dict[str, float] | str,
64
+ "Position as [x, y, z] array, {x, y, z} object, or JSON string"] | None = None,
65
+ rotation: Annotated[list[float] | dict[str, float] | str,
66
+ "Rotation as [x, y, z] euler angles array, {x, y, z} object, or JSON string"] | None = None,
67
+ scale: Annotated[list[float] | dict[str, float] | str,
68
+ "Scale as [x, y, z] array, {x, y, z} object, or JSON string"] | None = None,
124
69
  components_to_add: Annotated[list[str],
125
70
  "List of component names to add"] | None = None,
126
71
  primitive_type: Annotated[str,
@@ -196,16 +141,16 @@ async def manage_gameobject(
196
141
  }
197
142
 
198
143
  # --- Normalize vector parameters with detailed error handling ---
199
- position, position_error = _normalize_vector(position, "position")
144
+ position, position_error = normalize_vector3(position, "position")
200
145
  if position_error:
201
146
  return {"success": False, "message": position_error}
202
- rotation, rotation_error = _normalize_vector(rotation, "rotation")
147
+ rotation, rotation_error = normalize_vector3(rotation, "rotation")
203
148
  if rotation_error:
204
149
  return {"success": False, "message": rotation_error}
205
- scale, scale_error = _normalize_vector(scale, "scale")
150
+ scale, scale_error = normalize_vector3(scale, "scale")
206
151
  if scale_error:
207
152
  return {"success": False, "message": scale_error}
208
- offset, offset_error = _normalize_vector(offset, "offset")
153
+ offset, offset_error = normalize_vector3(offset, "offset")
209
154
  if offset_error:
210
155
  return {"success": False, "message": offset_error}
211
156