mcpforunityserver 9.4.0b20260203025228__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. cli/__init__.py +3 -0
  2. cli/commands/__init__.py +3 -0
  3. cli/commands/animation.py +84 -0
  4. cli/commands/asset.py +280 -0
  5. cli/commands/audio.py +125 -0
  6. cli/commands/batch.py +171 -0
  7. cli/commands/code.py +182 -0
  8. cli/commands/component.py +190 -0
  9. cli/commands/editor.py +447 -0
  10. cli/commands/gameobject.py +487 -0
  11. cli/commands/instance.py +93 -0
  12. cli/commands/lighting.py +123 -0
  13. cli/commands/material.py +239 -0
  14. cli/commands/prefab.py +248 -0
  15. cli/commands/scene.py +231 -0
  16. cli/commands/script.py +222 -0
  17. cli/commands/shader.py +226 -0
  18. cli/commands/texture.py +540 -0
  19. cli/commands/tool.py +58 -0
  20. cli/commands/ui.py +258 -0
  21. cli/commands/vfx.py +421 -0
  22. cli/main.py +281 -0
  23. cli/utils/__init__.py +31 -0
  24. cli/utils/config.py +58 -0
  25. cli/utils/confirmation.py +37 -0
  26. cli/utils/connection.py +254 -0
  27. cli/utils/constants.py +23 -0
  28. cli/utils/output.py +195 -0
  29. cli/utils/parsers.py +112 -0
  30. cli/utils/suggestions.py +34 -0
  31. core/__init__.py +0 -0
  32. core/config.py +67 -0
  33. core/constants.py +4 -0
  34. core/logging_decorator.py +37 -0
  35. core/telemetry.py +551 -0
  36. core/telemetry_decorator.py +164 -0
  37. main.py +845 -0
  38. mcpforunityserver-9.4.0b20260203025228.dist-info/METADATA +328 -0
  39. mcpforunityserver-9.4.0b20260203025228.dist-info/RECORD +105 -0
  40. mcpforunityserver-9.4.0b20260203025228.dist-info/WHEEL +5 -0
  41. mcpforunityserver-9.4.0b20260203025228.dist-info/entry_points.txt +3 -0
  42. mcpforunityserver-9.4.0b20260203025228.dist-info/licenses/LICENSE +21 -0
  43. mcpforunityserver-9.4.0b20260203025228.dist-info/top_level.txt +7 -0
  44. models/__init__.py +4 -0
  45. models/models.py +56 -0
  46. models/unity_response.py +70 -0
  47. services/__init__.py +0 -0
  48. services/api_key_service.py +235 -0
  49. services/custom_tool_service.py +499 -0
  50. services/registry/__init__.py +22 -0
  51. services/registry/resource_registry.py +53 -0
  52. services/registry/tool_registry.py +51 -0
  53. services/resources/__init__.py +86 -0
  54. services/resources/active_tool.py +48 -0
  55. services/resources/custom_tools.py +57 -0
  56. services/resources/editor_state.py +304 -0
  57. services/resources/gameobject.py +243 -0
  58. services/resources/layers.py +30 -0
  59. services/resources/menu_items.py +35 -0
  60. services/resources/prefab.py +191 -0
  61. services/resources/prefab_stage.py +40 -0
  62. services/resources/project_info.py +40 -0
  63. services/resources/selection.py +56 -0
  64. services/resources/tags.py +31 -0
  65. services/resources/tests.py +88 -0
  66. services/resources/unity_instances.py +125 -0
  67. services/resources/windows.py +48 -0
  68. services/state/external_changes_scanner.py +245 -0
  69. services/tools/__init__.py +83 -0
  70. services/tools/batch_execute.py +93 -0
  71. services/tools/debug_request_context.py +86 -0
  72. services/tools/execute_custom_tool.py +43 -0
  73. services/tools/execute_menu_item.py +32 -0
  74. services/tools/find_gameobjects.py +110 -0
  75. services/tools/find_in_file.py +181 -0
  76. services/tools/manage_asset.py +119 -0
  77. services/tools/manage_components.py +131 -0
  78. services/tools/manage_editor.py +64 -0
  79. services/tools/manage_gameobject.py +260 -0
  80. services/tools/manage_material.py +111 -0
  81. services/tools/manage_prefabs.py +209 -0
  82. services/tools/manage_scene.py +111 -0
  83. services/tools/manage_script.py +645 -0
  84. services/tools/manage_scriptable_object.py +87 -0
  85. services/tools/manage_shader.py +71 -0
  86. services/tools/manage_texture.py +581 -0
  87. services/tools/manage_vfx.py +120 -0
  88. services/tools/preflight.py +110 -0
  89. services/tools/read_console.py +151 -0
  90. services/tools/refresh_unity.py +153 -0
  91. services/tools/run_tests.py +317 -0
  92. services/tools/script_apply_edits.py +1006 -0
  93. services/tools/set_active_instance.py +120 -0
  94. services/tools/utils.py +348 -0
  95. transport/__init__.py +0 -0
  96. transport/legacy/port_discovery.py +329 -0
  97. transport/legacy/stdio_port_registry.py +65 -0
  98. transport/legacy/unity_connection.py +910 -0
  99. transport/models.py +68 -0
  100. transport/plugin_hub.py +787 -0
  101. transport/plugin_registry.py +182 -0
  102. transport/unity_instance_middleware.py +262 -0
  103. transport/unity_transport.py +94 -0
  104. utils/focus_nudge.py +589 -0
  105. utils/module_discovery.py +55 -0
