Skip to content

Conversation

@dsarno
Copy link
Collaborator

@dsarno dsarno commented Dec 7, 2025

Summary

Introduces the manage_material tool, enabling comprehensive material management via MCP. This tool allows LLMs to create materials, modify shader properties (colors, textures, floats), and assign materials to GameObjects using various modes (shared, instance, property block). Previously, manage_asset handled material manipulation, but was error ridden. This will allow us to separate concerns and de-bloat manage_asset a bit, plus focus more on improved material handling features, which are complex and should live in their own tool.

Key Features

  • CRUD Operations: Create new materials with specific shaders and retrieve material property info.
  • Property Management: Support for setting Colors, Vectors, Floats, Ints, and Textures via JSON payloads.
  • Smart Resolution: Auto-resolves common property aliases (e.g., "albedo" → _BaseMap or _MainTex) and handles case-insensitive property lookups.
  • Assignment Modes:
    • shared: Modifies the shared material asset directly.
    • instance: Creates and assigns a unique material instance.
    • property_block: Uses MaterialPropertyBlock for performant, non-destructive per-instance overrides.

Technical Implementation

  • C#: Added ManageMaterial.cs for tool logic and MaterialOps.cs helper for robust JSON parsing and property application.
  • Python: Registered manage_material service with full async support.
  • Robustness: Implemented strict validation (null checks, slot bounds), HasProperty verification before assignment, and path sanitization for cross-platform compatibility.

Testing

  • Added comprehensive EditMode tests (ManageMaterialTests, ManageMaterialPropertiesTests) covering creation, property setting, and renderer assignment.
  • Verified compatibility with standard and URP shader property names.
  • Cleaned up manual setup scripts and temporary test assets.

Summary by CodeRabbit

  • New Features

    • Material management: create, configure, query and assign materials (colors, textures, shader properties) via Editor and new server tool.
  • Refactor

    • Centralized material property application for consistent alias resolution and behavior across tools; unified material command handler in the Editor.
  • Improvements

    • Safer, centralized JSON/string parsing for inputs; more robust handling of vectors, booleans, and color parsing.
  • Tests

    • Extensive new editor and integration tests covering material workflows, error cases, and console handling.
  • Documentation

    • Added manage_material tool entries to README.

✏️ Tip: You can customize this high-level summary in your review settings.

dsarno added 11 commits December 5, 2025 10:40
- Add ManageMaterial tool for creating and modifying materials
- Add MaterialOps helper for material property operations
- Add comprehensive test suite for material management
- Add string parameter parsing support for material properties
- Update related tools (ManageGameObject, manage_asset, etc.)
- Add test materials and scenes for material testing
- Move  and  logic from  to
- Update  to delegate to
- Update  to use enhanced  for creation and property setting
- Add texture path loading support to
- Fix Python server tools (redundant imports, exception handling, string formatting)
- Clean up documentation and error reports
- Improve ManageMaterial.cs (overwrite checks, error handling)
- Enhance MaterialOps.cs (robustness, logging, dead code removal)
- Update tests (assertions, unused imports)
- Fix manifest.json relative path
- Remove temporary test artifacts and manual setup scripts
…cp into feat/manage-material-tool

* 'feat/manage-material-tool' of github.com:dsarno/unity-mcp:
  Remove unnecessary SceneTemplateSettings.json
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 7, 2025

Walkthrough

Adds a MaterialOps utility for applying JSON-defined material properties, a ManageMaterial editor tool, server-side manage_material and centralized JSON parsing support, refactors ManageAsset/ManageGameObject to use MaterialOps, and adds extensive Unity editor tests for material workflows and parsing.

Changes

Cohort / File(s) Summary
MaterialOps Helper
MCPForUnity/Editor/Helpers/MaterialOps.cs, MCPForUnity/Editor/Helpers/MaterialOps.cs.meta
New static class providing ApplyProperties, ResolvePropertyName, GetMainColorPropertyName, TrySetShaderProperty, ParseColor; handles alias resolution, color/vector/float/texture parsing (including stringified JSON), shader selection, AssetDatabase texture loading, and modification tracking.
ManageMaterial Tool
MCPForUnity/Editor/Tools/ManageMaterial.cs, MCPForUnity/Editor/Tools/ManageMaterial.cs.meta
New editor tool exposing HandleCommand(JObject) for ping/create materials, set shader property/color, assign to renderer, set renderer color, and fetch material info; integrates with MaterialOps and Unity asset/undo APIs.
Refactored Editor Tools
MCPForUnity/Editor/Tools/ManageAsset.cs, MCPForUnity/Editor/Tools/ManageGameObject.cs
Replaced inline material property logic with MaterialOps.ApplyProperties / TrySetShaderProperty; ManageAsset adds GameObject/component modification branch and generic ApplyObjectProperties usage; ManageGameObject exposes internal InputSerializer, adds target aliasing, and enables non-public serialized component retrieval.
Server: manage_material & parsing
Server/src/services/tools/manage_material.py, Server/src/services/tools/manage_asset.py, Server/src/services/tools/manage_gameobject.py
New async manage_material tool that sends commands to Unity; server-side parsing now uses parse_json_payload for stringified params and vectors, updated coercion/validation, clearer error responses, and some function signature relaxations to accept string inputs.
Server Utilities
Server/src/services/tools/utils.py
Added parse_json_payload(value: Any) -> Any to centrally parse JSON-like strings into Python objects while returning non-strings unchanged.
Server: other tooling updates
Server/src/services/tools/read_console.py, Server/src/services/tools/script_apply_edits.py
read_console: more robust stacktrace/list handling and help text tweak; script_apply_edits: accepts edits as list or JSON string and uses parse_json_payload.
Editor Tests (material)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialTests.cs, .../ManageMaterialPropertiesTests.cs, .../ManageMaterialReproTests.cs, .../ManageMaterialStressTests.cs, plus .meta files
New NUnit editor tests covering material creation with various property formats, shader/property setting, renderer assignment, property-block isolation, invalid JSON handling, and integration scenarios.
Test updates & additions
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialParameterToolTests.cs, .../ReadConsoleTests.cs, Editor folder .meta
Tests updated to use ManageMaterial for assignment flows; added ReadConsole tests and required .meta files.
Misc editor meta
TestProjects/UnityMCPTests/Assets/Scripts/Editor.meta, various *.cs.meta
Added Unity .meta files for new/updated editor/test assets.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client
    participant Server
    participant Unity
    Note over Client,Server: Client requests tool action with properties (string or object)
    Client->>Server: POST /tool (action, properties, ...)
    Server->>Server: parse_json_payload(properties) => normalized params
    Server->>Unity: send command (action + normalized params) [async/retry]
    Unity->>Unity: ManageMaterial.HandleCommand parses params
    Unity->>Unity: MaterialOps.ApplyProperties / TrySetShaderProperty resolves aliases, parses colors/textures
    Unity->>Unity: Material/Renderer updated (AssetDatabase / Undo as needed)
    Unity-->>Server: response dict {status, message, data}
    Server-->>Client: forward response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Focus areas:
    • MaterialOps: alias resolution, ParseColor handling of arrays/objects/strings, TrySetShaderProperty texture loading and stringified JSON parsing.
    • ManageMaterial / ManageAsset integrations: parameter mapping, Undo/AssetDatabase interactions, and GameObject/component property application.
    • Server parse_json_payload adoption: ensure consistent parsing/coercion and error messages across manage_asset/manage_gameobject/script_apply_edits.
    • New/expanded tests: ensure assertions and expected error messages align with implementation.

Possibly related PRs

Suggested reviewers

  • justinpbarnett

Poem

🐰 I hopped into shaders, whiskers bright,
I parsed your JSON by lantern light,
Colors, maps, and aliases aligned,
One helper now keeps props in mind.
Hop, apply — materials sing tonight! 🎨

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main change: adding a new manage_material tool for material manipulation.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
MCPForUnity/Editor/Tools/ManageAsset.cs (2)

214-220: Serializer inconsistency: CreateDefault() lacks custom Unity converters.

MaterialOps.ApplyProperties is called with JsonSerializer.CreateDefault(), which doesn't include the custom converters (Vector3Converter, ColorConverter, etc.) that ManageGameObject.InputSerializer provides. This could cause deserialization failures for complex property types.

Consider reusing the shared serializer from ManageGameObject:

-                            MaterialOps.ApplyProperties(mat, propertiesForApply, Newtonsoft.Json.JsonSerializer.CreateDefault());
+                            MaterialOps.ApplyProperties(mat, propertiesForApply, ManageGameObject.InputSerializer);

440-445: Same serializer inconsistency in ModifyAsset.

This has the same issue as the create path - using CreateDefault() instead of the shared serializer with Unity type converters.

-                    modified |= MaterialOps.ApplyProperties(material, properties, Newtonsoft.Json.JsonSerializer.CreateDefault());
+                    modified |= MaterialOps.ApplyProperties(material, properties, ManageGameObject.InputSerializer);
🧹 Nitpick comments (12)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs (1)

1-10: Remove unused imports.

System.IO and System.Linq appear to be unused in this test file.

 using System;
-using System.IO;
 using System.Collections.Generic;
-using System.Linq;
 using Newtonsoft.Json.Linq;
 using NUnit.Framework;
 using UnityEditor;
Server/tests/integration/test_manage_asset_json_parsing.py (1)

35-37: Tighten behavior assertions and loosen log-string coupling

Two small suggestions for the JSON-parsing tests:

  1. For manage_asset logging (Line 36), you currently assert exact membership in ctx.log_info:
assert "manage_asset: coerced properties using centralized parser" in ctx.log_info

Given ctx.log_info is a collection of messages and you already use substring checks elsewhere, consider matching more defensively to tolerate prefixes/suffixes or structured logging:

