Skip to content

fix: make component properties reachable on manage_gameobject create - #1298

Open
asavs wants to merge 1 commit into
CoplayDev:betafrom
asavs:fix/gameobject-create-component-properties
Open

fix: make component properties reachable on manage_gameobject create#1298
asavs wants to merge 1 commit into
CoplayDev:betafrom
asavs:fix/gameobject-create-component-properties

Conversation

@asavs

@asavs asavs commented Jul 28, 2026

Copy link
Copy Markdown

Fixes #1297

What was broken

manage_gameobject at action: "create" had no reachable way to set component properties:

  • component_properties was accepted by the tool, coerced by the C# dispatcher (ManageGameObject.cs:55-68 runs for every action, not just modify), and then never read anywhere on the create path. The call returned "success": true and silently did nothing.
  • The shape create does read{"typeName": ..., "properties": {...}} objects nested inside components_to_add (GameObjectCreate.cs:250-253) — was rejected before it ever reached Unity, because the Python schema typed components_to_add as list[str] | str. Passing an object entry raised a Pydantic ValidationError two ways (as a bad list item and as a bad bare string).

So one argument was implemented but unreachable, and the other was reachable but discarded. action: "modify" was unaffected.

What changed and why

MCPForUnity/Editor/Tools/GameObjects/GameObjectComponentHelpers.cs
Factored the componentProperties loop + per-component error aggregation that used to live only in GameObjectModify.cs into a new ApplyComponentProperties(GameObject, JObject, out bool modified) helper. GameObjectModify already had all the logic create needed; duplicating ~50 lines of error-aggregation code across two files felt worse than the one shared helper, so I pulled it out rather than copy-pasting (the file already duplicates the smaller componentsToAdd loop between create/modify — this block was long enough that duplication seemed like the wrong call here).

MCPForUnity/Editor/Tools/GameObjects/GameObjectCreate.cs
Calls the new helper right after components are added. On failure, destroys the partially-created GameObject and returns the error — matching the existing behavior for a failed AddComponentInternal call a few lines above it, so a failed property set doesn't leave a half-configured object behind.

MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs
Switched to the shared helper. Behavior-preserving refactor only — same aggregation, same error shape, modified flips the same way it used to.

Server/src/services/tools/manage_gameobject.py
Widened components_to_add's type to list[str | dict[str, Any]] | dict[str, Any] | str, with a new _normalize_components_to_add() that validates each dict entry has a string typeName (accepts type_name too, for consistency with the snake_case-first Python parameter naming) and an optional dict properties, matching what GameObjectCreate.cs already reads. Plain string entries, JSON-string payloads, and a bare (non-list-wrapped) object all keep working, same tolerance pattern as the existing normalize_string_list/normalize_properties helpers in utils.py. Also updated the components_to_add / component_properties docstrings to say both now work on create, and regenerated website/docs/reference/tools/core/manage_gameobject.md via tools/generate_docs_reference.py (only that file changed).

I did not add snake_case acceptance on the C# side (componentProperties vs component_properties) — checked, and unlike ManagePrefabs.cs:940 this tool's Python layer always sends camelCase over the wire (manage_gameobject.py builds params["componentProperties"]/params["componentsToAdd"] unconditionally), so there's no snake_case form ever arriving in the JObject the C# side would need to check for.

Tested

