Skip to content
Open
4 changes: 3 additions & 1 deletion Server/src/services/tools/manage_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,12 @@ async def manage_components(
if props_error:
return {"success": False, "message": props_error}

# --- Validate value parameter for serialization issues ---
# --- Validate/normalize value parameter for serialization issues ---
if value is not None and isinstance(value, str) and value in ("[object Object]", "undefined"):
return {"success": False, "message": f"value received invalid input: '{value}'. Expected an actual value."}

value = parse_json_payload(value)

try:
params = {
"action": action,
Expand Down
104 changes: 104 additions & 0 deletions Server/tests/integration/test_manage_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,110 @@ async def fake_send(cmd, params, **kwargs):
assert captured["params"]["properties"] == {"mass": 10.0}


@pytest.mark.asyncio
async def test_manage_components_set_property_single_json_value(monkeypatch):
"""Test Color-style object payloads are parsed from JSON strings."""
captured = {}

async def fake_send(cmd, params, **kwargs):
captured["params"] = params
return {"success": True, "data": {"instanceID": 12345}}

monkeypatch.setattr(
manage_comp_mod,
"async_send_command_with_retry",
fake_send,
)

resp = await manage_comp_mod.manage_components(
ctx=DummyContext(),
action="set_property",
target="Light",
component_type="Light",
property="color",
value='{"r": 1.0, "g": 1.0, "b": 0.0, "a": 1.0}',
)

assert resp.get("success") is True
assert captured["params"]["property"] == "color"
assert captured["params"]["value"] == {
"r": 1.0,
"g": 1.0,
"b": 0.0,
"a": 1.0,
}


@pytest.mark.asyncio
@pytest.mark.parametrize(
("property_name", "raw_value", "expected_value"),
[
(
"position",
"[1.0, 2.0, 3.0]",
[1.0, 2.0, 3.0],
),
(
"localScale",
'{"x": 2.0, "y": 3.0, "z": 4.0}',
{"x": 2.0, "y": 3.0, "z": 4.0},
),
(
"rotation",
'{"x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0}',
{"x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0},
),
(
"color",
'{"r": 1.0, "g": 1.0, "b": 0.0, "a": 1.0}',
{"r": 1.0, "g": 1.0, "b": 0.0, "a": 1.0},
),
(
"pixelRect",
'{"x": 10.0, "y": 20.0, "width": 1920.0, "height": 1080.0}',
{"x": 10.0, "y": 20.0, "width": 1920.0, "height": 1080.0},
),
],
)
async def test_manage_components_set_property_single_json_value_for_unity_structs(
monkeypatch,
property_name,
raw_value,
expected_value,
):
"""Test JSON-string single values preserve the intended Unity struct shape.

These cases document the payload forms we rely on:
- Vector3 accepts array or object JSON
- Quaternion accepts object JSON
- Color and Rect should use object JSON matching Unity field names
"""
captured = {}

async def fake_send(cmd, params, **kwargs):
captured["params"] = params
return {"success": True, "data": {"instanceID": 12345}}

monkeypatch.setattr(
manage_comp_mod,
"async_send_command_with_retry",
fake_send,
)

resp = await manage_comp_mod.manage_components(
ctx=DummyContext(),
action="set_property",
target="TestObject",
component_type="Transform",
property=property_name,
value=raw_value,
)

assert resp.get("success") is True
assert captured["params"]["property"] == property_name
assert captured["params"]["value"] == expected_value


@pytest.mark.asyncio
async def test_manage_components_add_with_properties(monkeypatch):
"""Test adding a component with initial properties."""
Expand Down
20 changes: 20 additions & 0 deletions unity-mcp-skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,26 @@ color=[255, 0, 0, 255] # 0-255 range
color=[1.0, 0.0, 0.0, 1.0] # 0.0-1.0 normalized (auto-converted)
```

For `manage_components`, complex property values may arrive as JSON strings from some MCP clients.
The server now parses those JSON strings before forwarding them to Unity, but the Unity-side converter still matters:
- `Vector2` / `Vector3` / `Vector4` / `Quaternion`: array or object forms are both fine
- `Color` / `Rect`: prefer object form matching Unity field names

Examples:
```python
manage_components(action="set_property", target="Cube", component_type="Transform",
property="position", value="[1, 2, 3]") # Vector3 array is OK

manage_components(action="set_property", target="Cube", component_type="Transform",
property="localScale", value='{"x": 2, "y": 2, "z": 2}') # Vector3 object is OK

manage_components(action="set_property", target="Light", component_type="Light",
property="color", value='{"r": 1, "g": 0, "b": 0, "a": 1}') # Prefer object for Color

manage_components(action="set_property", target="Main Camera", component_type="Camera",
property="pixelRect", value='{"x": 0, "y": 0, "width": 1920, "height": 1080}') # Prefer object for Rect
```

### Paths
```python
# Assets-relative (default):
Expand Down