assert any(
    "manage_asset: coerced properties using centralized parser" in msg
    for msg in ctx.log_info
)

This reduces brittleness if logging formatting changes slightly.

  1. For manage_gameobject (Lines 115–143), the test no longer asserts that JSON parsing actually occurred; it only checks success from manage_gameobject. Since you're monkeypatching async_send_command_with_retry, you can also assert that component_properties reached Unity as a dict, which directly validates parse_json_payload behavior. For example:
captured_params = {}

async def fake_send(cmd, params, **kwargs):
    captured_params.update(params)
    return {"success": True, "message": "GameObject created successfully"}

monkeypatch.setattr(
    "services.tools.manage_gameobject.async_send_command_with_retry",
    fake_send,
)

# ...call manage_gameobject...

assert isinstance(captured_params.get("component_properties"), dict)

This keeps the test resilient to log-message wording while still verifying the JSON coercion path.

Also applies to: 115-143

Server/src/services/tools/utils.py (1)

5-5: parse_json_payload is safe and useful; minor doc tweak possible

The helper is a good, conservative JSON parser for loosely-typed inputs: it avoids touching non-JSON-looking strings, uses json.loads only when appropriate, and returns the original value on failure, which is ideal for tolerant tooling.

Minor nit: the docstring emphasizes “object/list” results, but the implementation also intentionally parses scalar JSON values (true/false/null and numeric literals). If that behavior is desired (it seems useful for booleans and numerics), consider explicitly mentioning scalars in the docstring to align expectations with behavior.

Also applies to: 27-59

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialStressTests.cs (1)

72-191: Stress tests exercise key ManageMaterial paths; consider tightening naming

The three tests do a good job of covering:

  • Error reporting for bad paths, bad color arrays, and out-of-range slots.
  • PropertyBlock isolation vs shared-material color.
  • End-to-end assignment then shared-material color modification.

One minor clarity nit: Integration_WithManageGameObject_AssignsMaterialAndModifies only calls ManageMaterial.HandleCommand and doesn’t involve manage_gameobject. Renaming the test (or updating the comment) to reflect that it’s purely a ManageMaterial integration flow would avoid confusion for future readers.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialPropertiesTests.cs (1)

99-123: Flexible error message assertion may become fragile.

The assertion on lines 121-122 checks for multiple possible error message patterns ("Path", "line", "position", "End of input"). While this handles different Newtonsoft.Json versions, consider using StringAssert.Contains with the most stable expected substring, or verifying just that the message is non-empty and different from the generic prefix.

-            Assert.IsTrue(msg.Contains("Path") || msg.Contains("line") || msg.Contains("position") || msg.Contains("End of input"), 
-                $"Message should contain details. Got: {msg}");
+            // Verify the message contains more than just the prefix (has exception details)
+            Assert.IsTrue(msg.Length > "Invalid JSON".Length, 
+                $"Message should contain exception details. Got: {msg}");
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialReproTests.cs (1)

40-43: Consider extracting ToJObject helper to a shared test utilities class.

This helper is duplicated across ManageMaterialPropertiesTests, ManageMaterialReproTests, MaterialParameterToolTests, and ReadConsoleTests. Extracting to a shared TestHelpers class would reduce duplication.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialTests.cs (1)

47-68: Remove dead code comments in TearDown.

Lines 49-67 contain extensive comments discussing implementation approaches but the actual cleanup logic for Assets/Temp is never executed. The if block starting at line 47 does nothing.

Either implement the cleanup logic or remove the dead code:

             // Clean up parent Temp folder if it's empty
             if (AssetDatabase.IsValidFolder("Assets/Temp"))
             {
-                // Check if empty using AssetDatabase (sub-assets)
-                var guids = AssetDatabase.FindAssets("", new[] { "Assets/Temp" });
-                // FindAssets returns the folder itself too usually, or contents? 
-                // Actually Directory.GetFiles/GetDirectories is fine for checking emptiness of a folder on disk,
-                // but AssetDatabase.DeleteAsset("Assets/Temp") is safer if we know it's empty.
-                // The review suggested using AssetDatabase consistently.
-                // Let's check sub-folders via AssetDatabase?
-                // Actually, if we just want to remove if empty, we can try to delete and catch, or check existence of sub items.
-                // But let's stick to the reviewer's point: "using AssetDatabase APIs consistently would be more robust".
-                // We can't easily check "is empty" with AssetDatabase without listing assets.
-                // So I will stick to the logic but use AssetDatabase for deletion (which it already does).
-                // Wait, lines 50-51 use Directory.GetDirectories. 
-                // I will assume the reviewer wants us to avoid System.IO.Directory.
-                // I'll skip this change if it's too complex to do purely with AssetDatabase without a helper, 
-                // OR I can just try to delete it and let Unity handle it if it's not empty? No, Unity deletes recursively.
-                // So checking emptiness is important.
-                // I will just use Directory.GetFiles but with Application.dataPath relative path?
-                // The reviewer said "using AssetDatabase APIs consistently".
-                // I'll leave it for now or use `AssetDatabase.GetSubFolders`.
+                // Only delete if empty
+                var subFolders = AssetDatabase.GetSubFolders("Assets/Temp");
+                if (subFolders.Length == 0)
+                {
+                    AssetDatabase.DeleteAsset("Assets/Temp");
+                }
             }
MCPForUnity/Editor/Tools/ManageMaterial.cs (1)

307-319: Undo may not revert material instantiation as noted, but consider warning the user.

The comment at line 312 correctly notes that Undo cannot fully revert material instantiation. Consider returning a warning in the response to inform the caller that the operation creates a persistent material instance.

                 if (slot >= 0 && slot < renderer.materials.Length)
                 {
                      Material mat = renderer.materials[slot]; 
                      // Note: Undo cannot fully revert material instantiation
                      Undo.RecordObject(mat, "Set Instance Material Color");
                      if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color);
                      else mat.SetColor("_Color", color);
-                     return new { status = "success", message = "Set instance material color" };
+                     return new { status = "success", message = "Set instance material color", warning = "Material instance created; Undo cannot fully revert instantiation." };
                 }
Server/src/services/tools/manage_material.py (1)

58-68: Slot coercion is overly complex for edge cases.

The slot coercion logic handles "0" (digit string) and potentially "0" as JSON. The json.loads(slot) at line 66 would fail for "0" since it's not valid JSON (it needs to be just 0 without quotes in the string). The first check slot.isdigit() already handles numeric strings, so the JSON fallback is unlikely to trigger meaningfully.

Consider simplifying:

     # Coerce slot to int if it's a string
     if slot is not None:
         if isinstance(slot, str):
-            if slot.isdigit():
+            try:
                 slot = int(slot)
-            else:
-                # Try parsing if it's a JSON number string
-                try:
-                    slot = int(json.loads(slot))
-                except (json.JSONDecodeError, ValueError, TypeError):
-                    pass # Let it fail downstream or keep as string if that was intended (though C# expects int)
+            except ValueError:
+                pass  # Let it fail downstream; C# expects int
MCPForUnity/Editor/Helpers/MaterialOps.cs (2)

80-83: Silent error swallowing may hide issues.

The empty catch { } block at line 82 silently ignores parsing errors for color values. Consider at minimum logging a warning to aid debugging when color parsing fails unexpectedly.

                 try
                 {
                     Color newColor = ParseColor(colorArr, serializer);
                     if (mat.HasProperty(propName) && mat.GetColor(propName) != newColor)
                     {
                         mat.SetColor(propName, newColor);
                         modified = true;
                     }
                 }
-                catch { }
+                catch (Exception ex)
+                {
+                    Debug.LogWarning($"[MaterialOps] Failed to parse color array: {ex.Message}");
+                }

222-246: Multiple silent catch blocks obscure failures.

Lines 228, 229, 236, and 243 all have empty catch { } blocks that silently swallow exceptions. While the intent is to try multiple conversions, completely silent failures make debugging difficult. The warning at line 292-294 only triggers if all attempts fail.

Consider logging at debug level when individual conversions fail, or at minimum ensure the final warning includes context about what was attempted.

Server/src/services/tools/manage_gameobject.py (1)

96-126: LGTM on the parsing refactor.

The updated _coerce_vec function correctly prioritizes the centralized parse_json_payload for JSON arrays while maintaining backward compatibility with legacy comma/space-separated string formats.

Nit (optional): The import math on line 103 is inside the function and will be executed on every call. Consider moving it to the module level for a minor efficiency improvement, though this is pre-existing code.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b09e48a and f059dcf.