Python (ran, all green):

  • cd Server && uv sync --extra dev && uv run pytest tests/ -v — 1355 passed, 3 skipped (was 1336 passed, 3 skipped before this branch's new test file).
  • Added Server/tests/test_manage_gameobject.py (19 new tests) covering: components_to_add accepting plain strings, object entries, mixed lists, a bare object, JSON-string forms, type_nametypeName normalization, and rejection of malformed entries (missing typeName, non-object properties, wrong entry type); component_properties forwarding on create.
  • Beyond the unit tests, I reproduced the issue's exact repro payload through a real FastMCP call_tool invocation (not just calling the Python function directly, which would bypass Pydantic validation): built a FastMCP instance, registered manage_gameobject, and called it with {"action": "create", "name": "Probe", "components_to_add": [{"typeName": "BoxCollider", "properties": {"size": [2,2,2]}}]}.
    • On the unmodified file (verified via git stash of just that file): raises pydantic.ValidationError: 2 validation errors for call[manage_gameobject] — the exact error quoted in the issue.
    • With the fix: validates and reaches the (mocked) Unity send with componentsToAdd intact as [{'typeName': 'BoxCollider', 'properties': {'size': [2, 2, 2]}}].
  • Regenerated the docs reference and confirmed only manage_gameobject.md changed, with just the two touched parameter rows different.

C# — reviewed, not run. I don't have a live Unity Editor session to run EditMode tests against from here. I added coverage to TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectCreateTests.cs (a "Component Add and Property Tests" region: property application via component_properties on a newly-added component, the {typeName, properties} object-entry form, a mixed string+object components_to_add list, and the failure/cleanup path when component_properties targets a component that was never added) following the file's existing conventions, and hand-traced GameObjectComponentHelpers.ApplyComponentProperties / the two call sites for brace/paren balance and control flow, but none of it has been compiled or executed inside the Editor. Please run the EditMode suite before merging.

Related, out of scope

While reading Server/src/cli/commands/gameobject.py I noticed the gameobject create CLI command has a comment "Add components separately since componentsToAdd doesn't work" (line 180) and works around it by calling manage_components in a follow-up round trip after create. That looks like a related-but-distinct issue scoped to the CLI's create command rather than the MCP tool schema this issue is about, so I left it alone — flagging here in case it's worth its own issue.

Summary by CodeRabbit

  • New Features

    • Added support for defining component properties while creating or modifying GameObjects.
    • Components can now be added using names, detailed component objects, or mixed lists.
    • Component properties can be supplied directly with component definitions or separately.
  • Bug Fixes

    • Invalid component configurations are rejected with clearer errors.
    • Failed creation operations clean up partially created GameObjects.
  • Documentation

    • Updated manage_gameobject guidance for component inputs and property application.

Fixes CoplayDev#1297. At action:"create", component_properties was accepted and
coerced by the C# dispatcher (ManageGameObject.cs) but only ever consumed
by the "modify" handler, so it silently did nothing. Meanwhile the shape
"create" already reads directly out of each componentsToAdd entry
({typeName, properties}) was rejected before it reached Unity, because the
Python schema typed components_to_add as list[str].

- GameObjectComponentHelpers.cs: factor the componentProperties loop +
  error aggregation out of GameObjectModify.cs into a shared
  ApplyComponentProperties helper, so both actions apply it identically.
- GameObjectCreate.cs: call the new helper after components are added,
  destroying the partially-created object and returning the error if any
  property fails to set (matching how component-add failures are handled).
- GameObjectModify.cs: switch to the shared helper (behavior-preserving
  refactor, no functional change on the modify path).
- manage_gameobject.py: widen components_to_add to accept
  {"typeName": ..., "properties": {...}} objects alongside plain strings,
  matching what GameObjectCreate.cs already reads.
- Regenerated website/docs/reference/tools/core/manage_gameobject.md via
  tools/generate_docs_reference.py for the updated parameter docs.

Tested: Server/tests/test_manage_gameobject.py exercises the Python
contract end-to-end, including a real fastmcp/pydantic schema validation
run of the issue's exact repro payloads (confirmed the pre-fix
ValidationError reproduces on the unmodified file, and is gone after).
Added TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/
ManageGameObjectCreateTests.cs coverage for the C# side, but this was not
run against a live Editor.
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

GameObject component properties