@@ -0,0 +1,209 @@
1
+ from typing import Annotated, Any, Literal
2
+
3
+ from fastmcp import Context
4
+ from mcp.types import ToolAnnotations
5
+
6
+ from services.registry import mcp_for_unity_tool
7
+ from services.tools import get_unity_instance_from_context
8
+ from services.tools.utils import coerce_bool, normalize_vector3
9
+ from transport.unity_transport import send_with_unity_instance
10
+ from transport.legacy.unity_connection import async_send_command_with_retry
11
+ from services.tools.preflight import preflight
12
+
13
+
14
+ # Required parameters for each action
15
+ REQUIRED_PARAMS = {
16
+ "get_info": ["prefab_path"],
17
+ "get_hierarchy": ["prefab_path"],
18
+ "create_from_gameobject": ["target", "prefab_path"],
19
+ "modify_contents": ["prefab_path"],
20
+ }
21
+
22
+
23
+ @mcp_for_unity_tool(
24
+ description=(
25
+ "Manages Unity Prefab assets via headless operations (no UI, no prefab stages). "
26
+ "Actions: get_info, get_hierarchy, create_from_gameobject, modify_contents. "
27
+ "Use modify_contents for headless prefab editing - ideal for automated workflows. "
28
+ "Use create_child parameter with modify_contents to add child GameObjects to a prefab "
29
+ "(single object or array for batch creation in one save). "
30
+ "Example: create_child=[{\"name\": \"Child1\", \"primitive_type\": \"Sphere\", \"position\": [1,0,0]}, "
31
+ "{\"name\": \"Child2\", \"primitive_type\": \"Cube\", \"parent\": \"Child1\"}]. "
32
+ "Use manage_asset action=search filterType=Prefab to list prefabs."
33
+ ),
34
+ annotations=ToolAnnotations(
35
+ title="Manage Prefabs",
36
+ destructiveHint=True,
37
+ ),
38
+ )
39
+ async def manage_prefabs(
40
+ ctx: Context,
41
+ action: Annotated[
42
+ Literal[
43
+ "create_from_gameobject",
44
+ "get_info",
45
+ "get_hierarchy",
46
+ "modify_contents",
47
+ ],
48
+ "Prefab operation to perform.",
49
+ ],
50
+ prefab_path: Annotated[str, "Prefab asset path (e.g., Assets/Prefabs/MyPrefab.prefab)."] | None = None,
51
+ target: Annotated[str, "Target GameObject: scene object for create_from_gameobject, or object within prefab for modify_contents (name or path like 'Parent/Child')."] | None = None,
52
+ allow_overwrite: Annotated[bool, "Allow replacing existing prefab."] | None = None,
53
+ search_inactive: Annotated[bool, "Include inactive GameObjects in search."] | None = None,
54
+ unlink_if_instance: Annotated[bool, "Unlink from existing prefab before creating new one."] | None = None,
55
+ # modify_contents parameters
56
+ position: Annotated[list[float] | dict[str, float] | str, "New local position [x, y, z] or {x, y, z} for modify_contents."] | None = None,
57
+ rotation: Annotated[list[float] | dict[str, float] | str, "New local rotation (euler angles) [x, y, z] or {x, y, z} for modify_contents."] | None = None,
58
+ scale: Annotated[list[float] | dict[str, float] | str, "New local scale [x, y, z] or {x, y, z} for modify_contents."] | None = None,
59
+ name: Annotated[str, "New name for the target object in modify_contents."] | None = None,
60
+ tag: Annotated[str, "New tag for the target object in modify_contents."] | None = None,
61
+ layer: Annotated[str, "New layer name for the target object in modify_contents."] | None = None,
62
+ set_active: Annotated[bool, "Set active state of target object in modify_contents."] | None = None,
63
+ parent: Annotated[str, "New parent object name/path within prefab for modify_contents."] | None = None,
64
+ components_to_add: Annotated[list[str], "Component types to add in modify_contents."] | None = None,
65
+ components_to_remove: Annotated[list[str], "Component types to remove in modify_contents."] | None = None,
66
+ create_child: Annotated[dict[str, Any] | list[dict[str, Any]], "Create child GameObject(s) in the prefab. Single object or array of objects, each with: name (required), parent (optional, defaults to target), primitive_type (optional: Cube, Sphere, Capsule, Cylinder, Plane, Quad), position, rotation, scale, components_to_add, tag, layer, set_active."] | None = None,
67
+ ) -> dict[str, Any]:
68
+ # Back-compat: map 'name' → 'target' for create_from_gameobject (Unity accepts both)
69
+ if action == "create_from_gameobject" and target is None and name is not None:
70
+ target = name
71
+
72
+ # Validate required parameters
73
+ required = REQUIRED_PARAMS.get(action, [])
74
+ for param_name in required:
75
+ # Use updated local value for target after back-compat mapping
76
+ param_value = target if param_name == "target" else locals().get(param_name)
77
+ # Check for None and empty/whitespace strings
78
+ if param_value is None or (isinstance(param_value, str) and not param_value.strip()):
79
+ return {
80
+ "success": False,
81
+ "message": f"Action '{action}' requires parameter '{param_name}'."
82
+ }
83
+
84
+ unity_instance = get_unity_instance_from_context(ctx)
85
+
86
+ # Preflight check for operations to ensure Unity is ready
87
+ try:
88
+ gate = await preflight(ctx, wait_for_no_compile=True, refresh_if_dirty=True)
89
+ if gate is not None:
90
+ return gate.model_dump()
91
+ except Exception as exc:
92
+ return {
93
+ "success": False,
94
+ "message": f"Unity preflight check failed: {exc}"
95
+ }
96
+
97
+ try:
98
+ # Build parameters dictionary
99
+ params: dict[str, Any] = {"action": action}
100
+
101
+ # Handle prefab path parameter
102
+ if prefab_path:
103
+ params["prefabPath"] = prefab_path
104
+
105
+ if target:
106
+ params["target"] = target
107
+
108
+ allow_overwrite_val = coerce_bool(allow_overwrite)
109
+ if allow_overwrite_val is not None:
110
+ params["allowOverwrite"] = allow_overwrite_val
111
+
112
+ search_inactive_val = coerce_bool(search_inactive)
113
+ if search_inactive_val is not None:
114
+ params["searchInactive"] = search_inactive_val
115
+
116
+ unlink_if_instance_val = coerce_bool(unlink_if_instance)
117
+ if unlink_if_instance_val is not None:
118
+ params["unlinkIfInstance"] = unlink_if_instance_val
119
+
120
+ # modify_contents parameters
121
+ if position is not None:
122
+ position_value, position_error = normalize_vector3(position, "position")
123
+ if position_error:
124
+ return {"success": False, "message": position_error}
125
+ params["position"] = position_value
126
+ if rotation is not None:
127
+ rotation_value, rotation_error = normalize_vector3(rotation, "rotation")
128
+ if rotation_error:
129
+ return {"success": False, "message": rotation_error}
130
+ params["rotation"] = rotation_value
131
+ if scale is not None:
132
+ scale_value, scale_error = normalize_vector3(scale, "scale")
133
+ if scale_error:
134
+ return {"success": False, "message": scale_error}
135
+ params["scale"] = scale_value
136
+ if name is not None:
137
+ params["name"] = name
138
+ if tag is not None:
139
+ params["tag"] = tag
140
+ if layer is not None:
141
+ params["layer"] = layer
142
+ set_active_val = coerce_bool(set_active)
143
+ if set_active_val is not None:
144
+ params["setActive"] = set_active_val
145
+ if parent is not None:
146
+ params["parent"] = parent
147
+ if components_to_add is not None:
148
+ params["componentsToAdd"] = components_to_add
149
+ if components_to_remove is not None:
150
+ params["componentsToRemove"] = components_to_remove
151
+ if create_child is not None:
152
+ # Normalize vector fields within create_child (handles single object or array)
153
+ def normalize_child_params(child: Any, index: int | None = None) -> tuple[dict | None, str | None]:
154
+ prefix = f"create_child[{index}]" if index is not None else "create_child"
155
+ if not isinstance(child, dict):
156
+ return None, f"{prefix} must be a dict with child properties (name, primitive_type, position, etc.), got {type(child).__name__}"
157
+ child_params = dict(child)
158
+ for vec_field in ("position", "rotation", "scale"):
159
+ if vec_field in child_params and child_params[vec_field] is not None:
160
+ vec_val, vec_err = normalize_vector3(child_params[vec_field], f"{prefix}.{vec_field}")
161
+ if vec_err:
162
+ return None, vec_err
163
+ child_params[vec_field] = vec_val
164
+ return child_params, None
165
+
166
+ if isinstance(create_child, list):
167
+ # Array of children
168
+ normalized_children = []
169
+ for i, child in enumerate(create_child):
170
+ child_params, err = normalize_child_params(child, i)
171
+ if err:
172
+ return {"success": False, "message": err}
173
+ normalized_children.append(child_params)
174
+ params["createChild"] = normalized_children
175
+ else:
176
+ # Single child object
177
+ child_params, err = normalize_child_params(create_child)
178
+ if err:
179
+ return {"success": False, "message": err}
180
+ params["createChild"] = child_params
181
+
182
+ # Send command to Unity
183
+ response = await send_with_unity_instance(
184
+ async_send_command_with_retry, unity_instance, "manage_prefabs", params
185
+ )
186
+
187
+ # Return Unity response directly; ensure success field exists
188
+ # Handle MCPResponse objects (returned on error) by converting to dict
189
+ if hasattr(response, 'model_dump'):
190
+ return response.model_dump()
191
+ if isinstance(response, dict):
192
+ if "success" not in response:
193
+ response["success"] = False
194
+ return response
195
+ return {
196
+ "success": False,
197
+ "message": f"Unexpected response type: {type(response).__name__}"
198
+ }
199
+
200
+ except TimeoutError:
201
+ return {
202
+ "success": False,
203
+ "message": "Unity connection timeout. Please check if Unity is running and responsive."
204
+ }
205
+ except Exception as exc:
206
+ return {
207
+ "success": False,
208
+ "message": f"Error managing prefabs: {exc}"
209
+ }
@@ -0,0 +1,111 @@
1
+ from typing import Annotated, Literal, Any
2
+
3
+ from fastmcp import Context
4
+ from mcp.types import ToolAnnotations
5
+
6
+ from services.registry import mcp_for_unity_tool
7
+ from services.tools import get_unity_instance_from_context
8
+ from services.tools.utils import coerce_int, coerce_bool
9
+ from transport.unity_transport import send_with_unity_instance
10
+ from transport.legacy.unity_connection import async_send_command_with_retry
11
+ from services.tools.preflight import preflight
12
+
13
+
14
+ @mcp_for_unity_tool(
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
+ ),
20
+ )
21
+ async def manage_scene(
22
+ ctx: Context,
23
+ action: Annotated[Literal[
24
+ "create",
25
+ "load",
26
+ "save",
27
+ "get_hierarchy",
28
+ "get_active",
29
+ "get_build_settings",
30
+ "screenshot",
31
+ ], "Perform CRUD operations on Unity scenes, and capture a screenshot."],
32
+ name: Annotated[str, "Scene name."] | None = None,
33
+ path: Annotated[str, "Scene path."] | None = None,
34
+ build_index: Annotated[int | str,
35
+ "Unity build index (quote as string, e.g., '0')."] | 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,
40
+ # --- get_hierarchy paging/safety ---
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,
55
+ ) -> dict[str, Any]:
56
+ # Get active instance from session state
57
+ # Removed session_state import
58
+ unity_instance = get_unity_instance_from_context(ctx)
59
+ gate = await preflight(ctx, wait_for_no_compile=True, refresh_if_dirty=True)
60
+ if gate is not None:
61
+ return gate.model_dump()
62
+ try:
63
+ coerced_build_index = coerce_int(build_index, default=None)
64
+ coerced_super_size = coerce_int(screenshot_super_size, default=None)
65
+ coerced_page_size = coerce_int(page_size, default=None)
66
+ coerced_cursor = coerce_int(cursor, default=None)
67
+ coerced_max_nodes = coerce_int(max_nodes, default=None)
68
+ coerced_max_depth = coerce_int(max_depth, 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)
73
+
74
+ params: dict[str, Any] = {"action": action}
75
+ if name:
76
+ params["name"] = name
77
+ if path:
78
+ params["path"] = path
79
+ if coerced_build_index is not None:
80
+ params["buildIndex"] = coerced_build_index
81
+ if screenshot_file_name:
82
+ params["fileName"] = screenshot_file_name
83
+ if coerced_super_size is not None:
84
+ params["superSize"] = coerced_super_size
85
+
86
+ # get_hierarchy paging/safety params (optional)
87
+ if parent is not None:
88
+ params["parent"] = parent
89
+ if coerced_page_size is not None:
90
+ params["pageSize"] = coerced_page_size
91
+ if coerced_cursor is not None:
92
+ params["cursor"] = coerced_cursor
93
+ if coerced_max_nodes is not None:
94
+ params["maxNodes"] = coerced_max_nodes
95
+ if coerced_max_depth is not None:
96
+ params["maxDepth"] = coerced_max_depth
97
+ if coerced_max_children_per_node is not None:
98
+ params["maxChildrenPerNode"] = coerced_max_children_per_node
99
+ if coerced_include_transform is not None:
100
+ params["includeTransform"] = coerced_include_transform
101
+
102
+ # Use centralized retry helper with instance routing
103
+ response = await send_with_unity_instance(async_send_command_with_retry, unity_instance, "manage_scene", params)
104
+
105
+ # Preserve structured failure data; unwrap success into a friendlier shape
106
+ if isinstance(response, dict) and response.get("success"):
107
+ return {"success": True, "message": response.get("message", "Scene operation successful."), "data": response.get("data")}
108
+ return response if isinstance(response, dict) else {"success": False, "message": str(response)}
109
+
110
+ except Exception as e:
111
+ return {"success": False, "message": f"Python error managing scene: {str(e)}"}