mcpforunityserver 8.7.0__py3-none-any.whl → 9.0.0__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 (53) hide show
  1. main.py +4 -3
  2. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.0.0.dist-info}/METADATA +2 -2
  3. mcpforunityserver-9.0.0.dist-info/RECORD +72 -0
  4. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.0.0.dist-info}/top_level.txt +0 -1
  5. services/custom_tool_service.py +13 -8
  6. services/resources/active_tool.py +1 -1
  7. services/resources/custom_tools.py +2 -2
  8. services/resources/editor_state.py +283 -30
  9. services/resources/gameobject.py +243 -0
  10. services/resources/layers.py +1 -1
  11. services/resources/prefab_stage.py +1 -1
  12. services/resources/project_info.py +1 -1
  13. services/resources/selection.py +1 -1
  14. services/resources/tags.py +1 -1
  15. services/resources/unity_instances.py +1 -1
  16. services/resources/windows.py +1 -1
  17. services/state/external_changes_scanner.py +3 -4
  18. services/tools/batch_execute.py +24 -9
  19. services/tools/debug_request_context.py +8 -2
  20. services/tools/execute_custom_tool.py +6 -1
  21. services/tools/execute_menu_item.py +6 -3
  22. services/tools/find_gameobjects.py +89 -0
  23. services/tools/find_in_file.py +26 -19
  24. services/tools/manage_asset.py +13 -44
  25. services/tools/manage_components.py +131 -0
  26. services/tools/manage_editor.py +9 -8
  27. services/tools/manage_gameobject.py +115 -79
  28. services/tools/manage_material.py +80 -31
  29. services/tools/manage_prefabs.py +7 -1
  30. services/tools/manage_scene.py +30 -13
  31. services/tools/manage_script.py +62 -19
  32. services/tools/manage_scriptable_object.py +22 -10
  33. services/tools/manage_shader.py +8 -1
  34. services/tools/manage_vfx.py +738 -0
  35. services/tools/preflight.py +15 -12
  36. services/tools/read_console.py +31 -14
  37. services/tools/refresh_unity.py +28 -18
  38. services/tools/run_tests.py +162 -53
  39. services/tools/script_apply_edits.py +15 -7
  40. services/tools/set_active_instance.py +12 -7
  41. services/tools/utils.py +60 -6
  42. transport/legacy/port_discovery.py +2 -2
  43. transport/legacy/unity_connection.py +102 -17
  44. transport/plugin_hub.py +68 -24
  45. transport/unity_instance_middleware.py +4 -3
  46. transport/unity_transport.py +2 -1
  47. mcpforunityserver-8.7.0.dist-info/RECORD +0 -71
  48. routes/__init__.py +0 -0
  49. services/resources/editor_state_v2.py +0 -270
  50. services/tools/test_jobs.py +0 -94
  51. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.0.0.dist-info}/WHEEL +0 -0
  52. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.0.0.dist-info}/entry_points.txt +0 -0
  53. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,18 +2,57 @@
2
2
  Defines the manage_material tool for interacting with Unity materials.