Layer / File(s) Summary
Component input normalization and forwarding
Server/src/services/tools/manage_gameobject.py, Server/tests/test_manage_gameobject.py, website/docs/reference/tools/core/manage_gameobject.md
components_to_add now accepts strings, component objects, mixed lists, and JSON encodings with validation and normalization. Documentation and server tests cover the expanded contract.
Unity property application during create and modify
MCPForUnity/Editor/Tools/GameObjects/GameObjectComponentHelpers.cs, MCPForUnity/Editor/Tools/GameObjects/GameObjectCreate.cs, MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs, TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectCreateTests.cs
A shared helper applies component properties and aggregates errors. Create applies properties after adding components and destroys failed creations; modify delegates to the helper. Unity tests cover object entries, mixed inputs, and failure cleanup.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant manage_gameobject
  participant GameObjectCreate
  participant ApplyComponentProperties
  Caller->>manage_gameobject: submit components_to_add and component_properties
  manage_gameobject->>manage_gameobject: normalize and validate inputs
  manage_gameobject->>GameObjectCreate: forward componentsToAdd and componentProperties
  GameObjectCreate->>ApplyComponentProperties: apply properties after adding components
  ApplyComponentProperties-->>GameObjectCreate: return modified state or errors
Loading

Possibly related PRs

Suggested reviewers: scriptwonder

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: exposing component properties on manage_gameobject create.
Description check ✅ Passed The description is detailed and covers the fix, tests, and related issue, but it doesn't follow the repo's exact template headings.
Linked Issues check ✅ Passed The PR implements the linked issue's two fixes: create-time property application and support for object entries in components_to_add.
Out of Scope Changes check ✅ Passed The changes stay focused on manage_gameobject create/modify handling, tests, and docs, with no obvious unrelated code.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
Server/tests/test_manage_gameobject.py (1)

67-75: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Test name doesn't match what it asserts.

test_tool_description_mentions_component_properties only checks that a non-empty description exists (the comment acknowledges this). Consider renaming to something like test_tool_registered_with_description, or asserting on the parameter annotation instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Server/tests/test_manage_gameobject.py` around lines 67 - 75, Rename
test_tool_description_mentions_component_properties to reflect that it only
verifies the manage_gameobject tool is registered with a non-empty description,
such as test_tool_registered_with_description; leave the existing assertion
behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@MCPForUnity/Editor/Tools/GameObjects/GameObjectComponentHelpers.cs`:
- Around line 163-179: Update the componentProperties iteration around
SetComponentPropertiesInternal to explicitly reject entries whose prop.Value is
not a JObject. Add a descriptive validation error to componentErrors for each
unsupported value instead of skipping it, and ensure modified remains false for
rejected entries while preserving the existing success and error handling for
object values.

In `@Server/tests/test_manage_gameobject.py`:
- Around line 113-123: Update
test_invalid_component_properties_rejected_before_send to assert that "params"
is not present in mock_unity, matching the fixture’s captured request structure
and later tests. Keep the existing failure assertion unchanged.

---

Nitpick comments:
In `@Server/tests/test_manage_gameobject.py`:
- Around line 67-75: Rename test_tool_description_mentions_component_properties
to reflect that it only verifies the manage_gameobject tool is registered with a
non-empty description, such as test_tool_registered_with_description; leave the
existing assertion behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4b4fa7b1-12fc-48d2-afdf-e0ee96ff6bc3

📥 Commits

Reviewing files that changed from the base of the PR and between fc70dda and 503d938.

📒 Files selected for processing (7)
  • MCPForUnity/Editor/Tools/GameObjects/GameObjectComponentHelpers.cs
  • MCPForUnity/Editor/Tools/GameObjects/GameObjectCreate.cs
  • MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs
  • Server/src/services/tools/manage_gameobject.py
  • Server/tests/test_manage_gameobject.py
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectCreateTests.cs
  • website/docs/reference/tools/core/manage_gameobject.md

