From b39b9dc3ffcb5d4006a20e503d182e766f0415ea Mon Sep 17 00:00:00 2001 From: Misaka Mikoto Date: Sat, 4 Apr 2026 20:14:28 +0800 Subject: [PATCH 1/7] When setting Color/Rect properties via `manage_components`, explicitly required to use the object format rather than an array. --- .../src/services/tools/manage_components.py | 4 +- .../integration/test_manage_components.py | 104 ++++++++++++++++++ unity-mcp-skill/SKILL.md | 20 ++++ 3 files changed, 127 insertions(+), 1 deletion(-) diff --git a/Server/src/services/tools/manage_components.py b/Server/src/services/tools/manage_components.py index da2cdba18..9d1d330b8 100644 --- a/Server/src/services/tools/manage_components.py +++ b/Server/src/services/tools/manage_components.py @@ -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, diff --git a/Server/tests/integration/test_manage_components.py b/Server/tests/integration/test_manage_components.py index 7e777088e..0aeea0504 100644 --- a/Server/tests/integration/test_manage_components.py +++ b/Server/tests/integration/test_manage_components.py @@ -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.""" diff --git a/unity-mcp-skill/SKILL.md b/unity-mcp-skill/SKILL.md index 905561cc7..0bde9dc15 100644 --- a/unity-mcp-skill/SKILL.md +++ b/unity-mcp-skill/SKILL.md @@ -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): From 1c12c45165c692adbe9d8d826e325db622e14d01 Mon Sep 17 00:00:00 2001 From: Misaka Mikoto Date: Sat, 4 Apr 2026 20:35:43 +0800 Subject: [PATCH 2/7] modify example code of SKILL.md indent from tab to space --- unity-mcp-skill/SKILL.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/unity-mcp-skill/SKILL.md b/unity-mcp-skill/SKILL.md index 0bde9dc15..049a5cb3b 100644 --- a/unity-mcp-skill/SKILL.md +++ b/unity-mcp-skill/SKILL.md @@ -172,16 +172,16 @@ The server now parses those JSON strings before forwarding them to Unity, but th Examples: ```python manage_components(action="set_property", target="Cube", component_type="Transform", - property="position", value="[1, 2, 3]") # Vector3 array is OK + 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 + 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 + 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 + property="pixelRect", value='{"x": 0, "y": 0, "width": 1920, "height": 1080}') # Prefer object for Rect ``` ### Paths From 172c8fc8b6ce0bdd6c6eac36b51b6987e28e37ba Mon Sep 17 00:00:00 2001 From: Misaka Mikoto Date: Sat, 4 Apr 2026 21:15:36 +0800 Subject: [PATCH 3/7] Modify `skill.md` to provide more precise description of the format for `Color` when used within `manage_components`. --- unity-mcp-skill/SKILL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/unity-mcp-skill/SKILL.md b/unity-mcp-skill/SKILL.md index 049a5cb3b..b31d467ed 100644 --- a/unity-mcp-skill/SKILL.md +++ b/unity-mcp-skill/SKILL.md @@ -167,7 +167,7 @@ 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 +- `Color` / `Rect`: use object form matching Unity field names on the `manage_components` path Examples: ```python @@ -178,10 +178,10 @@ manage_components(action="set_property", target="Cube", component_type="Transfor 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 + property="color", value='{"r": 1, "g": 0, "b": 0, "a": 1}') # Use object form 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 + property="pixelRect", value='{"x": 0, "y": 0, "width": 1920, "height": 1080}') # Use object form for Rect ``` ### Paths From 0a76f8376dc46046df4fee0690dda67c3c993c15 Mon Sep 17 00:00:00 2001 From: Misaka Mikoto Date: Sat, 4 Apr 2026 21:20:49 +0800 Subject: [PATCH 4/7] test: Add Test function `test_manage_components_set_property_single_plain_string_value_stays_string` --- .../integration/test_manage_components.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Server/tests/integration/test_manage_components.py b/Server/tests/integration/test_manage_components.py index 0aeea0504..51759ebbb 100644 --- a/Server/tests/integration/test_manage_components.py +++ b/Server/tests/integration/test_manage_components.py @@ -190,6 +190,36 @@ async def fake_send(cmd, params, **kwargs): } +@pytest.mark.asyncio +async def test_manage_components_set_property_single_plain_string_value_stays_string(monkeypatch): + """Test ordinary string values are forwarded unchanged.""" + 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="GameManager", + component_type="ExampleComponent", + property="displayName", + value="Player One", + ) + + assert resp.get("success") is True + assert captured["params"]["property"] == "displayName" + assert captured["params"]["value"] == "Player One" + assert isinstance(captured["params"]["value"], str) + + @pytest.mark.asyncio @pytest.mark.parametrize( ("property_name", "raw_value", "expected_value"), From 8175e85999e59ea3ec114fd73834e30f21f8a72d Mon Sep 17 00:00:00 2001 From: Misaka Mikoto Date: Sat, 4 Apr 2026 22:02:20 +0800 Subject: [PATCH 5/7] docs: Remove the Color/Rect formatting code examples from skill.md. --- unity-mcp-skill/SKILL.md | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/unity-mcp-skill/SKILL.md b/unity-mcp-skill/SKILL.md index b31d467ed..29165d94f 100644 --- a/unity-mcp-skill/SKILL.md +++ b/unity-mcp-skill/SKILL.md @@ -169,21 +169,6 @@ The server now parses those JSON strings before forwarding them to Unity, but th - `Vector2` / `Vector3` / `Vector4` / `Quaternion`: array or object forms are both fine - `Color` / `Rect`: use object form matching Unity field names on the `manage_components` path -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}') # Use object form for Color - -manage_components(action="set_property", target="Main Camera", component_type="Camera", - property="pixelRect", value='{"x": 0, "y": 0, "width": 1920, "height": 1080}') # Use object form for Rect -``` - ### Paths ```python # Assets-relative (default): From 26ac184db1f5a352e5fae2648841840217b59349 Mon Sep 17 00:00:00 2001 From: Misaka Mikoto Date: Sat, 4 Apr 2026 22:41:04 +0800 Subject: [PATCH 6/7] fix: 1. Remove the code examples for Color/Rect formatting in `skill.md` 2. Unity supports both array and object formats when parsing these two types. --- .../Serialization/UnityTypeConverters.cs | 23 +++- .../integration/test_manage_components.py | 124 ++++++------------ ...onversion_UnityStructArraySupport_Tests.cs | 47 +++++++ ...sion_UnityStructArraySupport_Tests.cs.meta | 11 ++ unity-mcp-skill/SKILL.md | 5 - 5 files changed, 117 insertions(+), 93 deletions(-) create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/PropertyConversion_UnityStructArraySupport_Tests.cs create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/PropertyConversion_UnityStructArraySupport_Tests.cs.meta diff --git a/MCPForUnity/Runtime/Serialization/UnityTypeConverters.cs b/MCPForUnity/Runtime/Serialization/UnityTypeConverters.cs index b593fb98e..e6fb207f2 100644 --- a/MCPForUnity/Runtime/Serialization/UnityTypeConverters.cs +++ b/MCPForUnity/Runtime/Serialization/UnityTypeConverters.cs @@ -113,12 +113,21 @@ public override void WriteJson(JsonWriter writer, Color value, JsonSerializer se public override Color ReadJson(JsonReader reader, Type objectType, Color existingValue, bool hasExistingValue, JsonSerializer serializer) { - JObject jo = JObject.Load(reader); + JToken token = JToken.Load(reader); + if (token is JArray arr && arr.Count >= 3) + { + float alpha = arr.Count >= 4 ? (float)arr[3] : 1f; + return new Color((float)arr[0], (float)arr[1], (float)arr[2], alpha); + } + + if (token is not JObject jo) + throw new JsonSerializationException($"Cannot deserialize Color from {token.Type}: '{token}'"); + return new Color( (float)jo["r"], (float)jo["g"], (float)jo["b"], - (float)jo["a"] + jo["a"] != null ? (float)jo["a"] : 1f ); } } @@ -141,7 +150,13 @@ public override void WriteJson(JsonWriter writer, Rect value, JsonSerializer ser public override Rect ReadJson(JsonReader reader, Type objectType, Rect existingValue, bool hasExistingValue, JsonSerializer serializer) { - JObject jo = JObject.Load(reader); + JToken token = JToken.Load(reader); + if (token is JArray arr && arr.Count >= 4) + return new Rect((float)arr[0], (float)arr[1], (float)arr[2], (float)arr[3]); + + if (token is not JObject jo) + throw new JsonSerializationException($"Cannot deserialize Rect from {token.Type}: '{token}'"); + return new Rect( (float)jo["x"], (float)jo["y"], @@ -468,4 +483,4 @@ private static bool IsValidGuid(string str) return true; } } -} \ No newline at end of file +} diff --git a/Server/tests/integration/test_manage_components.py b/Server/tests/integration/test_manage_components.py index 51759ebbb..5be5c6aa9 100644 --- a/Server/tests/integration/test_manage_components.py +++ b/Server/tests/integration/test_manage_components.py @@ -157,8 +157,35 @@ async def fake_send(cmd, params, **kwargs): @pytest.mark.asyncio -async def test_manage_components_set_property_single_json_value(monkeypatch): - """Test Color-style object payloads are parsed from JSON strings.""" +@pytest.mark.parametrize( + ("property_name", "raw_value", "expected_value"), + [ + ( + "position", + "[1.0, 2.0, 3.0]", + [1.0, 2.0, 3.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}, + ), + ], +) +async def test_manage_components_set_property_single_structured_json_value( + monkeypatch, + property_name, + raw_value, + expected_value, +): + """Test JSON-string single values are normalized before dispatch. + + The Python-side contract is intentionally generic: + - array-shaped JSON should become Python lists + - object-shaped JSON should become Python dicts + + Detailed Unity struct compatibility is covered by Unity-side tests. + """ captured = {} async def fake_send(cmd, params, **kwargs): @@ -174,25 +201,24 @@ async def fake_send(cmd, params, **kwargs): 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}', + target="TestObject", + component_type="Transform", + property=property_name, + value=raw_value, ) 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, - } + assert captured["params"]["property"] == property_name + assert captured["params"]["value"] == expected_value @pytest.mark.asyncio async def test_manage_components_set_property_single_plain_string_value_stays_string(monkeypatch): - """Test ordinary string values are forwarded unchanged.""" + """Test ordinary string values are forwarded unchanged. + + This guards the conservative behavior of parse_json_payload: plain strings + should not be coerced just because structured JSON-string values are now supported. + """ captured = {} async def fake_send(cmd, params, **kwargs): @@ -220,76 +246,6 @@ async def fake_send(cmd, params, **kwargs): assert isinstance(captured["params"]["value"], str) -@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.""" diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/PropertyConversion_UnityStructArraySupport_Tests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/PropertyConversion_UnityStructArraySupport_Tests.cs new file mode 100644 index 000000000..bcdb1ac7d --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/PropertyConversion_UnityStructArraySupport_Tests.cs @@ -0,0 +1,47 @@ +using NUnit.Framework; +using Newtonsoft.Json.Linq; +using UnityEngine; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnityTests.Editor.Tools +{ + /// + /// Guards array compatibility for Unity structs that commonly flow through + /// manage_components as JSON-stringified values. + /// + public class PropertyConversion_UnityStructArraySupport_Tests + { + [Test] + public void ConvertToType_ColorArrayWithAlpha_Succeeds() + { + var result = (Color)PropertyConversion.ConvertToType( + JArray.Parse("[1.0, 0.5, 0.25, 0.75]"), + typeof(Color) + ); + + Assert.AreEqual(new Color(1.0f, 0.5f, 0.25f, 0.75f), result); + } + + [Test] + public void ConvertToType_ColorArrayWithoutAlpha_DefaultsToOne() + { + var result = (Color)PropertyConversion.ConvertToType( + JArray.Parse("[1.0, 0.5, 0.25]"), + typeof(Color) + ); + + Assert.AreEqual(new Color(1.0f, 0.5f, 0.25f, 1.0f), result); + } + + [Test] + public void ConvertToType_RectArray_Succeeds() + { + var result = (Rect)PropertyConversion.ConvertToType( + JArray.Parse("[10.0, 20.0, 30.0, 40.0]"), + typeof(Rect) + ); + + Assert.AreEqual(new Rect(10.0f, 20.0f, 30.0f, 40.0f), result); + } + } +} diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/PropertyConversion_UnityStructArraySupport_Tests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/PropertyConversion_UnityStructArraySupport_Tests.cs.meta new file mode 100644 index 000000000..c1075f1d2 --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/PropertyConversion_UnityStructArraySupport_Tests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 81cb10f4d3334814a85f7e6d52db99f1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-mcp-skill/SKILL.md b/unity-mcp-skill/SKILL.md index 29165d94f..905561cc7 100644 --- a/unity-mcp-skill/SKILL.md +++ b/unity-mcp-skill/SKILL.md @@ -164,11 +164,6 @@ 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`: use object form matching Unity field names on the `manage_components` path - ### Paths ```python # Assets-relative (default): From 30f5079926c292c5838d69009461785053aeda0e Mon Sep 17 00:00:00 2001 From: Misaka Mikoto Date: Sat, 4 Apr 2026 23:49:16 +0800 Subject: [PATCH 7/7] fix: Normalize `commands[*].params` in `batch_execute`. --- MCPForUnity/Editor/Tools/BatchExecute.cs | 68 +++++++++++++++++- .../Tools/BatchExecuteKeyPreservationTests.cs | 69 +++++++++++++++++++ 2 files changed, 134 insertions(+), 3 deletions(-) diff --git a/MCPForUnity/Editor/Tools/BatchExecute.cs b/MCPForUnity/Editor/Tools/BatchExecute.cs index aca4a1994..e8884e1fb 100644 --- a/MCPForUnity/Editor/Tools/BatchExecute.cs +++ b/MCPForUnity/Editor/Tools/BatchExecute.cs @@ -86,7 +86,7 @@ public static async Task HandleCommand(JObject @params) string toolName = commandObj["tool"]?.ToString(); var rawParams = commandObj["params"] as JObject ?? new JObject(); - var commandParams = NormalizeParameterKeys(rawParams); + var commandParams = NormalizeCommandParams(rawParams); if (string.IsNullOrWhiteSpace(toolName)) { @@ -214,7 +214,7 @@ private static bool DetermineCallSucceeded(object result) return true; } - private static JObject NormalizeParameterKeys(JObject source) + private static JObject NormalizeCommandParams(JObject source) { if (source == null) { @@ -225,11 +225,73 @@ private static JObject NormalizeParameterKeys(JObject source) foreach (var property in source.Properties()) { string normalizedName = ToCamelCase(property.Name); - normalized[normalizedName] = property.Value; + normalized[normalizedName] = NormalizeStructuredJsonStrings(property.Value); } return normalized; } + private static JToken NormalizeStructuredJsonStrings(JToken token) + { + if (token == null) + { + return JValue.CreateNull(); + } + + return token.Type switch + { + JTokenType.Object => NormalizeObject((JObject)token), + JTokenType.Array => NormalizeArray((JArray)token), + JTokenType.String => TryParseStructuredJsonString(token.Value()), + _ => token.DeepClone() + }; + } + + private static JObject NormalizeObject(JObject source) + { + var normalized = new JObject(); + foreach (var property in source.Properties()) + { + normalized[property.Name] = NormalizeStructuredJsonStrings(property.Value); + } + return normalized; + } + + private static JArray NormalizeArray(JArray source) + { + var normalized = new JArray(); + foreach (var item in source) + { + normalized.Add(NormalizeStructuredJsonStrings(item)); + } + return normalized; + } + + private static JToken TryParseStructuredJsonString(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return new JValue(value); + } + + string trimmed = value.Trim(); + bool looksLikeObject = trimmed.StartsWith("{", StringComparison.Ordinal) && trimmed.EndsWith("}", StringComparison.Ordinal); + bool looksLikeArray = trimmed.StartsWith("[", StringComparison.Ordinal) && trimmed.EndsWith("]", StringComparison.Ordinal); + + if (!looksLikeObject && !looksLikeArray) + { + return new JValue(value); + } + + try + { + return NormalizeStructuredJsonStrings(JToken.Parse(trimmed)); + } + catch + { + return new JValue(value); + } + } + private static string ToCamelCase(string key) => StringCaseUtility.ToCamelCase(key); } } diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BatchExecuteKeyPreservationTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BatchExecuteKeyPreservationTests.cs index 3419dfed5..e1a360bac 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BatchExecuteKeyPreservationTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BatchExecuteKeyPreservationTests.cs @@ -162,5 +162,74 @@ public void Regression_CreateGameObject_StillWorksViaBatch() Object.DestroyImmediate(created); } } + + [Test] + public void StructuredJsonStrings_InCommandParams_AreParsedBeforeDispatch() + { + var light = testGo.AddComponent(); + testGo.name = "BatchStructuredJson_" + System.Guid.NewGuid().ToString("N").Substring(0, 8); + + var batchParams = new JObject + { + ["commands"] = new JArray + { + new JObject + { + ["tool"] = "manage_components", + ["params"] = new JObject + { + ["action"] = "set_property", + ["target"] = testGo.name, + ["search_method"] = "by_name", + ["component_type"] = "Light", + ["property"] = "color", + ["value"] = "[0.0, 1.0, 1.0, 1.0]" + } + } + } + }; + + var result = BatchExecute.HandleCommand(batchParams).GetAwaiter().GetResult(); + var resultObj = JObject.FromObject(result); + + Assert.IsTrue(resultObj.Value("success"), $"Batch should succeed: {resultObj}"); + Assert.AreEqual(new Color(0f, 1f, 1f, 1f), light.color); + } + + [Test] + public void PlainStrings_InCommandParams_ArePreserved() + { + testGo.AddComponent(); + + var batchParams = new JObject + { + ["commands"] = new JArray + { + new JObject + { + ["tool"] = "manage_components", + ["params"] = new JObject + { + ["action"] = "set_property", + ["target"] = testGo.name, + ["search_method"] = "by_name", + ["component_type"] = "CustomComponent", + ["property"] = "customText", + ["value"] = "Player One" + } + } + } + }; + + var result = BatchExecute.HandleCommand(batchParams).GetAwaiter().GetResult(); + var resultObj = JObject.FromObject(result); + + Assert.IsTrue(resultObj.Value("success"), $"Batch should succeed: {resultObj}"); + Assert.AreEqual("Player One", + testGo.GetComponent() + .GetType() + .GetField("customText", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.GetValue(testGo.GetComponent()) as string); + } } }