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.
- cli/__init__.py +3 -0
- cli/commands/__init__.py +3 -0
- cli/commands/animation.py +84 -0
- cli/commands/asset.py +280 -0
- cli/commands/audio.py +125 -0
- cli/commands/batch.py +171 -0
- cli/commands/code.py +182 -0
- cli/commands/component.py +190 -0
- cli/commands/editor.py +447 -0
- cli/commands/gameobject.py +487 -0
- cli/commands/instance.py +93 -0
- cli/commands/lighting.py +123 -0
- cli/commands/material.py +239 -0
- cli/commands/prefab.py +248 -0
- cli/commands/scene.py +231 -0
- cli/commands/script.py +222 -0
- cli/commands/shader.py +226 -0
- cli/commands/texture.py +540 -0
- cli/commands/tool.py +58 -0
- cli/commands/ui.py +258 -0
- cli/commands/vfx.py +421 -0
- cli/main.py +281 -0
- cli/utils/__init__.py +31 -0
- cli/utils/config.py +58 -0
- cli/utils/confirmation.py +37 -0
- cli/utils/connection.py +254 -0
- cli/utils/constants.py +23 -0
- cli/utils/output.py +195 -0
- cli/utils/parsers.py +112 -0
- cli/utils/suggestions.py +34 -0
- core/__init__.py +0 -0
- core/config.py +67 -0
- core/constants.py +4 -0
- core/logging_decorator.py +37 -0
- core/telemetry.py +551 -0
- core/telemetry_decorator.py +164 -0
- main.py +845 -0
- mcpforunityserver-9.4.0b20260203025228.dist-info/METADATA +328 -0
- mcpforunityserver-9.4.0b20260203025228.dist-info/RECORD +105 -0
- mcpforunityserver-9.4.0b20260203025228.dist-info/WHEEL +5 -0
- mcpforunityserver-9.4.0b20260203025228.dist-info/entry_points.txt +3 -0
- mcpforunityserver-9.4.0b20260203025228.dist-info/licenses/LICENSE +21 -0
- mcpforunityserver-9.4.0b20260203025228.dist-info/top_level.txt +7 -0
- models/__init__.py +4 -0
- models/models.py +56 -0
- models/unity_response.py +70 -0
- services/__init__.py +0 -0
- services/api_key_service.py +235 -0
- services/custom_tool_service.py +499 -0
- services/registry/__init__.py +22 -0
- services/registry/resource_registry.py +53 -0
- services/registry/tool_registry.py +51 -0
- services/resources/__init__.py +86 -0
- services/resources/active_tool.py +48 -0
- services/resources/custom_tools.py +57 -0
- services/resources/editor_state.py +304 -0
- services/resources/gameobject.py +243 -0
- services/resources/layers.py +30 -0
- services/resources/menu_items.py +35 -0
- services/resources/prefab.py +191 -0
- services/resources/prefab_stage.py +40 -0
- services/resources/project_info.py +40 -0
- services/resources/selection.py +56 -0
- services/resources/tags.py +31 -0
- services/resources/tests.py +88 -0
- services/resources/unity_instances.py +125 -0
- services/resources/windows.py +48 -0
- services/state/external_changes_scanner.py +245 -0
- services/tools/__init__.py +83 -0
- services/tools/batch_execute.py +93 -0
- services/tools/debug_request_context.py +86 -0
- services/tools/execute_custom_tool.py +43 -0
- services/tools/execute_menu_item.py +32 -0
- services/tools/find_gameobjects.py +110 -0
- services/tools/find_in_file.py +181 -0
- services/tools/manage_asset.py +119 -0
- services/tools/manage_components.py +131 -0
- services/tools/manage_editor.py +64 -0
- services/tools/manage_gameobject.py +260 -0
- services/tools/manage_material.py +111 -0
- services/tools/manage_prefabs.py +209 -0
- services/tools/manage_scene.py +111 -0
- services/tools/manage_script.py +645 -0
- services/tools/manage_scriptable_object.py +87 -0
- services/tools/manage_shader.py +71 -0
- services/tools/manage_texture.py +581 -0
- services/tools/manage_vfx.py +120 -0
- services/tools/preflight.py +110 -0
- services/tools/read_console.py +151 -0
- services/tools/refresh_unity.py +153 -0
- services/tools/run_tests.py +317 -0
- services/tools/script_apply_edits.py +1006 -0
- services/tools/set_active_instance.py +120 -0
- services/tools/utils.py +348 -0
- transport/__init__.py +0 -0
- transport/legacy/port_discovery.py +329 -0
- transport/legacy/stdio_port_registry.py +65 -0
- transport/legacy/unity_connection.py +910 -0
- transport/models.py +68 -0
- transport/plugin_hub.py +787 -0
- transport/plugin_registry.py +182 -0
- transport/unity_instance_middleware.py +262 -0
- transport/unity_transport.py +94 -0
- utils/focus_nudge.py +589 -0
- utils/module_discovery.py +55 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool for managing components on GameObjects in Unity.
|
|
3
|
+
Supports add, remove, and set_property operations.
|
|
4
|
+
"""
|
|
5
|
+
from typing import Annotated, Any, Literal
|
|
6
|
+
|
|
7
|
+
from fastmcp import Context
|
|
8
|
+
from services.registry import mcp_for_unity_tool
|
|
9
|
+
from services.tools import get_unity_instance_from_context
|
|
10
|
+
from transport.unity_transport import send_with_unity_instance
|
|
11
|
+
from transport.legacy.unity_connection import async_send_command_with_retry
|
|
12
|
+
from services.tools.utils import parse_json_payload, normalize_properties
|
|
13
|
+
from services.tools.preflight import preflight
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@mcp_for_unity_tool(
|
|
17
|
+
description="Manages components on GameObjects (add, remove, set_property). For reading component data, use the mcpforunity://scene/gameobject/{id}/components resource."
|
|
18
|
+
)
|
|
19
|
+
async def manage_components(
|
|
20
|
+
ctx: Context,
|
|
21
|
+
action: Annotated[
|
|
22
|
+
Literal["add", "remove", "set_property"],
|
|
23
|
+
"Action to perform: add (add component), remove (remove component), set_property (set component property)"
|
|
24
|
+
],
|
|
25
|
+
target: Annotated[
|
|
26
|
+
str | int,
|
|
27
|
+
"Target GameObject - instance ID (preferred) or name/path"
|
|
28
|
+
],
|
|
29
|
+
component_type: Annotated[
|
|
30
|
+
str,
|
|
31
|
+
"Component type name (e.g., 'Rigidbody', 'BoxCollider', 'MyScript')"
|
|
32
|
+
],
|
|
33
|
+
search_method: Annotated[
|
|
34
|
+
Literal["by_id", "by_name", "by_path"],
|
|
35
|
+
"How to find the target GameObject"
|
|
36
|
+
] | None = None,
|
|
37
|
+
# For set_property action - single property
|
|
38
|
+
property: Annotated[str,
|
|
39
|
+
"Property name to set (for set_property action)"] | None = None,
|
|
40
|
+
value: Annotated[str | int | float | bool | dict | list ,
|
|
41
|
+
"Value to set (for set_property action)"] | None = None,
|
|
42
|
+
# For add/set_property - multiple properties
|
|
43
|
+
properties: Annotated[
|
|
44
|
+
dict[str, Any],
|
|
45
|
+
"Dictionary of property names to values. Example: {\"mass\": 5.0, \"useGravity\": false}"
|
|
46
|
+
] | None = None,
|
|
47
|
+
) -> dict[str, Any]:
|
|
48
|
+
"""
|
|
49
|
+
Manage components on GameObjects.
|
|
50
|
+
|
|
51
|
+
Actions:
|
|
52
|
+
- add: Add a new component to a GameObject
|
|
53
|
+
- remove: Remove a component from a GameObject
|
|
54
|
+
- set_property: Set one or more properties on a component
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
- Add Rigidbody: action="add", target="Player", component_type="Rigidbody"
|
|
58
|
+
- Remove BoxCollider: action="remove", target=-12345, component_type="BoxCollider"
|
|
59
|
+
- Set single property: action="set_property", target="Enemy", component_type="Rigidbody", property="mass", value=5.0
|
|
60
|
+
- Set multiple properties: action="set_property", target="Enemy", component_type="Rigidbody", properties={"mass": 5.0, "useGravity": false}
|
|
61
|
+
"""
|
|
62
|
+
unity_instance = get_unity_instance_from_context(ctx)
|
|
63
|
+
|
|
64
|
+
gate = await preflight(ctx, wait_for_no_compile=True, refresh_if_dirty=True)
|
|
65
|
+
if gate is not None:
|
|
66
|
+
return gate.model_dump()
|
|
67
|
+
|
|
68
|
+
if not action:
|
|
69
|
+
return {
|
|
70
|
+
"success": False,
|
|
71
|
+
"message": "Missing required parameter 'action'. Valid actions: add, remove, set_property"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if not target:
|
|
75
|
+
return {
|
|
76
|
+
"success": False,
|
|
77
|
+
"message": "Missing required parameter 'target'. Specify GameObject instance ID or name."
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if not component_type:
|
|
81
|
+
return {
|
|
82
|
+
"success": False,
|
|
83
|
+
"message": "Missing required parameter 'component_type'. Specify the component type name."
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# --- Normalize properties with detailed error handling ---
|
|
87
|
+
properties, props_error = normalize_properties(properties)
|
|
88
|
+
if props_error:
|
|
89
|
+
return {"success": False, "message": props_error}
|
|
90
|
+
|
|
91
|
+
# --- Validate value parameter for serialization issues ---
|
|
92
|
+
if value is not None and isinstance(value, str) and value in ("[object Object]", "undefined"):
|
|
93
|
+
return {"success": False, "message": f"value received invalid input: '{value}'. Expected an actual value."}
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
params = {
|
|
97
|
+
"action": action,
|
|
98
|
+
"target": target,
|
|
99
|
+
"componentType": component_type,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if search_method:
|
|
103
|
+
params["searchMethod"] = search_method
|
|
104
|
+
|
|
105
|
+
if action == "set_property":
|
|
106
|
+
if property and value is not None:
|
|
107
|
+
params["property"] = property
|
|
108
|
+
params["value"] = value
|
|
109
|
+
if properties:
|
|
110
|
+
params["properties"] = properties
|
|
111
|
+
|
|
112
|
+
if action == "add" and properties:
|
|
113
|
+
params["properties"] = properties
|
|
114
|
+
|
|
115
|
+
response = await send_with_unity_instance(
|
|
116
|
+
async_send_command_with_retry,
|
|
117
|
+
unity_instance,
|
|
118
|
+
"manage_components",
|
|
119
|
+
params,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if isinstance(response, dict) and response.get("success"):
|
|
123
|
+
return {
|
|
124
|
+
"success": True,
|
|
125
|
+
"message": response.get("message", f"Component {action} successful."),
|
|
126
|
+
"data": response.get("data")
|
|
127
|
+
}
|
|
128
|
+
return response if isinstance(response, dict) else {"success": False, "message": str(response)}
|
|
129
|
+
|
|
130
|
+
except Exception as e:
|
|
131
|
+
return {"success": False, "message": f"Error managing component: {e!s}"}
|
|
@@ -0,0 +1,64 @@
|
|
|
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 core.telemetry import is_telemetry_enabled, record_tool_usage
|
|
8
|
+
from services.tools import get_unity_instance_from_context
|
|
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.utils import coerce_bool
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@mcp_for_unity_tool(
|
|
15
|
+
description="Controls and queries the Unity editor's state and settings. Tip: pass booleans as true/false; if your client only sends strings, 'true'/'false' are accepted. Read-only actions: telemetry_status, telemetry_ping. Modifying actions: play, pause, stop, set_active_tool, add_tag, remove_tag, add_layer, remove_layer.",
|
|
16
|
+
annotations=ToolAnnotations(
|
|
17
|
+
title="Manage Editor",
|
|
18
|
+
),
|
|
19
|
+
)
|
|
20
|
+
async def manage_editor(
|
|
21
|
+
ctx: Context,
|
|
22
|
+
action: Annotated[Literal["telemetry_status", "telemetry_ping", "play", "pause", "stop", "set_active_tool", "add_tag", "remove_tag", "add_layer", "remove_layer"], "Get and update the Unity Editor state."],
|
|
23
|
+
wait_for_completion: Annotated[bool | str,
|
|
24
|
+
"Optional. If True, waits for certain actions (accepts true/false or 'true'/'false')"] | None = None,
|
|
25
|
+
tool_name: Annotated[str,
|
|
26
|
+
"Tool name when setting active tool"] | None = None,
|
|
27
|
+
tag_name: Annotated[str,
|
|
28
|
+
"Tag name when adding and removing tags"] | None = None,
|
|
29
|
+
layer_name: Annotated[str,
|
|
30
|
+
"Layer name when adding and removing layers"] | None = None,
|
|
31
|
+
) -> dict[str, Any]:
|
|
32
|
+
# Get active instance from request state (injected by middleware)
|
|
33
|
+
unity_instance = get_unity_instance_from_context(ctx)
|
|
34
|
+
|
|
35
|
+
wait_for_completion = coerce_bool(wait_for_completion)
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
# Diagnostics: quick telemetry checks
|
|
39
|
+
if action == "telemetry_status":
|
|
40
|
+
return {"success": True, "telemetry_enabled": is_telemetry_enabled()}
|
|
41
|
+
|
|
42
|
+
if action == "telemetry_ping":
|
|
43
|
+
record_tool_usage("diagnostic_ping", True, 1.0, None)
|
|
44
|
+
return {"success": True, "message": "telemetry ping queued"}
|
|
45
|
+
# Prepare parameters, removing None values
|
|
46
|
+
params = {
|
|
47
|
+
"action": action,
|
|
48
|
+
"waitForCompletion": wait_for_completion,
|
|
49
|
+
"toolName": tool_name,
|
|
50
|
+
"tagName": tag_name,
|
|
51
|
+
"layerName": layer_name,
|
|
52
|
+
}
|
|
53
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
54
|
+
|
|
55
|
+
# Send command using centralized retry helper with instance routing
|
|
56
|
+
response = await send_with_unity_instance(async_send_command_with_retry, unity_instance, "manage_editor", params)
|
|
57
|
+
|
|
58
|
+
# Preserve structured failure data; unwrap success into a friendlier shape
|
|
59
|
+
if isinstance(response, dict) and response.get("success"):
|
|
60
|
+
return {"success": True, "message": response.get("message", "Editor operation successful."), "data": response.get("data")}
|
|
61
|
+
return response if isinstance(response, dict) else {"success": False, "message": str(response)}
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
return {"success": False, "message": f"Python error managing editor: {str(e)}"}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Annotated, Any, Literal
|
|
3
|
+
|
|
4
|
+
from fastmcp import Context
|
|
5
|
+
from mcp.types import ToolAnnotations
|
|
6
|
+
|
|
7
|
+
from services.registry import mcp_for_unity_tool
|
|
8
|
+
from services.tools import get_unity_instance_from_context
|
|
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.utils import coerce_bool, parse_json_payload, coerce_int, normalize_vector3
|
|
12
|
+
from services.tools.preflight import preflight
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _normalize_component_properties(value: Any) -> tuple[dict[str, dict[str, Any]] | None, str | None]:
|
|
16
|
+
"""
|
|
17
|
+
Robustly normalize component_properties to a dict.
|
|
18
|
+
Returns (parsed_dict, error_message). If error_message is set, parsed_dict is None.
|
|
19
|
+
"""
|
|
20
|
+
if value is None:
|
|
21
|
+
return None, None
|
|
22
|
+
|
|
23
|
+
# Already a dict - validate structure
|
|
24
|
+
if isinstance(value, dict):
|
|
25
|
+
return value, None
|
|
26
|
+
|
|
27
|
+
# Try parsing as JSON string
|
|
28
|
+
if isinstance(value, str):
|
|
29
|
+
# Check for obviously invalid values
|
|
30
|
+
if value in ("[object Object]", "undefined", "null", ""):
|
|
31
|
+
return None, f"component_properties received invalid value: '{value}'. Expected a JSON object like {{\"ComponentName\": {{\"property\": value}}}}"
|
|
32
|
+
|
|
33
|
+
parsed = parse_json_payload(value)
|
|
34
|
+
if isinstance(parsed, dict):
|
|
35
|
+
return parsed, None
|
|
36
|
+
|
|
37
|
+
return None, f"component_properties must be a JSON object (dict), got string that parsed to {type(parsed).__name__}"
|
|
38
|
+
|
|
39
|
+
return None, f"component_properties must be a dict or JSON string, got {type(value).__name__}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@mcp_for_unity_tool(
|
|
43
|
+
description="Performs CRUD operations on GameObjects. Actions: create, modify, delete, duplicate, move_relative. For finding GameObjects use find_gameobjects tool. For component operations use manage_components tool.",
|
|
44
|
+
annotations=ToolAnnotations(
|
|
45
|
+
title="Manage GameObject",
|
|
46
|
+
destructiveHint=True,
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
async def manage_gameobject(
|
|
50
|
+
ctx: Context,
|
|
51
|
+
action: Annotated[Literal["create", "modify", "delete", "duplicate",
|
|
52
|
+
"move_relative"], "Action to perform on GameObject."] | None = None,
|
|
53
|
+
target: Annotated[str,
|
|
54
|
+
"GameObject identifier by name or path for modify/delete/component actions"] | None = None,
|
|
55
|
+
search_method: Annotated[Literal["by_id", "by_name", "by_path", "by_tag", "by_layer", "by_component"],
|
|
56
|
+
"How to find objects. Used with 'find' and some 'target' lookups."] | None = None,
|
|
57
|
+
name: Annotated[str,
|
|
58
|
+
"GameObject name for 'create' (initial name) and 'modify' (rename) actions ONLY. For 'find' action, use 'search_term' instead."] | None = None,
|
|
59
|
+
tag: Annotated[str,
|
|
60
|
+
"Tag name - used for both 'create' (initial tag) and 'modify' (change tag)"] | None = None,
|
|
61
|
+
parent: Annotated[str,
|
|
62
|
+
"Parent GameObject reference - used for both 'create' (initial parent) and 'modify' (change parent)"] | 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,
|
|
69
|
+
components_to_add: Annotated[list[str],
|
|
70
|
+
"List of component names to add"] | None = None,
|
|
71
|
+
primitive_type: Annotated[str,
|
|
72
|
+
"Primitive type for 'create' action"] | None = None,
|
|
73
|
+
save_as_prefab: Annotated[bool | str,
|
|
74
|
+
"If True, saves the created GameObject as a prefab (accepts true/false or 'true'/'false')"] | None = None,
|
|
75
|
+
prefab_path: Annotated[str, "Path for prefab creation"] | None = None,
|
|
76
|
+
prefab_folder: Annotated[str,
|
|
77
|
+
"Folder for prefab creation"] | None = None,
|
|
78
|
+
# --- Parameters for 'modify' ---
|
|
79
|
+
set_active: Annotated[bool | str,
|
|
80
|
+
"If True, sets the GameObject active (accepts true/false or 'true'/'false')"] | None = None,
|
|
81
|
+
layer: Annotated[str, "Layer name"] | None = None,
|
|
82
|
+
components_to_remove: Annotated[list[str],
|
|
83
|
+
"List of component names to remove"] | None = None,
|
|
84
|
+
component_properties: Annotated[dict[str, dict[str, Any]],
|
|
85
|
+
"""Dictionary of component names to their properties to set. For example:
|
|
86
|
+
`{"MyScript": {"otherObject": {"find": "Player", "method": "by_name"}}}` assigns GameObject
|
|
87
|
+
`{"MyScript": {"playerHealth": {"find": "Player", "component": "HealthComponent"}}}` assigns Component
|
|
88
|
+
Example set nested property:
|
|
89
|
+
- Access shared material: `{"MeshRenderer": {"sharedMaterial.color": [1, 0, 0, 1]}}`"""] | None = None,
|
|
90
|
+
# --- Parameters for 'find' ---
|
|
91
|
+
search_term: Annotated[str,
|
|
92
|
+
"Search term for 'find' action ONLY. Use this (not 'name') when searching for GameObjects."] | None = None,
|
|
93
|
+
find_all: Annotated[bool | str,
|
|
94
|
+
"If True, finds all GameObjects matching the search term (accepts true/false or 'true'/'false')"] | None = None,
|
|
95
|
+
search_in_children: Annotated[bool | str,
|
|
96
|
+
"If True, searches in children of the GameObject (accepts true/false or 'true'/'false')"] | None = None,
|
|
97
|
+
search_inactive: Annotated[bool | str,
|
|
98
|
+
"If True, searches inactive GameObjects (accepts true/false or 'true'/'false')"] | None = None,
|
|
99
|
+
# -- Component Management Arguments --
|
|
100
|
+
component_name: Annotated[str,
|
|
101
|
+
"Component name for 'add_component' and 'remove_component' actions"] | None = None,
|
|
102
|
+
# Controls whether serialization of private [SerializeField] fields is included
|
|
103
|
+
includeNonPublicSerialized: Annotated[bool | str,
|
|
104
|
+
"Controls whether serialization of private [SerializeField] fields is included (accepts true/false or 'true'/'false')"] | None = None,
|
|
105
|
+
# --- Paging/safety for get_components ---
|
|
106
|
+
page_size: Annotated[int | str,
|
|
107
|
+
"Page size for get_components paging."] | None = None,
|
|
108
|
+
cursor: Annotated[int | str,
|
|
109
|
+
"Opaque cursor for get_components paging (offset)."] | None = None,
|
|
110
|
+
max_components: Annotated[int | str,
|
|
111
|
+
"Hard cap on returned components per request (safety)."] | None = None,
|
|
112
|
+
include_properties: Annotated[bool | str,
|
|
113
|
+
"If true, include serialized component properties (bounded)."] | None = None,
|
|
114
|
+
# --- Parameters for 'duplicate' ---
|
|
115
|
+
new_name: Annotated[str,
|
|
116
|
+
"New name for the duplicated object (default: SourceName_Copy)"] | None = None,
|
|
117
|
+
offset: Annotated[list[float] | str,
|
|
118
|
+
"Offset from original/reference position as [x, y, z] array (list or JSON string)"] | None = None,
|
|
119
|
+
# --- Parameters for 'move_relative' ---
|
|
120
|
+
reference_object: Annotated[str,
|
|
121
|
+
"Reference object for relative movement (required for move_relative)"] | None = None,
|
|
122
|
+
direction: Annotated[Literal["left", "right", "up", "down", "forward", "back", "front", "backward", "behind"],
|
|
123
|
+
"Direction for relative movement (e.g., 'right', 'up', 'forward')"] | None = None,
|
|
124
|
+
distance: Annotated[float,
|
|
125
|
+
"Distance to move in the specified direction (default: 1.0)"] | None = None,
|
|
126
|
+
world_space: Annotated[bool | str,
|
|
127
|
+
"If True (default), use world space directions; if False, use reference object's local directions"] | None = None,
|
|
128
|
+
) -> dict[str, Any]:
|
|
129
|
+
# Get active instance from session state
|
|
130
|
+
# Removed session_state import
|
|
131
|
+
unity_instance = get_unity_instance_from_context(ctx)
|
|
132
|
+
|
|
133
|
+
gate = await preflight(ctx, wait_for_no_compile=True, refresh_if_dirty=True)
|
|
134
|
+
if gate is not None:
|
|
135
|
+
return gate.model_dump()
|
|
136
|
+
|
|
137
|
+
if action is None:
|
|
138
|
+
return {
|
|
139
|
+
"success": False,
|
|
140
|
+
"message": "Missing required parameter 'action'. Valid actions: create, modify, delete, duplicate, move_relative. For finding GameObjects use find_gameobjects tool. For component operations use manage_components tool."
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# --- Normalize vector parameters with detailed error handling ---
|
|
144
|
+
position, position_error = normalize_vector3(position, "position")
|
|
145
|
+
if position_error:
|
|
146
|
+
return {"success": False, "message": position_error}
|
|
147
|
+
rotation, rotation_error = normalize_vector3(rotation, "rotation")
|
|
148
|
+
if rotation_error:
|
|
149
|
+
return {"success": False, "message": rotation_error}
|
|
150
|
+
scale, scale_error = normalize_vector3(scale, "scale")
|
|
151
|
+
if scale_error:
|
|
152
|
+
return {"success": False, "message": scale_error}
|
|
153
|
+
offset, offset_error = normalize_vector3(offset, "offset")
|
|
154
|
+
if offset_error:
|
|
155
|
+
return {"success": False, "message": offset_error}
|
|
156
|
+
|
|
157
|
+
# --- Normalize boolean parameters ---
|
|
158
|
+
save_as_prefab = coerce_bool(save_as_prefab)
|
|
159
|
+
set_active = coerce_bool(set_active)
|
|
160
|
+
find_all = coerce_bool(find_all)
|
|
161
|
+
search_in_children = coerce_bool(search_in_children)
|
|
162
|
+
search_inactive = coerce_bool(search_inactive)
|
|
163
|
+
includeNonPublicSerialized = coerce_bool(includeNonPublicSerialized)
|
|
164
|
+
include_properties = coerce_bool(include_properties)
|
|
165
|
+
world_space = coerce_bool(world_space, default=True)
|
|
166
|
+
|
|
167
|
+
# --- Normalize integer parameters ---
|
|
168
|
+
page_size = coerce_int(page_size, default=None)
|
|
169
|
+
cursor = coerce_int(cursor, default=None)
|
|
170
|
+
max_components = coerce_int(max_components, default=None)
|
|
171
|
+
|
|
172
|
+
# --- Normalize component_properties with detailed error handling ---
|
|
173
|
+
component_properties, comp_props_error = _normalize_component_properties(
|
|
174
|
+
component_properties)
|
|
175
|
+
if comp_props_error:
|
|
176
|
+
return {"success": False, "message": comp_props_error}
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
# Validate parameter usage to prevent silent failures
|
|
180
|
+
if action in ["create", "modify"]:
|
|
181
|
+
if search_term is not None:
|
|
182
|
+
return {
|
|
183
|
+
"success": False,
|
|
184
|
+
"message": f"For '{action}' action, use 'name' parameter, not 'search_term'."
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# Prepare parameters, removing None values
|
|
188
|
+
params = {
|
|
189
|
+
"action": action,
|
|
190
|
+
"target": target,
|
|
191
|
+
"searchMethod": search_method,
|
|
192
|
+
"name": name,
|
|
193
|
+
"tag": tag,
|
|
194
|
+
"parent": parent,
|
|
195
|
+
"position": position,
|
|
196
|
+
"rotation": rotation,
|
|
197
|
+
"scale": scale,
|
|
198
|
+
"componentsToAdd": components_to_add,
|
|
199
|
+
"primitiveType": primitive_type,
|
|
200
|
+
"saveAsPrefab": save_as_prefab,
|
|
201
|
+
"prefabPath": prefab_path,
|
|
202
|
+
"prefabFolder": prefab_folder,
|
|
203
|
+
"setActive": set_active,
|
|
204
|
+
"layer": layer,
|
|
205
|
+
"componentsToRemove": components_to_remove,
|
|
206
|
+
"componentProperties": component_properties,
|
|
207
|
+
"searchTerm": search_term,
|
|
208
|
+
"findAll": find_all,
|
|
209
|
+
"searchInChildren": search_in_children,
|
|
210
|
+
"searchInactive": search_inactive,
|
|
211
|
+
"componentName": component_name,
|
|
212
|
+
"includeNonPublicSerialized": includeNonPublicSerialized,
|
|
213
|
+
"pageSize": page_size,
|
|
214
|
+
"cursor": cursor,
|
|
215
|
+
"maxComponents": max_components,
|
|
216
|
+
"includeProperties": include_properties,
|
|
217
|
+
# Parameters for 'duplicate'
|
|
218
|
+
"new_name": new_name,
|
|
219
|
+
"offset": offset,
|
|
220
|
+
# Parameters for 'move_relative'
|
|
221
|
+
"reference_object": reference_object,
|
|
222
|
+
"direction": direction,
|
|
223
|
+
"distance": distance,
|
|
224
|
+
"world_space": world_space,
|
|
225
|
+
}
|
|
226
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
227
|
+
|
|
228
|
+
# --- Handle Prefab Path Logic ---
|
|
229
|
+
# Check if 'saveAsPrefab' is explicitly True in params
|
|
230
|
+
if action == "create" and params.get("saveAsPrefab"):
|
|
231
|
+
if "prefabPath" not in params:
|
|
232
|
+
if "name" not in params or not params["name"]:
|
|
233
|
+
return {"success": False, "message": "Cannot create default prefab path: 'name' parameter is missing."}
|
|
234
|
+
# Use the provided prefab_folder (which has a default) and the name to construct the path
|
|
235
|
+
constructed_path = f"{prefab_folder}/{params['name']}.prefab"
|
|
236
|
+
# Ensure clean path separators (Unity prefers '/')
|
|
237
|
+
params["prefabPath"] = constructed_path.replace("\\", "/")
|
|
238
|
+
elif not params["prefabPath"].lower().endswith(".prefab"):
|
|
239
|
+
return {"success": False, "message": f"Invalid prefab_path: '{params['prefabPath']}' must end with .prefab"}
|
|
240
|
+
# Ensure prefabFolder itself isn't sent if prefabPath was constructed or provided
|
|
241
|
+
# The C# side only needs the final prefabPath
|
|
242
|
+
params.pop("prefabFolder", None)
|
|
243
|
+
# --------------------------------
|
|
244
|
+
|
|
245
|
+
# Use centralized retry helper with instance routing
|
|
246
|
+
response = await send_with_unity_instance(
|
|
247
|
+
async_send_command_with_retry,
|
|
248
|
+
unity_instance,
|
|
249
|
+
"manage_gameobject",
|
|
250
|
+
params,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Check if the response indicates success
|
|
254
|
+
# If the response is not successful, raise an exception with the error message
|
|
255
|
+
if isinstance(response, dict) and response.get("success"):
|
|
256
|
+
return {"success": True, "message": response.get("message", "GameObject operation successful."), "data": response.get("data")}
|
|
257
|
+
return response if isinstance(response, dict) else {"success": False, "message": str(response)}
|
|
258
|
+
|
|
259
|
+
except Exception as e:
|
|
260
|
+
return {"success": False, "message": f"Python error managing GameObject: {e!s}"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Defines the manage_material tool for interacting with Unity materials.
|
|
3
|
+
"""
|
|
4
|
+
import json
|
|
5
|
+
from typing import Annotated, Any, Literal
|
|
6
|
+
|
|
7
|
+
from fastmcp import Context
|
|
8
|
+
from mcp.types import ToolAnnotations
|
|
9
|
+
|
|
10
|
+
from services.registry import mcp_for_unity_tool
|
|
11
|
+
from services.tools import get_unity_instance_from_context
|
|
12
|
+
from services.tools.utils import parse_json_payload, coerce_int, normalize_properties, normalize_color
|
|
13
|
+
from transport.unity_transport import send_with_unity_instance
|
|
14
|
+
from transport.legacy.unity_connection import async_send_command_with_retry
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@mcp_for_unity_tool(
|
|
18
|
+
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.",
|
|
19
|
+
annotations=ToolAnnotations(
|
|
20
|
+
title="Manage Material",
|
|
21
|
+
destructiveHint=True,
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
async def manage_material(
|
|
25
|
+
ctx: Context,
|
|
26
|
+
action: Annotated[Literal[
|
|
27
|
+
"ping",
|
|
28
|
+
"create",
|
|
29
|
+
"set_material_shader_property",
|
|
30
|
+
"set_material_color",
|
|
31
|
+
"assign_material_to_renderer",
|
|
32
|
+
"set_renderer_color",
|
|
33
|
+
"get_material_info"
|
|
34
|
+
], "Action to perform."],
|
|
35
|
+
|
|
36
|
+
# Common / Shared
|
|
37
|
+
material_path: Annotated[str,
|
|
38
|
+
"Path to material asset (Assets/...)"] | None = None,
|
|
39
|
+
property: Annotated[str,
|
|
40
|
+
"Shader property name (e.g., _BaseColor, _MainTex)"] | None = None,
|
|
41
|
+
|
|
42
|
+
# create
|
|
43
|
+
shader: Annotated[str, "Shader name (default: Standard)"] | None = None,
|
|
44
|
+
properties: Annotated[dict[str, Any],
|
|
45
|
+
"Initial properties to set as {name: value} dict."] | None = None,
|
|
46
|
+
|
|
47
|
+
# set_material_shader_property
|
|
48
|
+
value: Annotated[list | float | int | str | bool | None,
|
|
49
|
+
"Value to set (color array, float, texture path/instruction)"] | None = None,
|
|
50
|
+
|
|
51
|
+
# set_material_color / set_renderer_color
|
|
52
|
+
color: Annotated[list[float] | dict[str, float] | str,
|
|
53
|
+
"Color as [r, g, b] or [r, g, b, a] array, {r, g, b, a} object, or JSON string."] | None = None,
|
|
54
|
+
|
|
55
|
+
# assign_material_to_renderer / set_renderer_color
|
|
56
|
+
target: Annotated[str,
|
|
57
|
+
"Target GameObject (name, path, or find instruction)"] | None = None,
|
|
58
|
+
search_method: Annotated[Literal["by_name", "by_path", "by_tag",
|
|
59
|
+
"by_layer", "by_component"], "Search method for target"] | None = None,
|
|
60
|
+
slot: Annotated[int, "Material slot index (0-based)"] | None = None,
|
|
61
|
+
mode: Annotated[Literal["shared", "instance", "property_block"],
|
|
62
|
+
"Assignment/modification mode"] | None = None,
|
|
63
|
+
|
|
64
|
+
) -> dict[str, Any]:
|
|
65
|
+
unity_instance = get_unity_instance_from_context(ctx)
|
|
66
|
+
|
|
67
|
+
# --- Normalize color with validation ---
|
|
68
|
+
color, color_error = normalize_color(color, output_range="float")
|
|
69
|
+
if color_error:
|
|
70
|
+
return {"success": False, "message": color_error}
|
|
71
|
+
|
|
72
|
+
# --- Normalize properties with validation ---
|
|
73
|
+
properties, props_error = normalize_properties(properties)
|
|
74
|
+
if props_error:
|
|
75
|
+
return {"success": False, "message": props_error}
|
|
76
|
+
|
|
77
|
+
# --- Normalize value (parse JSON if string) ---
|
|
78
|
+
value = parse_json_payload(value)
|
|
79
|
+
if isinstance(value, str) and value in ("[object Object]", "undefined"):
|
|
80
|
+
return {"success": False, "message": f"value received invalid input: '{value}'"}
|
|
81
|
+
|
|
82
|
+
# --- Normalize slot to int ---
|
|
83
|
+
slot = coerce_int(slot)
|
|
84
|
+
|
|
85
|
+
# Prepare parameters for the C# handler
|
|
86
|
+
params_dict = {
|
|
87
|
+
"action": action.lower(),
|
|
88
|
+
"materialPath": material_path,
|
|
89
|
+
"shader": shader,
|
|
90
|
+
"properties": properties,
|
|
91
|
+
"property": property,
|
|
92
|
+
"value": value,
|
|
93
|
+
"color": color,
|
|
94
|
+
"target": target,
|
|
95
|
+
"searchMethod": search_method,
|
|
96
|
+
"slot": slot,
|
|
97
|
+
"mode": mode
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Remove None values
|
|
101
|
+
params_dict = {k: v for k, v in params_dict.items() if v is not None}
|
|
102
|
+
|
|
103
|
+
# Use centralized async retry helper with instance routing
|
|
104
|
+
result = await send_with_unity_instance(
|
|
105
|
+
async_send_command_with_retry,
|
|
106
|
+
unity_instance,
|
|
107
|
+
"manage_material",
|
|
108
|
+
params_dict,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return result if isinstance(result, dict) else {"success": False, "message": str(result)}
|