Comment on lines +163 to +179
foreach (var prop in componentPropertiesObj.Properties())
{
string compName = prop.Name;
JObject propertiesToSet = prop.Value as JObject;
if (propertiesToSet != null)
{
var setResult = SetComponentPropertiesInternal(targetGo, compName, propertiesToSet);
if (setResult != null)
{
componentErrors.Add(setResult);
}
else
{
modified = true;
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Non-object property values are silently ignored.

If a componentProperties entry's value isn't a JSON object (e.g. {"BoxCollider": "size"}), the entry is skipped with no error and modified stays false, so create/modify report success while nothing was applied. The Python layer only validates the outer dict, so this reaches here. Reporting it keeps the "reject unsupported input explicitly" half of the contract.

🐛 Proposed fix
                 string compName = prop.Name;
                 JObject propertiesToSet = prop.Value as JObject;
-                if (propertiesToSet != null)
-                {
-                    var setResult = SetComponentPropertiesInternal(targetGo, compName, propertiesToSet);
-                    if (setResult != null)
-                    {
-                        componentErrors.Add(setResult);
-                    }
-                    else
-                    {
-                        modified = true;
-                    }
-                }
+                if (propertiesToSet == null)
+                {
+                    componentErrors.Add(new ErrorResponse(
+                        $"componentProperties['{compName}'] must be an object of property names to values.",
+                        new { errors = new[] { $"Invalid value for '{compName}': {prop.Value.Type}." } }));
+                    continue;
+                }
+
+                var setResult = SetComponentPropertiesInternal(targetGo, compName, propertiesToSet);
+                if (setResult != null)
+                {
+                    componentErrors.Add(setResult);
+                }
+                else
+                {
+                    modified = true;
+                }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
foreach (var prop in componentPropertiesObj.Properties())
{
string compName = prop.Name;
JObject propertiesToSet = prop.Value as JObject;
if (propertiesToSet != null)
{
var setResult = SetComponentPropertiesInternal(targetGo, compName, propertiesToSet);
if (setResult != null)
{
componentErrors.Add(setResult);
}
else
{
modified = true;
}
}
}
foreach (var prop in componentPropertiesObj.Properties())
{
string compName = prop.Name;
JObject propertiesToSet = prop.Value as JObject;
if (propertiesToSet == null)
{
componentErrors.Add(new ErrorResponse(
$"componentProperties['{compName}'] must be an object of property names to values.",
new { errors = new[] { $"Invalid value for '{compName}': {prop.Value.Type}." } }));
continue;
}
var setResult = SetComponentPropertiesInternal(targetGo, compName, propertiesToSet);
if (setResult != null)
{
componentErrors.Add(setResult);
}
else
{
modified = true;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MCPForUnity/Editor/Tools/GameObjects/GameObjectComponentHelpers.cs` around
lines 163 - 179, Update the componentProperties iteration around
SetComponentPropertiesInternal to explicitly reject entries whose prop.Value is
not a JObject. Add a descriptive validation error to componentErrors for each
unsupported value instead of skipping it, and ensure modified remains false for
rejected entries while preserving the existing success and error handling for
object values.

Comment on lines +113 to +123
def test_invalid_component_properties_rejected_before_send(self, mock_unity):
result = asyncio.run(
manage_gameobject(
SimpleNamespace(),
action="create",
name="Probe",
component_properties="not json",
)
)
assert result["success"] is False
assert "component_properties" not in mock_unity

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assertion is vacuous — component_properties is never a key of mock_unity.

The fixture captures unity_instance, tool_name, params, so this passes even if the invalid payload were forwarded. Use the "params" not in mock_unity form the later tests use.

💚 Proposed fix
         assert result["success"] is False
-        assert "component_properties" not in mock_unity
+        assert "params" not in mock_unity
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_invalid_component_properties_rejected_before_send(self, mock_unity):
result = asyncio.run(
manage_gameobject(
SimpleNamespace(),
action="create",
name="Probe",
component_properties="not json",
)
)
assert result["success"] is False
assert "component_properties" not in mock_unity
def test_invalid_component_properties_rejected_before_send(self, mock_unity):
result = asyncio.run(
manage_gameobject(
SimpleNamespace(),
action="create",
name="Probe",
component_properties="not json",
)
)
assert result["success"] is False
assert "params" not in mock_unity
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Server/tests/test_manage_gameobject.py` around lines 113 - 123, Update
test_invalid_component_properties_rejected_before_send to assert that "params"
is not present in mock_unity, matching the fixture’s captured request structure
and later tests. Keep the existing failure assertion unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

manage_gameobject: component properties cannot be set at create - one argument is ignored, the other is unreachable

1 participant