3
3
  """
4
4
  import json
5
- from typing import Annotated, Any, Literal, Union
5
+ from typing import Annotated, Any, Literal
6
6
 
7
7
  from fastmcp import Context
8
+ from mcp.types import ToolAnnotations
9
+
8
10
  from services.registry import mcp_for_unity_tool
9
11
  from services.tools import get_unity_instance_from_context
10
- from services.tools.utils import parse_json_payload
12
+ from services.tools.utils import parse_json_payload, coerce_int, normalize_properties
11
13
  from transport.unity_transport import send_with_unity_instance
12
14
  from transport.legacy.unity_connection import async_send_command_with_retry
13
15
 
14
16
 
17
+ def _normalize_color(value: Any) -> tuple[list[float] | None, str | None]:
18
+ """
19
+ Normalize color parameter to [r, g, b] or [r, g, b, a] format.
20
+ Returns (parsed_color, error_message).
21
+ """
22
+ if value is None:
23
+ return None, None
24
+
25
+ # Already a list - validate
26
+ if isinstance(value, (list, tuple)):
27
+ if len(value) in (3, 4):
28
+ try:
29
+ return [float(c) for c in value], None
30
+ except (ValueError, TypeError):
31
+ return None, f"color values must be numbers, got {value}"
32
+ return None, f"color must have 3 or 4 components, got {len(value)}"
33
+
34
+ # Try parsing as string
35
+ if isinstance(value, str):
36
+ if value in ("[object Object]", "undefined", "null", ""):
37
+ return None, f"color received invalid value: '{value}'. Expected [r, g, b] or [r, g, b, a]"
38
+
39
+ parsed = parse_json_payload(value)
40
+ if isinstance(parsed, (list, tuple)) and len(parsed) in (3, 4):
41
+ try:
42
+ return [float(c) for c in parsed], None
43
+ except (ValueError, TypeError):
44
+ return None, f"color values must be numbers, got {parsed}"
45
+ return None, f"Failed to parse color string: {value}"
46
+
47
+ return None, f"color must be a list or JSON string, got {type(value).__name__}"
48
+
49
+
15
50
  @mcp_for_unity_tool(
16
- description="Manages Unity materials (set properties, colors, shaders, etc)."
51
+ description="Manages Unity materials (set properties, colors, shaders, etc). Read-only actions: ping, get_material_info. Modifying actions: create, set_material_shader_property, set_material_color, assign_material_to_renderer, set_renderer_color.",
52
+ annotations=ToolAnnotations(
53
+ title="Manage Material",
54
+ destructiveHint=True,
55
+ ),
17
56
  )
18
57
  async def manage_material(
19
58
  ctx: Context,
@@ -26,45 +65,55 @@ async def manage_material(
26
65
  "set_renderer_color",
27
66
  "get_material_info"
28
67
  ], "Action to perform."],
29
-
68
+
30
69
  # Common / Shared
31
- material_path: Annotated[str, "Path to material asset (Assets/...)"] | None = None,
32
- property: Annotated[str, "Shader property name (e.g., _BaseColor, _MainTex)"] | None = None,
70
+ material_path: Annotated[str,
71
+ "Path to material asset (Assets/...)"] | None = None,
72
+ property: Annotated[str,
73
+ "Shader property name (e.g., _BaseColor, _MainTex)"] | None = None,
33
74
 
34
75
  # create
35
76
  shader: Annotated[str, "Shader name (default: Standard)"] | None = None,
36
- properties: Annotated[Union[dict[str, Any], str], "Initial properties to set {name: value}."] | None = None,
37
-
77
+ properties: Annotated[dict[str, Any],
78
+ "Initial properties to set as {name: value} dict."] | None = None,
79
+
38
80
  # set_material_shader_property
39
- value: Annotated[Union[list, float, int, str, bool, None], "Value to set (color array, float, texture path/instruction)"] | None = None,
40
-
81
+ value: Annotated[list | float | int | str | bool | None,
82
+ "Value to set (color array, float, texture path/instruction)"] | None = None,
83
+
41
84
  # set_material_color / set_renderer_color
42
- color: Annotated[Union[list[float], list[int], str], "Color as [r,g,b] or [r,g,b,a]."] | None = None,
43
-
85
+ color: Annotated[list[float],
86
+ "Color as [r, g, b] or [r, g, b, a] array."] | None = None,
87
+
44
88
  # assign_material_to_renderer / set_renderer_color
45
- target: Annotated[str, "Target GameObject (name, path, or find instruction)"] | None = None,
46
- search_method: Annotated[Literal["by_name", "by_path", "by_tag", "by_layer", "by_component"], "Search method for target"] | None = None,
47
- slot: Annotated[int | str, "Material slot index"] | None = None,
48
- mode: Annotated[Literal["shared", "instance", "property_block"], "Assignment/modification mode"] | None = None,
49
-
89
+ target: Annotated[str,
90
+ "Target GameObject (name, path, or find instruction)"] | None = None,
91
+ search_method: Annotated[Literal["by_name", "by_path", "by_tag",
92
+ "by_layer", "by_component"], "Search method for target"] | None = None,
93
+ slot: Annotated[int, "Material slot index (0-based)"] | None = None,
94
+ mode: Annotated[Literal["shared", "instance", "property_block"],
95
+ "Assignment/modification mode"] | None = None,
96
+
50
97
  ) -> dict[str, Any]:
51
98
  unity_instance = get_unity_instance_from_context(ctx)
52
99
 
53
- # Parse inputs that might be stringified JSON
54
- color = parse_json_payload(color)
55
- properties = parse_json_payload(properties)
100
+ # --- Normalize color with validation ---
101
+ color, color_error = _normalize_color(color)
102
+ if color_error:
103
+ return {"success": False, "message": color_error}
104
+
105
+ # --- Normalize properties with validation ---
106
+ properties, props_error = normalize_properties(properties)
107
+ if props_error:
108
+ return {"success": False, "message": props_error}
109
+
110
+ # --- Normalize value (parse JSON if string) ---
56
111
  value = parse_json_payload(value)
112
+ if isinstance(value, str) and value in ("[object Object]", "undefined"):
113
+ return {"success": False, "message": f"value received invalid input: '{value}'"}
57
114
 
58
- # Coerce slot to int if it's a string
59
- if slot is not None:
60
- if isinstance(slot, str):
61
- try:
62
- slot = int(slot)
63
- except ValueError:
64
- return {
65
- "success": False,
66
- "message": f"Invalid slot value: '{slot}' must be a valid integer"
67
- }
115
+ # --- Normalize slot to int ---
116
+ slot = coerce_int(slot)
68
117
 
69
118
  # Prepare parameters for the C# handler
70
119
  params_dict = {
@@ -91,5 +140,5 @@ async def manage_material(
91
140
  "manage_material",
92
141
  params_dict,
93
142
  )
94
-
143
+
95
144
  return result if isinstance(result, dict) else {"success": False, "message": str(result)}
@@ -1,6 +1,8 @@
1
1
  from typing import Annotated, Any, Literal
2
2
 
3
3
  from fastmcp import Context
4
+ from mcp.types import ToolAnnotations
5
+
4
6
  from services.registry import mcp_for_unity_tool
5
7
  from services.tools import get_unity_instance_from_context
6
8
  from transport.unity_transport import send_with_unity_instance
@@ -9,7 +11,11 @@ from services.tools.utils import coerce_bool
9
11
 
10
12
 
11
13
  @mcp_for_unity_tool(
12
- description="Performs prefab operations (open_stage, close_stage, save_open_stage, create_from_gameobject)."
14
+ description="Performs prefab operations (open_stage, close_stage, save_open_stage, create_from_gameobject).",
15
+ annotations=ToolAnnotations(
16
+ title="Manage Prefabs",
17
+ destructiveHint=True,
18
+ ),
13
19
  )
14
20
  async def manage_prefabs(
15
21
  ctx: Context,
@@ -1,6 +1,8 @@
1
1
  from typing import Annotated, Literal, Any
2
2
 
3
3
  from fastmcp import Context
4
+ from mcp.types import ToolAnnotations
5
+
4
6
  from services.registry import mcp_for_unity_tool
5
7
  from services.tools import get_unity_instance_from_context
6
8
  from services.tools.utils import coerce_int, coerce_bool
@@ -10,7 +12,11 @@ from services.tools.preflight import preflight
10
12
 
11
13
 
12
14
  @mcp_for_unity_tool(
13
- description="Performs CRUD operations on Unity scenes."
15
+ description="Performs CRUD operations on Unity scenes. Read-only actions: get_hierarchy, get_active, get_build_settings, screenshot. Modifying actions: create, load, save.",
16
+ annotations=ToolAnnotations(
17
+ title="Manage Scene",
18
+ destructiveHint=True,
19
+ ),
14
20
  )
15
21
  async def manage_scene(
16
22
  ctx: Context,
@@ -27,16 +33,25 @@ async def manage_scene(
27
33
  path: Annotated[str, "Scene path."] | None = None,
28
34
  build_index: Annotated[int | str,
29
35
  "Unity build index (quote as string, e.g., '0')."] | None = None,
30
- screenshot_file_name: Annotated[str, "Screenshot file name (optional). Defaults to timestamp when omitted."] | None = None,
31
- screenshot_super_size: Annotated[int | str, "Screenshot supersize multiplier (integer ≥1). Optional." ] | None = None,
36
+ screenshot_file_name: Annotated[str,
37
+ "Screenshot file name (optional). Defaults to timestamp when omitted."] | None = None,
38
+ screenshot_super_size: Annotated[int | str,
39
+ "Screenshot supersize multiplier (integer ≥1). Optional."] | None = None,
32
40
  # --- get_hierarchy paging/safety ---
33
- parent: Annotated[str | int, "Optional parent GameObject reference (name/path/instanceID) to list direct children."] | None = None,
34
- page_size: Annotated[int | str, "Page size for get_hierarchy paging."] | None = None,
35
- cursor: Annotated[int | str, "Opaque cursor for paging (offset)."] | None = None,
36
- max_nodes: Annotated[int | str, "Hard cap on returned nodes per request (safety)."] | None = None,
37
- max_depth: Annotated[int | str, "Accepted for forward-compatibility; current paging returns a single level."] | None = None,
38
- max_children_per_node: Annotated[int | str, "Child paging hint (safety)."] | None = None,
39
- include_transform: Annotated[bool | str, "If true, include local transform in node summaries."] | None = None,
41
+ parent: Annotated[str | int,
42
+ "Optional parent GameObject reference (name/path/instanceID) to list direct children."] | None = None,
43
+ page_size: Annotated[int | str,
44
+ "Page size for get_hierarchy paging."] | None = None,
45
+ cursor: Annotated[int | str,
46
+ "Opaque cursor for paging (offset)."] | None = None,
47
+ max_nodes: Annotated[int | str,
48
+ "Hard cap on returned nodes per request (safety)."] | None = None,
49
+ max_depth: Annotated[int | str,
50
+ "Accepted for forward-compatibility; current paging returns a single level."] | None = None,
51
+ max_children_per_node: Annotated[int | str,
52
+ "Child paging hint (safety)."] | None = None,
53
+ include_transform: Annotated[bool | str,
54
+ "If true, include local transform in node summaries."] | None = None,
40
55
  ) -> dict[str, Any]:
41
56
  # Get active instance from session state
42
57
  # Removed session_state import
@@ -51,8 +66,10 @@ async def manage_scene(
51
66
  coerced_cursor = coerce_int(cursor, default=None)
52
67
  coerced_max_nodes = coerce_int(max_nodes, default=None)
53
68
  coerced_max_depth = coerce_int(max_depth, default=None)
54
- coerced_max_children_per_node = coerce_int(max_children_per_node, default=None)
55
- coerced_include_transform = coerce_bool(include_transform, default=None)
69
+ coerced_max_children_per_node = coerce_int(
70
+ max_children_per_node, default=None)
71
+ coerced_include_transform = coerce_bool(
72
+ include_transform, default=None)
56
73
 
57
74
  params: dict[str, Any] = {"action": action}
58
75
  if name:
@@ -65,7 +82,7 @@ async def manage_scene(
65
82
  params["fileName"] = screenshot_file_name
66
83
  if coerced_super_size is not None:
67
84
  params["superSize"] = coerced_super_size
68
-
85
+
69
86
  # get_hierarchy paging/safety params (optional)
70
87
  if parent is not None:
71
88
  params["parent"] = parent
@@ -4,6 +4,7 @@ from typing import Annotated, Any, Literal
4
4
  from urllib.parse import urlparse, unquote
5
5
 
6
6
  from fastmcp import FastMCP, Context
7
+ from mcp.types import ToolAnnotations
7
8
 
8
9
  from services.registry import mcp_for_unity_tool
9
10
  from services.tools import get_unity_instance_from_context
@@ -15,7 +16,7 @@ def _split_uri(uri: str) -> tuple[str, str]:
15
16
  """Split an incoming URI or path into (name, directory) suitable for Unity.