⛔ Files ignored due to path filters (1)
  • Server/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • MCPForUnity/Editor/Helpers/MaterialOps.cs (1 hunks)
  • MCPForUnity/Editor/Helpers/MaterialOps.cs.meta (1 hunks)
  • MCPForUnity/Editor/Tools/ManageAsset.cs (2 hunks)
  • MCPForUnity/Editor/Tools/ManageGameObject.cs (3 hunks)
  • MCPForUnity/Editor/Tools/ManageMaterial.cs (1 hunks)
  • MCPForUnity/Editor/Tools/ManageMaterial.cs.meta (1 hunks)
  • Server/src/services/tools/manage_asset.py (2 hunks)
  • Server/src/services/tools/manage_gameobject.py (8 hunks)
  • Server/src/services/tools/manage_material.py (1 hunks)
  • Server/src/services/tools/read_console.py (2 hunks)
  • Server/src/services/tools/script_apply_edits.py (4 hunks)
  • Server/src/services/tools/utils.py (2 hunks)
  • Server/tests/integration/test_manage_asset_json_parsing.py (2 hunks)
  • TestProjects/UnityMCPTests/Assets/Scripts/Editor.meta (1 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialPropertiesTests.cs (1 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialPropertiesTests.cs.meta (1 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialReproTests.cs (1 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialReproTests.cs.meta (1 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialStressTests.cs (1 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialStressTests.cs.meta (1 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialTests.cs (1 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialTests.cs.meta (1 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialParameterToolTests.cs (3 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs (1 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs.meta (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-05T18:23:12.349Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 368
File: MCPForUnity/UnityMcpServer~/src/resources/menu_items.py:15-15
Timestamp: 2025-11-05T18:23:12.349Z
Learning: In Unity MCP, the `name` parameter in the `mcp_for_unity_resource` decorator is the external API name exposed to MCP clients (LLMs, AI agents). The command string passed to `async_send_command_with_retry` or `async_send_with_unity_instance` (e.g., "get_menu_items") is the internal command identifier that must match the C# side. These are decoupled, allowing external API naming to evolve independently of internal command routing.

Applied to files:

  • Server/src/services/tools/manage_asset.py
📚 Learning: 2025-09-03T16:00:55.839Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 0
File: :0-0
Timestamp: 2025-09-03T16:00:55.839Z
Learning: ComponentResolver in UnityMcpBridge/Editor/Tools/ManageGameObject.cs is a nested static class within ManageGameObject, not a sibling type. The `using static MCPForUnity.Editor.Tools.ManageGameObject;` import is required to access ComponentResolver methods directly without the outer class qualifier.

Applied to files:

  • MCPForUnity/Editor/Tools/ManageGameObject.cs
🧬 Code graph analysis (11)
MCPForUnity/Editor/Helpers/MaterialOps.cs (1)
MCPForUnity/Editor/Helpers/AssetPathUtility.cs (2)
  • AssetPathUtility (15-201)
  • SanitizeAssetPath (20-34)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialParameterToolTests.cs (2)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialTests.cs (1)
  • JObject (71-74)
MCPForUnity/Editor/Tools/ManageMaterial.cs (1)
  • HandleCommand (14-55)
Server/src/services/tools/manage_material.py (3)
Server/src/services/tools/utils.py (1)
  • parse_json_payload (27-59)
Server/src/transport/unity_transport.py (1)
  • send_with_unity_instance (78-103)
Server/src/transport/legacy/unity_connection.py (1)
  • async_send_command_with_retry (753-785)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialStressTests.cs (3)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialReproTests.cs (2)
  • JObject (40-43)
  • Test (45-70)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs (3)
  • JObject (51-54)
  • Test (16-30)
  • Test (32-49)
MCPForUnity/Editor/Tools/ManageMaterial.cs (1)
  • HandleCommand (14-55)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs (1)
MCPForUnity/Editor/Tools/ReadConsole.cs (1)
  • ReadConsole (38-126)
Server/src/services/tools/script_apply_edits.py (1)
Server/src/services/tools/utils.py (1)
  • parse_json_payload (27-59)
Server/src/services/tools/manage_asset.py (1)
Server/src/services/tools/utils.py (1)
  • parse_json_payload (27-59)
MCPForUnity/Editor/Tools/ManageGameObject.cs (1)
MCPForUnity/Editor/Helpers/MaterialOps.cs (2)
  • MaterialOps (12-340)
  • TrySetShaderProperty (201-296)
Server/src/services/tools/manage_gameobject.py (3)
Server/src/transport/unity_transport.py (1)
  • send_with_unity_instance (78-103)
Server/src/transport/legacy/unity_connection.py (1)
  • async_send_command_with_retry (753-785)
Server/src/services/tools/utils.py (2)
  • coerce_bool (11-24)
  • parse_json_payload (27-59)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialPropertiesTests.cs (2)
MCPForUnity/Editor/Tools/ManageMaterial.cs (1)
  • HandleCommand (14-55)
MCPForUnity/Editor/Helpers/MaterialOps.cs (1)
  • Color (301-339)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialReproTests.cs (2)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs (1)
  • JObject (51-54)
MCPForUnity/Editor/Tools/ManageMaterial.cs (1)
  • HandleCommand (14-55)
🔇 Additional comments (39)
TestProjects/UnityMCPTests/Assets/Scripts/Editor.meta (1)

1-8: No review needed for auto-generated metadata.

This is a standard Unity folder metadata file generated automatically by the editor. No manual code review is required.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialTests.cs.meta (1)

1-11: LGTM!

Standard Unity-generated meta file with valid GUID for the new test asset.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialReproTests.cs.meta (1)

1-11: LGTM!

Standard Unity-generated meta file with valid GUID for the repro test asset.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs.meta (1)

1-11: LGTM!

Standard Unity-generated meta file with valid GUID for the ReadConsole test asset.

Server/src/services/tools/read_console.py (1)

102-112: LGTM!

The refactored stacktrace stripping logic properly handles both the standard {"data": {"lines": [...]}} format and the legacy direct list format. The defensive checks with isinstance and the use of pop with a default value make this robust against varying response structures.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs (3)

16-30: LGTM!

Clean test following arrange-act-assert pattern with helpful diagnostic output on assertion failure.


51-54: LGTM!

The helper method cleanly handles both JObject results and other object types that need serialization.


32-49: Verify the expected data structure format.

The test asserts result["data"] is a JArray, but ensure this matches the actual return format of ReadConsole.HandleCommand. If the Python server code expects {"data": {"lines": [...]}} (where data contains a nested lines key), verify consistency between the C# handler and Python expectations.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialPropertiesTests.cs.meta (1)

1-11: Unity test .meta looks standard

MonoImporter configuration is conventional and poses no issues.

MCPForUnity/Editor/Helpers/MaterialOps.cs.meta (1)

1-11: MaterialOps .meta is fine

Standard MonoImporter metadata for the new helper script; no concerns.

MCPForUnity/Editor/Tools/ManageMaterial.cs.meta (1)

1-11: ManageMaterial .meta is valid

Meta file matches the usual pattern for editor tools; nothing to change.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialStressTests.cs.meta (1)

1-11: Stress-test .meta is standard

Meta for ManageMaterialStressTests.cs is conventional; no action needed.

Server/src/services/tools/manage_asset.py (1)

12-13: Centralized JSON parsing integration looks solid

The _normalize_properties changes cleanly delegate to parse_json_payload first and only fall back to the legacy json.loads/ast.literal_eval path when needed, preserving existing error messages while avoiding literal_eval for well-formed JSON. Logging of both the centralized path and the fallback source (JSON vs Python literal) remains clear and should keep diagnostics intact.

Also applies to: 59-79

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialStressTests.cs (1)

18-65: SetUp/TearDown and temp-asset handling look robust

The test fixture’s setup/teardown correctly creates a dedicated temp root, persists the material asset, and cleans up both the cube and Assets/Temp only when empty. This should keep the test environment isolated and avoid polluting the project.

MCPForUnity/Editor/Tools/ManageGameObject.cs (3)

25-38: Visibility change to internal enables cross-module reuse.

Widening InputSerializer from private to internal allows MaterialOps and other helpers to share the same configured serializer instance, avoiding duplication. This is a sensible change for the centralized material handling approach.


58-69: Usability improvement: aliasing nametarget for non-create actions.

This is a helpful UX enhancement. However, note that mutating @params["target"] (line 67) has a side effect on the caller's JObject. This is intentional for downstream methods but worth documenting.

Consider adding a brief inline comment clarifying this is intentional mutation for downstream consistency.


2124-2127: Good refactor: Centralizing shader property handling via MaterialOps.

Delegating to MaterialOps.TrySetShaderProperty removes duplicated property-setting logic and ensures consistent behavior across ManageGameObject, ManageAsset, and ManageMaterial. The serializer is correctly passed for type conversions.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialPropertiesTests.cs (3)

10-42: Well-structured test fixture with proper setup/teardown.

The test class properly creates and cleans up temporary asset folders. The ToJObject helper normalizes results for consistent assertions. Good foundation for the property tests.


43-81: Good coverage for valid JSON input formats.

These tests verify that both JSON string arrays ("{\"_Color\": [1.0, 0.0, 0.0, 1.0]}") and native JObject arrays work correctly for setting material properties. The color assertions (Color.red, Color.green) confirm the properties are actually applied.


125-146: Null property handling test is appropriately defensive.

Testing that null property values don't crash the system is important. The assertion allowing either "success" or "error" (but not a crash/internal error) is a reasonable approach for graceful degradation.

MCPForUnity/Editor/Tools/ManageAsset.cs (1)

378-435: New GameObject/Prefab component modification pathway looks correct.

The logic properly:

  1. Iterates component properties from the JSON
  2. Uses ComponentResolver.TryResolve for type resolution
  3. Falls back to string-based lookup
  4. Logs appropriate warnings for missing components
  5. Aggregates modification state with |=

Good defensive programming with the warnings for unresolved components.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialReproTests.cs (2)

10-43: Test fixture structure is sound.

Good use of GUID-based unique paths (Guid.NewGuid().ToString("N")) to ensure test isolation across parallel runs or repeated executions.


45-70: Good regression test for detailed error messaging.

The test verifies that invalid JSON produces an error with detailed exception information (line 69 ensures the message contains more than just the generic prefix). This is useful for debugging malformed inputs.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialParameterToolTests.cs (2)

117-153: Test correctly migrated to use new ManageMaterial tool.

The refactored test:

  1. Uses the dedicated assign_material_to_renderer action instead of generic modify
  2. Provides explicit materialPath and slot parameters
  3. Asserts on status == "success" matching the new tool's response format

This aligns with the PR's goal of separating material logic from ManageGameObject.


155-176: Dependent test correctly updated to call renamed method.

The ReadRendererData_DoesNotInstantiateMaterial_AndIncludesSharedMaterial test properly calls the renamed setup method. The rest of the test logic remains unchanged, verifying that serialization doesn't instantiate materials.

Server/src/services/tools/script_apply_edits.py (1)

375-379: LGTM! Good defensive parsing for stringified JSON.

The addition of parse_json_payload handling aligns with the centralized JSON parsing pattern used by other tools in this PR (e.g., manage_material.py). The validation ensures a list is always passed downstream.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialTests.cs (2)

76-107: Good test coverage for shader property differences.

The test correctly handles both URP (_BaseColor) and Standard (_Color) shader variations, checking the material's shader name before setting the appropriate property. The assertion logic also adapts based on the actual shader in use.


134-168: Good use of try/finally for runtime object cleanup.

The test properly cleans up the runtime GameObject in the finally block to avoid polluting other tests.

MCPForUnity/Editor/Tools/ManageMaterial.cs (1)

434-442: Apply properties before saving to ensure changes persist.

Properties are applied after CreateAsset at line 434 but before SaveAssets at line 442. The EditorUtility.SetDirty(material) at line 441 should ensure the changes are saved. This flow looks correct.

Server/src/services/tools/manage_material.py (2)

70-73: action.lower() may be unnecessary given Literal constraint.

The action parameter is annotated with Literal[...] containing lowercase values. If clients adhere to the type hint, .lower() is redundant. However, keeping it provides defensive normalization for clients that may not respect the type hints.


88-91: LGTM! Proper async delegation pattern.

The function correctly uses send_with_unity_instance with async_send_command_with_retry, following the established pattern from other tools in the codebase.

MCPForUnity/Editor/Helpers/MaterialOps.cs (2)

154-178: Good alias resolution for cross-pipeline compatibility.

The ResolvePropertyName method provides robust mapping between common property aliases (e.g., _Color_BaseColor, _MainTex_BaseMap) and handles both shader property names and friendly names like "metallic" and "albedo". This supports the PR's goal of smart alias resolution.


301-339: ParseColor handles multiple formats robustly.

The method correctly handles:

  • Stringified JSON arrays
  • 3-element arrays (RGB with alpha defaulting to 1)
  • 4-element arrays (RGBA)
  • Falls back to serializer for object formats
Server/src/services/tools/manage_gameobject.py (6)

1-9: LGTM!

The imports are correctly updated to include Union for the updated type annotations and parse_json_payload from the centralized utilities module. This aligns with the broader refactoring toward shared JSON parsing utilities.


17-17: Good defensive pattern for handling missing action.

Making action Optional with explicit validation provides a better user experience with a clear error message instead of a cryptic type error.


28-33: LGTM!

The Union[list[float], str] type annotations accurately document that these vector parameters accept both native lists and JSON strings, improving clarity for MCP clients.


49-54: LGTM!

The Union type for component_properties properly documents that it accepts both dict objects and JSON strings.


140-145: LGTM!

Clean simplification using the centralized parse_json_payload utility with proper type validation afterward.


240-241: LGTM!

The {e!s} formatting is a clean standardization of exception string conversion.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (7)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs (2)

14-47: Strengthen assertions to validate console behavior, not just shape

Both tests currently only assert success (and that data is a JArray), without checking that the console was actually cleared or that the logged message made it through the pipeline. This makes the tests pass even if the underlying implementation partially breaks.

Consider tightening the assertions, e.g.:

  • In HandleCommand_Clear_Works, call a get before and after clear (or inspect the returned payload if clear already returns a count) and assert that the entries are reduced/empty after clear.
  • In HandleCommand_Get_Works, use the Debug.Log("Test Log Message") setup to assert that at least one entry exists and ideally that one of the returned log entries’ message field contains "Test Log Message".

This will better guard against regressions in the reflection layer around UnityEditor.LogEntries and ensure you’re testing observable behavior, not just the contract surface.


49-52: Handle null result in ToJObject for clearer test failures

ToJObject will throw via JObject.FromObject(result) if HandleCommand ever returns null, which can obscure the failure cause.

You could make null handling explicit and keep assertion messages clearer:

-        private static JObject ToJObject(object result)
-        {
-            return result as JObject ?? JObject.FromObject(result);
-        }
+        private static JObject ToJObject(object result)
+        {
+            if (result == null)
+            {
+                Assert.Fail("ReadConsole.HandleCommand returned null.");
+                return new JObject(); // Unreachable, but satisfies return type.
+            }
+
+            return result as JObject ?? JObject.FromObject(result);
+        }

This keeps the helper convenient while giving a precise error if the contract is broken.

Server/tests/integration/test_manage_asset_json_parsing.py (1)

144-157: Remove dead comment block or complete the test.

Lines 144-157 contain an extended comment explaining limitations but no actual assertion. The test test_component_properties_json_string_parsing effectively only asserts result["success"] is True without verifying that JSON parsing occurred. Since test_component_properties_parsing_verification (lines 158-181) already properly tests this behavior with captured params, consider removing the redundant/incomplete test or consolidating them.

     @pytest.mark.asyncio
     async def test_component_properties_json_string_parsing(self, monkeypatch):
-        """Test that JSON string component_properties are correctly parsed."""
+        """Test that JSON string component_properties result in successful operation."""
         from services.tools.manage_gameobject import manage_gameobject

         ctx = DummyContext()

         async def fake_send(cmd, params, **kwargs):
             return {"success": True, "message": "GameObject created successfully"}
         monkeypatch.setattr(
             "services.tools.manage_gameobject.async_send_command_with_retry",
             fake_send,
         )

         # Test with JSON string component_properties
         result = await manage_gameobject(
             ctx=ctx,
             action="create",
             name="TestObject",
             component_properties='{"MeshRenderer": {"material": "Assets/Materials/BlueMaterial.mat"}}'
         )

-        # Verify JSON parsing was logged
-        
         # Verify the result
         assert result["success"] is True
-        
-        # Verify that component_properties reached Unity as a dict
-        # We can't easily check 'captured_params' here without refactoring the test to capture args,
-        # but since we mocked the transport, we can trust the return value and rely on
-        # unit tests for parse_json_payload.
-        # However, to follow the feedback, let's verify implicit behavior or refactor.
-        # Since I cannot easily monkeypatch a capture variable here without changing the test structure significantly,
-        # I will rely on the fact that if it wasn't parsed, it would likely fail downstream or be passed as string.
-        # The feedback suggested: "captured_params = {} ... monkeypatch ... assert isinstance(captured_params...)"
-        # I'll implement that pattern.
MCPForUnity/Editor/Tools/ManageMaterial.cs (2)

11-12: Consider making the class static.

All methods are static, so the class should also be declared static to prevent unnecessary instantiation and align with ManageAsset.

     [McpForUnityTool("manage_material", AutoRegister = false)]
-    public class ManageMaterial
+    public static class ManageMaterial

283-293: Redundant slot bounds check.

The slot bounds are already validated at lines 275-278, so the check at line 283 (slot < renderer.sharedMaterials.Length) is always true and can be simplified.

-                if (slot < renderer.sharedMaterials.Length && renderer.sharedMaterials[slot] != null)
+                if (renderer.sharedMaterials[slot] != null)
                 {
                     Material mat = renderer.sharedMaterials[slot];
                     if (mat.HasProperty("_BaseColor")) block.SetColor("_BaseColor", color);
                     else if (mat.HasProperty("_Color")) block.SetColor("_Color", color);
                     else block.SetColor("_Color", color);
                 }
Server/src/services/tools/manage_material.py (1)

85-87: Consider formatting for readability.

Line 85 is quite long. Consider breaking it across multiple lines for better readability.

-    result = await send_with_unity_instance(async_send_command_with_retry, unity_instance, "manage_material", params_dict)
+    result = await send_with_unity_instance(
+        async_send_command_with_retry,
+        unity_instance,
+        "manage_material",
+        params_dict
+    )
MCPForUnity/Editor/Helpers/MaterialOps.cs (1)

309-347: Add error handling for serializer fallback.

Line 346 uses the serializer as a fallback without error handling. If the token cannot be converted to a Color, this will throw an unhandled exception.

             }
-            
-            return token.ToObject<Color>(serializer);
+            
+            try
+            {
+                return token.ToObject<Color>(serializer);
+            }
+            catch (Exception ex)
+            {
+                Debug.LogWarning($"[MaterialOps] Failed to parse color from token: {ex.Message}");
+                return Color.white; // Return sensible default
+            }
         }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f059dcf and 753e156.

📒 Files selected for processing (11)
  • MCPForUnity/Editor/Helpers/MaterialOps.cs (1 hunks)
  • MCPForUnity/Editor/Tools/ManageAsset.cs (2 hunks)
  • MCPForUnity/Editor/Tools/ManageMaterial.cs (1 hunks)
  • Server/src/services/tools/manage_gameobject.py (7 hunks)
  • Server/src/services/tools/manage_material.py (1 hunks)
  • Server/src/services/tools/utils.py (2 hunks)
  • Server/tests/integration/test_manage_asset_json_parsing.py (2 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialPropertiesTests.cs (1 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialStressTests.cs (1 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialTests.cs (1 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialPropertiesTests.cs
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialTests.cs
🧰 Additional context used
🧬 Code graph analysis (4)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs (1)
MCPForUnity/Editor/Tools/ReadConsole.cs (1)
  • ReadConsole (38-126)
Server/src/services/tools/manage_material.py (3)
Server/src/services/tools/utils.py (1)
  • parse_json_payload (27-60)
Server/src/transport/unity_transport.py (1)
  • send_with_unity_instance (78-103)
Server/src/transport/legacy/unity_connection.py (1)
  • async_send_command_with_retry (753-785)
MCPForUnity/Editor/Tools/ManageMaterial.cs (4)
MCPForUnity/Editor/Tools/ManageGameObject.cs (4)
  • HandleCommand (42-217)
  • Type (2465-2479)
  • Color (2308-2321)
  • GameObject (1512-1535)
MCPForUnity/Editor/Tools/ManageAsset.cs (2)
  • HandleCommand (46-127)
  • CreateAsset (163-300)
MCPForUnity/Editor/Helpers/GameObjectSerializer.cs (1)
  • JToken (536-555)
MCPForUnity/Editor/Helpers/MaterialOps.cs (5)
  • MaterialOps (12-348)
  • ResolvePropertyName (157-181)
  • TrySetShaderProperty (204-304)
  • Color (309-347)
  • ApplyProperties (17-152)
MCPForUnity/Editor/Helpers/MaterialOps.cs (1)
MCPForUnity/Editor/Helpers/AssetPathUtility.cs (2)
  • AssetPathUtility (15-201)
  • SanitizeAssetPath (20-34)
🪛 Ruff (0.14.7)
Server/tests/integration/test_manage_asset_json_parsing.py

165-165: Unused function argument: cmd

(ARG001)


165-165: Unused function argument: kwargs

(ARG001)

🔇 Additional comments (17)
Server/src/services/tools/utils.py (1)

27-60: LGTM! Tolerant JSON parsing utility with sensible fallback.

The fast-path heuristic correctly avoids unnecessary parsing attempts while gracefully falling back to the original value on failure. Minor note: scientific notation (e.g., "1e5") won't be detected by the fast path, but since json.loads would handle it and failures return the original string, this is acceptable.

Server/tests/integration/test_manage_asset_json_parsing.py (1)

158-181: LGTM! Proper verification of parsed parameters.

This test correctly captures the parameters sent to the transport and verifies that componentProperties is a dict after parsing. This is the right approach for verifying the centralized parser behavior.

MCPForUnity/Editor/Tools/ManageAsset.cs (2)

216-216: LGTM! Clean delegation to centralized MaterialOps.

Good refactor to use MaterialOps.ApplyProperties for material property application, ensuring consistent behavior across the codebase.


444-444: Consistent usage of MaterialOps for modification path.

This aligns with the creation path and ensures material modifications use the same centralized logic.

MCPForUnity/Editor/Tools/ManageMaterial.cs (2)

14-55: LGTM! Clean command dispatcher with proper error handling.

The action routing is well-structured with a catch-all exception handler that returns structured error responses including stack trace for debugging.


186-233: LGTM! Proper slot bounds validation.

Good job validating slot bounds at lines 223-226 before accessing the materials array, addressing the pattern mentioned in past review comments.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageMaterialStressTests.cs (4)

18-40: LGTM! Well-structured test setup with proper isolation.

Good use of GUID for unique test paths, shader fallback for compatibility across render pipelines, and proper asset database operations.


72-113: LGTM! Comprehensive error handling validation.

Good coverage of error paths: invalid material path, malformed color array, and out-of-bounds slot index. The assertions properly verify error responses without throwing exceptions.


115-152: LGTM! Important state isolation test.

This test properly validates that property_block mode doesn't leak color changes to the shared material asset - a critical behavior for the material management tool.


154-191: LGTM! Good integration test for the full workflow.

Tests the complete flow of assigning a material and modifying its color, verifying the changes propagate correctly to the renderer.

Server/src/services/tools/manage_material.py (1)

1-17: LGTM!

The imports are well-organized, and the tool registration with the mcp_for_unity_tool decorator is appropriate.

MCPForUnity/Editor/Helpers/MaterialOps.cs (3)

157-181: LGTM!

The property name resolution logic correctly handles common aliases with case-insensitive matching and sensible fallback behavior.


186-198: LGTM!

The main color property detection logic is well-structured with appropriate fallback behavior.


286-298: Verify Object-to-Texture deserialization behavior.

Deserializing a JObject directly to a Unity Texture type (line 290) requires custom JSON converters. With the default serializer, this will likely always fail and fall through to the warning at line 300. Verify whether this code path is intended to work or if it's dead code. Consider either adding custom JSON converters for Texture serialization, or removing this block if texture assignment via objects is not supported.

Server/src/services/tools/manage_gameobject.py (3)

2-3: LGTM!

The addition of Union types and parse_json_payload import properly supports the centralized JSON parsing improvements in this PR.

Also applies to: 10-10


90-94: Excellent UX improvement!

Adding explicit validation for the missing action parameter prevents confusing downstream errors and provides clear guidance to users.


96-145: LGTM!

The refactoring to use parse_json_payload centralizes JSON parsing logic while maintaining backward compatibility with legacy comma-separated string formats.

@Scriptwonder
Copy link
Collaborator

Does it solve the material problem you mentioned? I was about to tackle it but saw this repo, looks like a big re-structure.

@dsarno
Copy link
Collaborator Author

dsarno commented Dec 8, 2025

@Scriptwonder I've tested it a bunch and it seems to work pretty well. Not perfect yet because material params are more complex, but even the dumber models seem to be able to figure it out if they get stuck. I'd like to move forward with it for now and see how it goes. It's not a huge change really,just a new smallish tool and removing some of that responsibility from manage_asset.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 (6)
Server/tests/integration/test_manage_asset_json_parsing.py (1)

147-170: Excellent addition - this test directly validates the parsing contract.

The new test effectively verifies that component_properties JSON strings are parsed to dictionaries before being dispatched to Unity, strengthening the test coverage for the centralized parsing approach.

To silence the static analysis warning about unused arguments in fake_send, consider prefixing them with underscores to indicate they're intentionally unused:

-        async def fake_send(cmd, params, **kwargs):
+        async def fake_send(_cmd, params, **_kwargs):
             captured_params.update(params)
             return {"success": True, "message": "GameObject created successfully"}
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs (2)

43-75: Reduce chance of missing the unique log entry by increasing count

HandleCommand_Get_Works searches only the first 10 entries for the unique message. If there’s a burst of logging between the Debug.Log and the get call, the message could fall outside that window and make the test flaky.

A small tweak like increasing count makes the test more robust without changing intent:

-                ["action"] = "get",
-                ["count"] = 10 // Fetch enough to likely catch our message
+                ["action"] = "get",
+                ["count"] = 1000 // Fetch enough to very likely catch our message

The rest of the assertions and search loop look solid.


77-86: Helper for coercing HandleCommand results looks good

ToJObject gives strong guarantees for the tests: it fails fast on null and cleanly handles both JObject and POCO-style results via JObject.FromObject. The unreachable return new JObject() after Assert.Fail is harmless and can be left as-is for clarity or removed if you prefer strictness.

MCPForUnity/Editor/Tools/ManageMaterial.cs (2)

85-116: Texture-instruction branch skips alias resolution and HasProperty checks.

In SetMaterialShaderProperty, when value is an instruction object you call mat.SetTexture(property, tex) before running ResolvePropertyName. That means aliases like "albedo" or case variants won’t be mapped, and you may set a non‑existent property while returning success.

Consider normalizing and validating property up front so both the instruction and fallback paths share the same behavior:

-            Undo.RecordObject(mat, "Set Material Property");
-            
-            // 1. Try handling Texture instruction explicitly (ManageMaterial special feature)
-            if (value.Type == JTokenType.Object)
+            Undo.RecordObject(mat, "Set Material Property");
+
+            // Normalize alias/casing once for all code paths
+            property = MaterialOps.ResolvePropertyName(mat, property);
+
+            // 1. Try handling Texture instruction explicitly (ManageMaterial special feature)
+            if (value.Type == JTokenType.Object)
             {
                 // Check if it looks like an instruction
                 if (value is JObject obj && (obj.ContainsKey("find") || obj.ContainsKey("method")))
                 {
                     Texture tex = ManageGameObject.FindObjectByInstruction(obj, typeof(Texture)) as Texture;
-                    if (tex != null)
+                    if (tex != null && mat.HasProperty(property))
                     {
                         mat.SetTexture(property, tex);
                         EditorUtility.SetDirty(mat);
                         return new { status = "success", message = $"Set texture property {property} on {mat.name}" };
                     }
                 }
             }
-            
-            // 2. Fallback to standard logic via MaterialOps (handles Colors, Floats, Strings->Path)
-            property = MaterialOps.ResolvePropertyName(mat, property);
+
+            // 2. Fallback to standard logic via MaterialOps (handles Colors, Floats, Strings->Path)
             bool success = MaterialOps.TrySetShaderProperty(mat, property, value, ManageGameObject.InputSerializer);

401-438: Consider using RenderPipelineUtility.ResolveShader instead of Shader.Find.

CreateMaterial currently calls Shader.Find(shaderName) directly, so aliases like lit, urp_lit, etc. and pipeline‑aware fallbacks from RenderPipelineUtility are bypassed. Using the helper would make shader selection consistent with MaterialOps.ApplyProperties and more robust across pipelines.

-            Shader shader = Shader.Find(shaderName);
+            Shader shader = RenderPipelineUtility.ResolveShader(shaderName);
MCPForUnity/Editor/Helpers/MaterialOps.cs (1)

227-286: Guard numeric properties with HasProperty to avoid false “success”.

In TrySetShaderProperty, float/bool branches call material.SetFloat(propertyName, ...) without checking material.HasProperty(propertyName). If the property doesn’t exist, Unity will log an error but this method still returns true, which can mislead callers (e.g., ApplyProperties treating it as modified).

For consistency with the color/vector paths and ResolvePropertyName, consider adding a HasProperty check:

-            else if (value.Type == JTokenType.Float || value.Type == JTokenType.Integer)
-            {
-                try { material.SetFloat(propertyName, value.ToObject<float>(serializer)); return true; } 
+            else if (value.Type == JTokenType.Float || value.Type == JTokenType.Integer)
+            {
+                if (!material.HasProperty(propertyName))
+                    return false;
+
+                try { material.SetFloat(propertyName, value.ToObject<float>(serializer)); return true; } 
                 catch (Exception ex)
                 {
                      Debug.Log($"[MaterialOps] SetFloat attempt for '{propertyName}' failed: {ex.Message}");
                 }
             }
             else if (value.Type == JTokenType.Boolean)
             {
-                try { material.SetFloat(propertyName, value.ToObject<bool>(serializer) ? 1f : 0f); return true; } 
+                if (!material.HasProperty(propertyName))
+                    return false;
+
+                try { material.SetFloat(propertyName, value.ToObject<bool>(serializer) ? 1f : 0f); return true; } 
                 catch (Exception ex)
                 {
                      Debug.Log($"[MaterialOps] SetFloat (bool) attempt for '{propertyName}' failed: {ex.Message}");
                 }
             }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 753e156 and 784ecd4.

📒 Files selected for processing (5)
  • MCPForUnity/Editor/Helpers/MaterialOps.cs (1 hunks)
  • MCPForUnity/Editor/Tools/ManageMaterial.cs (1 hunks)
  • Server/src/services/tools/manage_material.py (1 hunks)
  • Server/tests/integration/test_manage_asset_json_parsing.py (3 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-05T18:23:12.349Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 368
File: MCPForUnity/UnityMcpServer~/src/resources/menu_items.py:15-15
Timestamp: 2025-11-05T18:23:12.349Z
Learning: In Unity MCP, the `name` parameter in the `mcp_for_unity_resource` decorator is the external API name exposed to MCP clients (LLMs, AI agents). The command string passed to `async_send_command_with_retry` or `async_send_with_unity_instance` (e.g., "get_menu_items") is the internal command identifier that must match the C# side. These are decoupled, allowing external API naming to evolve independently of internal command routing.

Applied to files:

  • MCPForUnity/Editor/Tools/ManageMaterial.cs
📚 Learning: 2025-09-03T16:00:55.839Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 0
File: :0-0
Timestamp: 2025-09-03T16:00:55.839Z
Learning: ComponentResolver in UnityMcpBridge/Editor/Tools/ManageGameObject.cs is a nested static class within ManageGameObject, not a sibling type. The `using static MCPForUnity.Editor.Tools.ManageGameObject;` import is required to access ComponentResolver methods directly without the outer class qualifier.

Applied to files:

  • MCPForUnity/Editor/Tools/ManageMaterial.cs
🧬 Code graph analysis (4)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs (1)
MCPForUnity/Editor/Tools/ReadConsole.cs (1)
  • ReadConsole (38-126)
MCPForUnity/Editor/Helpers/MaterialOps.cs (2)
MCPForUnity/Editor/Helpers/RenderPipelineUtility.cs (1)
  • RenderPipelineUtility (7-194)
MCPForUnity/Editor/Helpers/AssetPathUtility.cs (2)
  • AssetPathUtility (15-201)
  • SanitizeAssetPath (20-34)
Server/tests/integration/test_manage_asset_json_parsing.py (2)
Server/src/services/tools/manage_gameobject.py (1)
  • manage_gameobject (16-240)
Server/tests/integration/test_helpers.py (1)
  • DummyContext (16-55)
MCPForUnity/Editor/Tools/ManageMaterial.cs (2)
MCPForUnity/Editor/Tools/ManageGameObject.cs (4)
  • UnityEngine (2361-2458)
  • Type (2465-2479)
  • Color (2308-2321)
  • GameObject (1512-1535)
MCPForUnity/Editor/Helpers/MaterialOps.cs (5)
  • MaterialOps (12-384)
  • ResolvePropertyName (160-184)
  • TrySetShaderProperty (207-328)
  • Color (333-383)
  • ApplyProperties (17-155)
🪛 Ruff (0.14.7)
Server/tests/integration/test_manage_asset_json_parsing.py

154-154: Unused function argument: cmd

(ARG001)


154-154: Unused function argument: kwargs

(ARG001)

🔇 Additional comments (3)
Server/tests/integration/test_manage_asset_json_parsing.py (2)

36-39: LGTM!

The assertion correctly verifies that the centralized parser was invoked by checking for the expected log message. This aligns with the PR's goal of centralizing JSON parsing.


123-123: LGTM!

The updated docstring appropriately focuses on the test outcome rather than implementation details, making it more maintainable.

Server/src/services/tools/manage_material.py (1)

18-67: Async tool wiring and slot coercion look solid.

The parameter mapping to C# (materialPath, searchMethod, etc.), JSON payload parsing via parse_json_payload, and explicit slot coercion with a clear error on invalid strings all look correct and should give much clearer failures than deferring to Unity.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
MCPForUnity/Editor/Helpers/MaterialOps.cs (1)

213-224: Optional: log parse/texture-conversion failures for easier debugging

The catch { } blocks around JToken.Parse (stringified JSON) and value.ToObject<Texture> swallow exceptions silently. While the final warning at the end of TrySetShaderProperty gives some feedback, having at least a lightweight debug-level log when these conversions fail would make diagnosing malformed JSON or unexpected texture payloads much easier.

Consider adding a Debug.Log (or project-standard logging) including propertyName and the exception message in these catches, similar to the color/vector/float branches you already updated.

Also applies to: 312-313, 327-327

🧹 Nitpick comments (1)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs (1)

15-42: Avoid potential flakiness by not asserting global console emptiness

Requiring entriesAfter == null || entriesAfter.Count == 0 assumes no other logs are written after clear, which might not hold if Unity or other tests log during the window, causing intermittent failures even when Clear works.

Consider asserting that the specific marker log you added is gone instead of asserting the entire console is empty, e.g.:

-            // Ensure there's something to clear
-            Debug.Log("Log to clear");
+            // Ensure there's something to clear
+            const string markerMessage = "Log to clear";
+            Debug.Log(markerMessage);
@@
-            var entriesAfter = getAfter["data"] as JArray;
-            Assert.IsTrue(entriesAfter == null || entriesAfter.Count == 0, "Console should be empty after clear.");
+            var entriesAfter = getAfter["data"] as JArray;
+            if (entriesAfter != null)
+            {
+                foreach (var entry in entriesAfter)
+                {
+                    Assert.IsFalse(
+                        entry["message"]?.ToString().Contains(markerMessage) == true,
+                        "Console should not contain the marker log after clear."
+                    );
+                }
+            }

This still validates that clear removed the intended entry but is resilient to unrelated logs.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 784ecd4 and dc095d5.

📒 Files selected for processing (4)
  • MCPForUnity/Editor/Helpers/MaterialOps.cs (1 hunks)
  • MCPForUnity/Editor/Tools/ManageMaterial.cs (1 hunks)
  • Server/tests/integration/test_manage_asset_json_parsing.py (3 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • MCPForUnity/Editor/Tools/ManageMaterial.cs
🧰 Additional context used
🧬 Code graph analysis (3)
Server/tests/integration/test_manage_asset_json_parsing.py (2)
Server/src/services/tools/manage_gameobject.py (1)
  • manage_gameobject (16-240)
Server/tests/integration/test_helpers.py (1)
  • DummyContext (16-55)
MCPForUnity/Editor/Helpers/MaterialOps.cs (1)
MCPForUnity/Editor/Helpers/AssetPathUtility.cs (2)
  • AssetPathUtility (15-201)
  • SanitizeAssetPath (20-34)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs (1)
MCPForUnity/Editor/Tools/ReadConsole.cs (1)
  • ReadConsole (38-126)
🪛 Ruff (0.14.7)
Server/tests/integration/test_manage_asset_json_parsing.py

128-128: Unused function argument: params

(ARG001)

🔇 Additional comments (7)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs (2)

45-77: Get test is robust and focused on observable behavior

Using a GUID-backed uniqueMessage, asserting success, and then searching the returned data for that message gives a clear, behavior-focused test of get. This looks good as-is.


79-88: Null-guard and normalization helper looks good

ToJObject failing fast on null and normalizing non-JObject results via JObject.FromObject is a reasonable, defensive helper for these tests.

MCPForUnity/Editor/Helpers/MaterialOps.cs (2)

14-155: ApplyProperties, aliasing, and structured formats look solid

The overall flow in ApplyProperties (shader/color/float/texture structured handling + alias-aware fallback via ResolvePropertyName and TrySetShaderProperty) is coherent and defensive: null checks, HasProperty checks, and modification tracking are all in place and align well with the centralized material-handling goal of the PR.


339-389: ParseColor covers key cases; consider extending formats only if needed

ParseColor’s handling of arrays (RGB/RGBA) and delegating to the serializer for object cases is clear and matches the material JSON formats used in this PR. The explicit exception and warning path for invalid arrays is also helpful for callers.

If future payloads introduce alternative formats (e.g., "r,g,b,a" strings or 0–255 integer channels), they can be added here without touching the higher-level APIs.

Server/tests/integration/test_manage_asset_json_parsing.py (3)

35-39: More robust log assertion for JSON coercion

Using any("manage_asset: coerced properties using centralized parser" in msg for msg in ctx.log_info) is a good change: it makes the test resilient to additional info logs while still verifying that the coercion path was exercised.


148-170: New parsing verification test accurately asserts dict coercion

test_component_properties_parsing_verification correctly captures the params sent to Unity and asserts that componentProperties is a dict, aligning the test with the parse_json_payload behavior in manage_gameobject. This is a useful safeguard against regressions in the centralized parsing path.


121-161: Clarify which fake_send function has unused parameters

The review states both helpers have unused params arguments, but the second function's code snippet shows captured_params.update(params)—indicating the parameter is actively used. If the second function uses params, it should not be flagged as unused, and renaming it provides no lint benefit. Clarify whether the second function actually uses this parameter or if only the first function needs the rename.

…operty checks

- Add comprehensive ManageMaterial tool documentation to MCPForUnity/README.md
- Add manage_material to tools list in README.md and README-zh.md
- Fix MaterialOps.cs to check HasProperty before SetTexture calls to prevent Unity warnings
- Ensures consistency with other property setters in MaterialOps
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
MCPForUnity/Editor/Helpers/MaterialOps.cs (1)

293-328: Avoid empty catch blocks in texture conversion branches

The string/object texture branches still swallow exceptions silently (catch { }), unlike the color/vector/float branches above where you now log failures. This can make it hard to understand why a texture assignment failed when the final generic warning at Lines 330–332 isn’t enough.

Consider logging at least the exception message and some context:

-            else if (value.Type == JTokenType.String)
-            {
-                try
-                {
+            else if (value.Type == JTokenType.String)
+            {
+                try
+                {
                     string path = value.ToString();
                     if (!string.IsNullOrEmpty(path) && path.Contains("/"))
                     {
                         var sanitizedPath = AssetPathUtility.SanitizeAssetPath(path);
                         Texture tex = AssetDatabase.LoadAssetAtPath<Texture>(sanitizedPath);
                         if (tex != null && material.HasProperty(propertyName))
                         {
                             material.SetTexture(propertyName, tex);
                             return true;
                         }
                     }
-                }
-                catch { }
+                }
+                catch (Exception ex)
+                {
+                    Debug.LogWarning($"[MaterialOps] SetTexture (string path) for '{propertyName}' failed: {ex.Message}");
+                }
             }
 …
-            if (value.Type == JTokenType.Object)
-            {
-                 try
-                {
+            if (value.Type == JTokenType.Object)
+            {
+                try
+                {
                     Texture texture = value.ToObject<Texture>(serializer);
                     if (texture != null && material.HasProperty(propertyName))
                     {
                         material.SetTexture(propertyName, texture);
                         return true;
                     }
-                }
-                catch { }
+                }
+                catch (Exception ex)
+                {
+                    Debug.LogWarning($"[MaterialOps] SetTexture (object) for '{propertyName}' failed: {ex.Message}");
+                }
             }

This keeps control flow the same while giving you concrete signals when conversions fail.

🧹 Nitpick comments (1)
MCPForUnity/Editor/Helpers/MaterialOps.cs (1)

46-69: Restrict structured color arrays to 3 or 4 elements to match ParseColor

ParseColor throws unless the array has exactly 3 or 4 elements, but the guard at Line 50 currently accepts colArr.Count >= 3. For counts >4 this always falls into the else branch in ParseColor and throws only to be caught/logged here.

Tightening the check would make intent clearer and avoid work on obviously-invalid input:

-                if (colorProps["value"] is JArray colArr && colArr.Count >= 3)
+                if (colorProps["value"] is JArray colArr && (colArr.Count == 3 || colArr.Count == 4))

(Same pattern already exists implicitly in ParseColor; this just aligns the structured path with it.)

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dc095d5 and ceef08b.

📒 Files selected for processing (3)
  • MCPForUnity/Editor/Helpers/MaterialOps.cs (1 hunks)
  • README-zh.md (1 hunks)
  • README.md (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
MCPForUnity/Editor/Helpers/MaterialOps.cs (2)
MCPForUnity/Editor/Helpers/RenderPipelineUtility.cs (1)
  • RenderPipelineUtility (7-194)
MCPForUnity/Editor/Helpers/AssetPathUtility.cs (2)
  • AssetPathUtility (15-201)
  • SanitizeAssetPath (20-34)
🔇 Additional comments (3)
MCPForUnity/Editor/Helpers/MaterialOps.cs (1)

17-155: Centralized JSON→material application looks robust and low-noise

The overall ApplyProperties flow (null-guards, case-insensitive lookup, separation of structured vs direct keys, alias resolution + HasProperty checks, and modified tracking) is well-structured and should behave predictably across shaders and pipelines.

README-zh.md (1)

42-61: manage_material tool entry matches actual capabilities

The new manage_material bullet accurately reflects the tool’s responsibilities (creation, property setting, renderer assignment, querying info) and is consistent with the implementation.

README.md (1)

42-63: Docs for manage_material tool are clear and aligned with the implementation

The new manage_material description concisely captures the supported actions and fits well in the tools list alongside existing entries.

@Scriptwonder
Copy link
Collaborator

Sounds good to me, and looking forward to actually use it during the gamejam (which I am sure we can get a lot of feedback on the tools during it). Can you also edit the readme and specify it? Thanks.

@dsarno dsarno merged commit fe4cae7 into CoplayDev:main Dec 8, 2025
1 check was pending
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
MCPForUnity/Editor/Tools/ManageMaterial.cs (2)

14-55: Add a null guard for @params in HandleCommand.

HandleCommand assumes @params is non-null; a null call will throw before you hit the existing "Action is required" check. Consider an early guard:

public static object HandleCommand(JObject @params)
{
-    string action = @params["action"]?.ToString();
+    if (@params == null)
+    {
+        return new { status = "error", message = "params object is required" };
+    }
+
+    string action = @params["action"]?.ToString();

This keeps the tool robust against malformed calls.


128-194: Use ResolvePropertyName/GetMainColorPropertyName to align color property behavior.

SetMaterialColor currently:

  • Checks the provided property name directly with mat.HasProperty(property).
  • Falls back only to _BaseColor / _Color.

Elsewhere you already centralize aliasing and main-color detection in MaterialOps, so this entrypoint will behave differently (e.g., "albedo" / "basecolor" won’t resolve, and shaders using _MainColor/_Tint are ignored here).

You can reuse the helpers to make behavior consistent:

-            bool foundProp = false;
-            if (!string.IsNullOrEmpty(property))
-            {
-                if (mat.HasProperty(property))
-                {
-                    mat.SetColor(property, color);
-                    foundProp = true;
-                }
-            }
-            else
-            {
-                // Fallback logic: _BaseColor (URP/HDRP) then _Color (Built-in)
-                if (mat.HasProperty("_BaseColor"))
-                {
-                    mat.SetColor("_BaseColor", color);
-                    foundProp = true;
-                    property = "_BaseColor";
-                }
-                else if (mat.HasProperty("_Color"))
-                {
-                    mat.SetColor("_Color", color);
-                    foundProp = true;
-                    property = "_Color";
-                }
-            }
+            bool foundProp = false;
+            if (!string.IsNullOrEmpty(property))
+            {
+                // Normalize aliases/casing for explicit property names
+                property = MaterialOps.ResolvePropertyName(mat, property);
+                if (mat.HasProperty(property))
+                {
+                    mat.SetColor(property, color);
+                    foundProp = true;
+                }
+            }
+            else
+            {
+                // Use shared helper to pick the main color slot (_BaseColor, _Color, _Tint, etc.)
+                property = MaterialOps.GetMainColorPropertyName(mat);
+                if (mat.HasProperty(property))
+                {
+                    mat.SetColor(property, color);
+                    foundProp = true;
+                }
+            }

This way manage_material color setting matches the alias and shader‑agnostic behavior you already expose via MaterialOps.

MCPForUnity/Editor/Helpers/MaterialOps.cs (1)

45-111: Consider resolving aliases for structured "color"/"float" names as well.

In ApplyProperties:

  • Structured blocks ("color": { "name": "...", "value": [...] } and "float": { "name": "...", "value": ... }) use the raw name when checking mat.HasProperty(propName).
  • Direct properties below already pass all keys through ResolvePropertyName.

That means "albedo", "metallic", etc. work in the direct form but not inside the structured "color"/"float" objects, which is a bit surprising.

You could normalize structured names too, e.g.:

// color (structured)
- string propName = colorProps["name"]?.ToString() ?? GetMainColorPropertyName(mat);
+ string rawColorName = colorProps["name"]?.ToString();
+ string propName = string.IsNullOrEmpty(rawColorName)
+     ? GetMainColorPropertyName(mat)
+     : ResolvePropertyName(mat, rawColorName);

// float (structured)
- string propName = floatProps["name"]?.ToString();
+ string rawFloatName = floatProps["name"]?.ToString();
+ string propName = string.IsNullOrEmpty(rawFloatName)
+     ? null
+     : ResolvePropertyName(mat, rawFloatName);

This keeps alias behavior consistent regardless of whether callers use structured or direct property formats.

Also applies to: 139-152

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ceef08b and fe685c2.

📒 Files selected for processing (2)
  • MCPForUnity/Editor/Helpers/MaterialOps.cs (1 hunks)
  • MCPForUnity/Editor/Tools/ManageMaterial.cs (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-11-05T18:23:12.349Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 368
File: MCPForUnity/UnityMcpServer~/src/resources/menu_items.py:15-15
Timestamp: 2025-11-05T18:23:12.349Z
Learning: In Unity MCP, the `name` parameter in the `mcp_for_unity_resource` decorator is the external API name exposed to MCP clients (LLMs, AI agents). The command string passed to `async_send_command_with_retry` or `async_send_with_unity_instance` (e.g., "get_menu_items") is the internal command identifier that must match the C# side. These are decoupled, allowing external API naming to evolve independently of internal command routing.

Applied to files:

  • MCPForUnity/Editor/Tools/ManageMaterial.cs
📚 Learning: 2025-09-03T16:00:55.839Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 0
File: :0-0
Timestamp: 2025-09-03T16:00:55.839Z
Learning: ComponentResolver in UnityMcpBridge/Editor/Tools/ManageGameObject.cs is a nested static class within ManageGameObject, not a sibling type. The `using static MCPForUnity.Editor.Tools.ManageGameObject;` import is required to access ComponentResolver methods directly without the outer class qualifier.

Applied to files:

  • MCPForUnity/Editor/Tools/ManageMaterial.cs
📚 Learning: 2025-09-05T16:22:04.960Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 265
File: README.md:204-204
Timestamp: 2025-09-05T16:22:04.960Z
Learning: In the Unity MCP project, the ServerInstaller.cs creates a symlink from ~/Library/AppSupport to ~/Library/Application Support on macOS to mitigate argument parsing and quoting issues in some MCP clients. The README documentation should use the shortened AppSupport path, not the full "Application Support" path with spaces.

Applied to files:

  • MCPForUnity/Editor/Tools/ManageMaterial.cs
📚 Learning: 2025-09-05T16:22:04.960Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 265
File: README.md:204-204
Timestamp: 2025-09-05T16:22:04.960Z
Learning: In the Unity MCP project, the ServerInstaller.cs creates a symlink from ~/Library/AppSupport to ~/Library/Application Support on macOS to avoid spaces in paths that can cause arg parsing and quoting issues in some MCP clients. The README documentation should use the shortened AppSupport path, not the full "Application Support" path.

Applied to files:

  • MCPForUnity/Editor/Tools/ManageMaterial.cs
🔇 Additional comments (5)
MCPForUnity/Editor/Tools/ManageMaterial.cs (3)

57-71: Path normalization looks solid and cross‑platform‑friendly.

Using AssetPathUtility.SanitizeAssetPath plus enforcing the .mat extension solves the earlier double-Assets/ and backslash issues and keeps behavior consistent with other tools.


73-126: Shader property setting flow is well‑structured.

Good composition here: you resolve aliases via MaterialOps.ResolvePropertyName, special‑case { find/method } texture instructions, and then delegate everything else to MaterialOps.TrySetShaderProperty with proper HasProperty checks and EditorUtility.SetDirty on success. This should cover most material workflows cleanly.


459-516: Material creation flow and safety checks look good.

CreateMaterial now:

  • Normalizes/sanitizes the path and explicitly enforces Assets/ for CreateAsset.
  • Resolves shaders via RenderPipelineUtility.ResolveShader.
  • Prevents silent overwrites with AssetDatabase.LoadAssetAtPath<Material> pre‑check.
  • Applies initial properties via MaterialOps.ApplyProperties and saves assets.

That matches Unity editor best practices for asset creation and keeps behavior predictable.

MCPForUnity/Editor/Helpers/MaterialOps.cs (2)

203-340: TrySetShaderProperty covers the main JSON → shader cases with good diagnostics.

The conversion flow here is comprehensive:

  • Handles stringified JSON ("[...]" / "{...}") by reparsing and recursing.
  • Uses array length heuristics (4/3/2) for Color/Vector4/Vector2 with HasProperty guards.
  • Treats numbers and booleans as floats only when the property exists.
  • Assigns textures from sanitized asset paths and object values, now guarded by material.HasProperty(propertyName).
  • Logs detailed per-attempt failures plus a final warning when no conversion succeeds.

This should make property application debuggable without spamming Unity warnings for missing properties.


345-395: Color parsing helper is robust and consistent with the rest of the API.

ParseColor:

  • Supports stringified JSON tokens by reparsing into arrays/objects.
  • Handles 3- and 4-component arrays with sensible default alpha.
  • Falls back to ToObject<Color> for object forms and logs before rethrowing on failure.

This pairs well with both SetMaterialColor and TrySetShaderProperty and should be easy to reason about for callers.

Comment on lines +245 to +345
private static object SetRendererColor(JObject @params)
{
string target = @params["target"]?.ToString();
string searchMethod = @params["searchMethod"]?.ToString();
JToken colorToken = @params["color"];
int slot = @params["slot"]?.ToObject<int>() ?? 0;
string mode = @params["mode"]?.ToString() ?? "property_block";

if (string.IsNullOrEmpty(target) || colorToken == null)
{
return new { status = "error", message = "target and color are required" };
}

Color color;
try
{
color = MaterialOps.ParseColor(colorToken, ManageGameObject.InputSerializer);
}
catch (Exception e)
{
return new { status = "error", message = $"Invalid color format: {e.Message}" };
}

var goInstruction = new JObject { ["find"] = target };
if (!string.IsNullOrEmpty(searchMethod)) goInstruction["method"] = searchMethod;

GameObject go = ManageGameObject.FindObjectByInstruction(goInstruction, typeof(GameObject)) as GameObject;
if (go == null)
{
return new { status = "error", message = $"Could not find target GameObject: {target}" };
}

Renderer renderer = go.GetComponent<Renderer>();
if (renderer == null)
{
return new { status = "error", message = $"GameObject {go.name} has no Renderer component" };
}

if (mode == "property_block")
{
if (slot < 0 || slot >= renderer.sharedMaterials.Length)
{
return new { status = "error", message = $"Slot {slot} out of bounds (count: {renderer.sharedMaterials.Length})" };
}

MaterialPropertyBlock block = new MaterialPropertyBlock();
renderer.GetPropertyBlock(block, slot);

if (renderer.sharedMaterials[slot] != null)
{
Material mat = renderer.sharedMaterials[slot];
if (mat.HasProperty("_BaseColor")) block.SetColor("_BaseColor", color);
else if (mat.HasProperty("_Color")) block.SetColor("_Color", color);
else block.SetColor("_Color", color);
}
else
{
block.SetColor("_Color", color);
}

renderer.SetPropertyBlock(block, slot);
EditorUtility.SetDirty(renderer);
return new { status = "success", message = $"Set renderer color (PropertyBlock) on slot {slot}" };
}
else if (mode == "shared")
{
if (slot >= 0 && slot < renderer.sharedMaterials.Length)
{
Material mat = renderer.sharedMaterials[slot];
if (mat == null)
{
return new { status = "error", message = $"No material in slot {slot}" };
}
Undo.RecordObject(mat, "Set Material Color");
if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color);
else mat.SetColor("_Color", color);
EditorUtility.SetDirty(mat);
return new { status = "success", message = "Set shared material color" };
}
return new { status = "error", message = "Invalid slot" };
}
else if (mode == "instance")
{
if (slot >= 0 && slot < renderer.materials.Length)
{
Material mat = renderer.materials[slot];
if (mat == null)
{
return new { status = "error", message = $"No material in slot {slot}" };
}
// Note: Undo cannot fully revert material instantiation
Undo.RecordObject(mat, "Set Instance Material Color");
if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color);
else mat.SetColor("_Color", color);
return new { status = "success", message = "Set instance material color", warning = "Material instance created; Undo cannot fully revert instantiation." };
}
return new { status = "error", message = "Invalid slot" };
}

return new { status = "error", message = $"Unknown mode: {mode}" };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard SetColor with HasProperty in shared/instance modes and mark instance materials dirty.

In SetRendererColor:

  • shared mode calls mat.SetColor("_BaseColor") or mat.SetColor("_Color") without guaranteeing that either property exists. For custom shaders this will emit Unity warnings.
  • instance mode has the same pattern and also doesn’t call EditorUtility.SetDirty(mat).

You already have MaterialOps.GetMainColorPropertyName, so you can avoid warnings and centralize the color slot selection:

             else if (mode == "shared")
             {
                 if (slot >= 0 && slot < renderer.sharedMaterials.Length)
                 {
                      Material mat = renderer.sharedMaterials[slot];
                      if (mat == null)
                      {
                          return new { status = "error", message = $"No material in slot {slot}" };
                      }
-                     Undo.RecordObject(mat, "Set Material Color");
-                     if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color);
-                     else mat.SetColor("_Color", color);
-                     EditorUtility.SetDirty(mat);
-                     return new { status = "success", message = "Set shared material color" };
+                     string propName = MaterialOps.GetMainColorPropertyName(mat);
+                     if (!mat.HasProperty(propName))
+                     {
+                         return new { status = "error", message = $"Material {mat.name} has no supported color property" };
+                     }
+                     Undo.RecordObject(mat, "Set Material Color");
+                     mat.SetColor(propName, color);
+                     EditorUtility.SetDirty(mat);
+                     return new { status = "success", message = "Set shared material color" };
                 }
                 return new { status = "error", message = "Invalid slot" };
             }
             else if (mode == "instance")
             {
                 if (slot >= 0 && slot < renderer.materials.Length)
                 {
                      Material mat = renderer.materials[slot]; 
                      if (mat == null)
                      {
                          return new { status = "error", message = $"No material in slot {slot}" };
                      }
-                     // Note: Undo cannot fully revert material instantiation
-                     Undo.RecordObject(mat, "Set Instance Material Color");
-                     if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color);
-                     else mat.SetColor("_Color", color);
-                     return new { status = "success", message = "Set instance material color", warning = "Material instance created; Undo cannot fully revert instantiation." };
+                     // Note: Undo cannot fully revert material instantiation
+                     string propName = MaterialOps.GetMainColorPropertyName(mat);
+                     if (!mat.HasProperty(propName))
+                     {
+                         return new { status = "error", message = $"Material {mat.name} has no supported color property" };
+                     }
+                     Undo.RecordObject(mat, "Set Instance Material Color");
+                     mat.SetColor(propName, color);
+                     EditorUtility.SetDirty(mat);
+                     return new { status = "success", message = "Set instance material color", warning = "Material instance created; Undo cannot fully revert instantiation." };
                  }
                  return new { status = "error", message = "Invalid slot" };
             }

This avoids noisy console warnings and keeps color selection consistent with the rest of the material tooling.

@dsarno dsarno deleted the feat/manage-material-tool branch December 8, 2025 16:59
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.

2 participants