16
17
 
17
18
  Rules:
18
- - unity://path/Assets/... → keep as Assets-relative (after decode/normalize)
19
+ - mcpforunity://path/Assets/... → keep as Assets-relative (after decode/normalize)
19
20
  - file://... → percent-decode, normalize, strip host and leading slashes,
20
21
  then, if any 'Assets' segment exists, return path relative to that 'Assets' root.
21
22
  Otherwise, fall back to original name/dir behavior.
@@ -23,8 +24,8 @@ def _split_uri(uri: str) -> tuple[str, str]:
23
24
  return relative to 'Assets'.
24
25
  """
25
26
  raw_path: str
26
- if uri.startswith("unity://path/"):
27
- raw_path = uri[len("unity://path/"):]
27
+ if uri.startswith("mcpforunity://path/"):
28
+ raw_path = uri[len("mcpforunity://path/"):]
28
29
  elif uri.startswith("file://"):
29
30
  parsed = urlparse(uri)
30
31
  host = (parsed.netloc or "").strip()
@@ -63,8 +64,9 @@ def _split_uri(uri: str) -> tuple[str, str]:
63
64
  return name, directory
64
65
 
65
66
 
66
- @mcp_for_unity_tool(description=(
67
- """Apply small text edits to a C# script identified by URI.
67
+ @mcp_for_unity_tool(
68
+ description=(
69
+ """Apply small text edits to a C# script identified by URI.
68
70
  IMPORTANT: This tool replaces EXACT character positions. Always verify content at target lines/columns BEFORE editing!
69
71
  RECOMMENDED WORKFLOW:
70
72
  1. First call resources/read with start_line/line_count to verify exact content
@@ -76,10 +78,15 @@ def _split_uri(uri: str) -> tuple[str, str]:
76
78
  - For pattern-based replacements, consider anchor operations in script_apply_edits
77
79
  - Lines, columns are 1-indexed
78
80
  - Tabs count as 1 column"""
79
- ))
81
+ ),
82
+ annotations=ToolAnnotations(
83
+ title="Apply Text Edits",
84
+ destructiveHint=True,
85
+ ),
86
+ )
80
87
  async def apply_text_edits(
81
88
  ctx: Context,
82
- uri: Annotated[str, "URI of the script to edit under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."],
89
+ uri: Annotated[str, "URI of the script to edit under Assets/ directory, mcpforunity://path/Assets/... or file://... or Assets/..."],
83
90
  edits: Annotated[list[dict[str, Any]], "List of edits to apply to the script, i.e. a list of {startLine,startCol,endLine,endCol,newText} (1-indexed!)"],
84
91
  precondition_sha256: Annotated[str,
85
92
  "Optional SHA256 of the script to edit, used to prevent concurrent edits"] | None = None,
@@ -367,7 +374,13 @@ async def apply_text_edits(
367
374
  return {"success": False, "message": str(resp)}
368
375
 
369
376
 
370
- @mcp_for_unity_tool(description=("Create a new C# script at the given project path."))
377
+ @mcp_for_unity_tool(
378
+ description="Create a new C# script at the given project path.",
379
+ annotations=ToolAnnotations(
380
+ title="Create Script",
381
+ destructiveHint=True,
382
+ ),
383
+ )
371
384
  async def create_script(
372
385
  ctx: Context,
373
386
  path: Annotated[str, "Path under Assets/ to create the script at, e.g., 'Assets/Scripts/My.cs'"],
@@ -412,10 +425,16 @@ async def create_script(
412
425
  return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
413
426
 
414
427
 
415
- @mcp_for_unity_tool(description=("Delete a C# script by URI or Assets-relative path."))
428
+ @mcp_for_unity_tool(
429
+ description="Delete a C# script by URI or Assets-relative path.",
430
+ annotations=ToolAnnotations(
431
+ title="Delete Script",
432
+ destructiveHint=True,
433
+ ),
434
+ )
416
435
  async def delete_script(
417
436
  ctx: Context,
418
- uri: Annotated[str, "URI of the script to delete under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."],
437
+ uri: Annotated[str, "URI of the script to delete under Assets/ directory, mcpforunity://path/Assets/... or file://... or Assets/..."],
419
438
  ) -> dict[str, Any]:
420
439
  """Delete a C# script by URI."""
421
440
  unity_instance = get_unity_instance_from_context(ctx)
@@ -434,10 +453,16 @@ async def delete_script(
434
453
  return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
435
454
 
436
455
 
437
- @mcp_for_unity_tool(description=("Validate a C# script and return diagnostics."))
456
+ @mcp_for_unity_tool(
457
+ description="Validate a C# script and return diagnostics.",
458
+ annotations=ToolAnnotations(
459
+ title="Validate Script",
460
+ readOnlyHint=True,
461
+ ),
462
+ )
438
463
  async def validate_script(
439
464
  ctx: Context,
440
- uri: Annotated[str, "URI of the script to validate under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."],
465
+ uri: Annotated[str, "URI of the script to validate under Assets/ directory, mcpforunity://path/Assets/... or file://... or Assets/..."],
441
466
  level: Annotated[Literal['basic', 'standard'],
442
467
  "Validation level"] = "basic",
443
468
  include_diagnostics: Annotated[bool,
@@ -475,14 +500,20 @@ async def validate_script(
475
500
  return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
476
501
 
477
502
 
478
- @mcp_for_unity_tool(description=("Compatibility router for legacy script operations. Prefer apply_text_edits (ranges) or script_apply_edits (structured) for edits."))
503
+ @mcp_for_unity_tool(
504
+ description="Compatibility router for legacy script operations. Prefer apply_text_edits (ranges) or script_apply_edits (structured) for edits. Read-only action: read. Modifying actions: create, delete.",
505
+ annotations=ToolAnnotations(
506
+ title="Manage Script",
507
+ destructiveHint=True,
508
+ ),
509
+ )
479
510
  async def manage_script(
480
511
  ctx: Context,
481
512
  action: Annotated[Literal['create', 'read', 'delete'], "Perform CRUD operations on C# scripts."],
482
513
  name: Annotated[str, "Script name (no .cs extension)", "Name of the script to create"],
483
514
  path: Annotated[str, "Asset path (default: 'Assets/')", "Path under Assets/ to create the script at, e.g., 'Assets/Scripts/My.cs'"],
484
515
  contents: Annotated[str, "Contents of the script to create",
485
- "C# code for 'create'/'update'"] | None = None,
516
+ "C# code for 'create' action"] | None = None,
486
517
  script_type: Annotated[str, "Script type (e.g., 'C#')",
487
518
  "Type hint (e.g., 'MonoBehaviour')"] | None = None,
488
519
  namespace: Annotated[str, "Namespace for the script"] | None = None,
@@ -543,14 +574,20 @@ async def manage_script(
543
574
  }
544
575
 
545
576
 
546
- @mcp_for_unity_tool(description=(
547
- """Get manage_script capabilities (supported ops, limits, and guards).
577
+ @mcp_for_unity_tool(
578
+ description=(
579
+ """Get manage_script capabilities (supported ops, limits, and guards).
548
580
  Returns:
549
581
  - ops: list of supported structured ops
550
582
  - text_ops: list of supported text ops
551
583
  - max_edit_payload_bytes: server edit payload cap
552
584
  - guards: header/using guard enabled flag"""
553
- ))
585
+ ),
586
+ annotations=ToolAnnotations(
587
+ title="Manage Script Capabilities",
588
+ readOnlyHint=True,
589
+ ),
590
+ )
554
591
  async def manage_script_capabilities(ctx: Context) -> dict[str, Any]:
555
592
  await ctx.info("Processing manage_script_capabilities")
556
593
  try:
@@ -575,10 +612,16 @@ async def manage_script_capabilities(ctx: Context) -> dict[str, Any]:
575
612
  return {"success": False, "error": f"capabilities error: {e}"}
576
613
 
577
614
 
578
- @mcp_for_unity_tool(description="Get SHA256 and basic metadata for a Unity C# script without returning file contents")
615
+ @mcp_for_unity_tool(
616
+ description="Get SHA256 and basic metadata for a Unity C# script without returning file contents",
617
+ annotations=ToolAnnotations(
618
+ title="Get SHA",
619
+ readOnlyHint=True,
620
+ ),
621
+ )
579
622
  async def get_sha(
580
623
  ctx: Context,
581
- uri: Annotated[str, "URI of the script to edit under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."],
624
+ uri: Annotated[str, "URI of the script to edit under Assets/ directory, mcpforunity://path/Assets/... or file://... or Assets/..."],
582
625
  ) -> dict[str, Any]:
583
626
  unity_instance = get_unity_instance_from_context(ctx)
584
627
  await ctx.info(
@@ -13,6 +13,7 @@ from __future__ import annotations
13
13
  from typing import Annotated, Any, Literal
14
14
 
15
15
  from fastmcp import Context
16
+ from mcp.types import ToolAnnotations
16
17
 
17
18
  from services.registry import mcp_for_unity_tool
18
19
  from services.tools import get_unity_instance_from_context
@@ -22,20 +23,33 @@ from transport.legacy.unity_connection import async_send_command_with_retry
22
23
 
23
24
 
24
25
  @mcp_for_unity_tool(
25
- description="Creates and modifies ScriptableObject assets using Unity SerializedObject property paths."
26
+ description="Creates and modifies ScriptableObject assets using Unity SerializedObject property paths.",
27
+ annotations=ToolAnnotations(
28
+ title="Manage Scriptable Object",
29
+ destructiveHint=True,
30
+ ),
26
31
  )
27
32
  async def manage_scriptable_object(
28
33
  ctx: Context,
29
34
  action: Annotated[Literal["create", "modify"], "Action to perform: create or modify."],
30
35
  # --- create params ---
31
- type_name: Annotated[str | None, "Namespace-qualified ScriptableObject type name (for create)."] = None,
32
- folder_path: Annotated[str | None, "Target folder under Assets/... (for create)."] = None,
33
- asset_name: Annotated[str | None, "Asset file name without extension (for create)."] = None,
34
- overwrite: Annotated[bool | str | None, "If true, overwrite existing asset at same path (for create)."] = None,
36
+ type_name: Annotated[str | None,
37
+ "Namespace-qualified ScriptableObject type name (for create)."] = None,
38
+ folder_path: Annotated[str | None,
39
+ "Target folder under Assets/... (for create)."] = None,
40
+ asset_name: Annotated[str | None,
41
+ "Asset file name without extension (for create)."] = None,
42
+ overwrite: Annotated[bool | str | None,
43
+ "If true, overwrite existing asset at same path (for create)."] = None,
35
44
  # --- modify params ---
36
- target: Annotated[dict[str, Any] | str | None, "Target asset reference {guid|path} (for modify)."] = None,
45
+ target: Annotated[dict[str, Any] | str | None,
46
+ "Target asset reference {guid|path} (for modify)."] = None,
37
47
  # --- shared ---
38
- patches: Annotated[list[dict[str, Any]] | str | None, "Patch list (or JSON string) to apply."] = None,
48
+ patches: Annotated[list[dict[str, Any]] | str | None,
49
+ "Patch list (or JSON string) to apply."] = None,
50
+ # --- validation ---
51
+ dry_run: Annotated[bool | str | None,
52
+ "If true, validate patches without applying (modify only)."] = None,
39
53
  ) -> dict[str, Any]:
40
54
  unity_instance = get_unity_instance_from_context(ctx)
41
55
 
@@ -57,6 +71,7 @@ async def manage_scriptable_object(
57
71
  "overwrite": coerce_bool(overwrite, default=None),
58
72
  "target": parsed_target,
59
73
  "patches": parsed_patches,
74
+ "dryRun": coerce_bool(dry_run, default=None),
60
75
  }
61
76
 
62
77
  # Remove None values to keep Unity handler simpler
@@ -70,6 +85,3 @@ async def manage_scriptable_object(
70
85
  )
71
86
  await ctx.info(f"Response {response}")
72
87
  return response if isinstance(response, dict) else {"success": False, "message": "Unexpected response from Unity."}
73
-
74
-
75
-
@@ -2,6 +2,8 @@ import base64
2
2
  from typing import Annotated, Any, Literal
3
3
 
4
4
  from fastmcp import Context
5
+ from mcp.types import ToolAnnotations
6
+
5
7
  from services.registry import mcp_for_unity_tool
6
8
  from services.tools import get_unity_instance_from_context
7
9
  from transport.unity_transport import send_with_unity_instance
@@ -9,7 +11,12 @@ from transport.legacy.unity_connection import async_send_command_with_retry
9
11
 
10
12
 
11
13
  @mcp_for_unity_tool(
12
- description="Manages shader scripts in Unity (create, read, update, delete)."
14
+ description="Manages shader scripts in Unity (create, read, update, delete). Read-only action: read. Modifying actions: create, update, delete.",
15
+ annotations=ToolAnnotations(
16
+ title="Manage Shader",
17
+ # Note: 'read' action is non-destructive; 'create', 'update', 'delete' are destructive
18
+ destructiveHint=True,
19
+ ),
13
20
  )
14
21
  async def manage_shader(
15
22
  ctx: Context,