Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 115 additions & 2 deletions MCPForUnity/Editor/Tools/ManageAsset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,19 @@ public static object HandleCommand(JObject @params)
string path = @params["path"]?.ToString();

// Coerce string JSON to JObject for 'properties' if provided as a JSON string
JsonUtil.CoerceJsonStringParameter(@params, "properties");
var propertiesToken = @params["properties"];
if (propertiesToken != null && propertiesToken.Type == JTokenType.String)
{
try
{
var parsed = JObject.Parse(propertiesToken.ToString());
@params["properties"] = parsed;
}
catch (Exception e)
{
Debug.LogWarning($"[ManageAsset] Could not parse 'properties' JSON string: {e.Message}");
}
}

try
{
Expand Down Expand Up @@ -1002,7 +1014,108 @@ private static bool ApplyMaterialProperties(Material mat, JObject properties)
}
}

// TODO: Add handlers for other property types (Vectors, Ints, Keywords, RenderQueue, etc.)
// --- Flexible direct property assignment ---
// Allow payloads like: { "_Color": [r,g,b,a] }, { "_Glossiness": 0.5 }, { "_MainTex": "Assets/.." }
// while retaining backward compatibility with the structured keys above.
// This iterates all top-level keys except the reserved structured ones and applies them
// if they match known shader properties.
var reservedKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "shader", "color", "float", "texture" };

// Helper resolves common URP/Standard aliasing (e.g., _Color <-> _BaseColor, _MainTex <-> _BaseMap, _Glossiness <-> _Smoothness)
string ResolvePropertyName(string name)
{
if (string.IsNullOrEmpty(name)) return name;
string[] candidates;
switch (name)
{
case "_Color": candidates = new[] { "_Color", "_BaseColor" }; break;
case "_BaseColor": candidates = new[] { "_BaseColor", "_Color" }; break;
case "_MainTex": candidates = new[] { "_MainTex", "_BaseMap" }; break;
case "_BaseMap": candidates = new[] { "_BaseMap", "_MainTex" }; break;
case "_Glossiness": candidates = new[] { "_Glossiness", "_Smoothness" }; break;
case "_Smoothness": candidates = new[] { "_Smoothness", "_Glossiness" }; break;
default: candidates = new[] { name }; break;
}
foreach (var candidate in candidates)
{
if (mat.HasProperty(candidate)) return candidate;
}
return name; // fall back to original
}

foreach (var prop in properties.Properties())
{
if (reservedKeys.Contains(prop.Name)) continue;
string shaderProp = ResolvePropertyName(prop.Name);
JToken v = prop.Value;

// Color: numeric array [r,g,b,(a)]
if (v is JArray arr && arr.Count >= 3 && arr.All(t => t.Type == JTokenType.Float || t.Type == JTokenType.Integer))
{
if (mat.HasProperty(shaderProp))
{
try
{
var c = new Color(
arr[0].ToObject<float>(),
arr[1].ToObject<float>(),
arr[2].ToObject<float>(),
arr.Count > 3 ? arr[3].ToObject<float>() : 1f
);
if (mat.GetColor(shaderProp) != c)
{
mat.SetColor(shaderProp, c);
modified = true;
}
}
catch (Exception ex)
{
Debug.LogWarning($"Error setting color '{shaderProp}': {ex.Message}");
}
}
continue;
}

// Float: single number
if (v.Type == JTokenType.Float || v.Type == JTokenType.Integer)
{
if (mat.HasProperty(shaderProp))
{
try
{
float f = v.ToObject<float>();
if (!Mathf.Approximately(mat.GetFloat(shaderProp), f))
{
mat.SetFloat(shaderProp, f);
modified = true;
}
}
catch (Exception ex)
{
Debug.LogWarning($"Error setting float '{shaderProp}': {ex.Message}");
}
}
continue;
}

// Texture: string path
if (v.Type == JTokenType.String)
{
string texPath = v.ToString();
if (!string.IsNullOrEmpty(texPath) && mat.HasProperty(shaderProp))
{
var tex = AssetDatabase.LoadAssetAtPath<Texture>(AssetPathUtility.SanitizeAssetPath(texPath));
if (tex != null && mat.GetTexture(shaderProp) != tex)
{
mat.SetTexture(shaderProp, tex);
modified = true;
}
}
continue;
}
}

// TODO: Add handlers for other property types (Vectors, Ints, Keywords, RenderQueue, etc.)
return modified;
}

Expand Down
14 changes: 13 additions & 1 deletion MCPForUnity/Editor/Tools/ManageGameObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,19 @@ public static object HandleCommand(JObject @params)
// --- End add parameter ---

// Coerce string JSON to JObject for 'componentProperties' if provided as a JSON string
JsonUtil.CoerceJsonStringParameter(@params, "componentProperties");
var componentPropsToken = @params["componentProperties"];
if (componentPropsToken != null && componentPropsToken.Type == JTokenType.String)
{
try
{
var parsed = JObject.Parse(componentPropsToken.ToString());
@params["componentProperties"] = parsed;
}
catch (Exception e)
{
Debug.LogWarning($"[ManageGameObject] Could not parse 'componentProperties' JSON string: {e.Message}");
}
}

// --- Prefab Redirection Check ---
string targetPath =
Expand Down
2 changes: 1 addition & 1 deletion MCPForUnity/UnityMcpServer~/src/tools/manage_asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ async def manage_asset(
try:
properties = json.loads(properties)
ctx.info("manage_asset: coerced properties from JSON string to dict")
except json.JSONDecodeError as e:
except Exception as e:
ctx.warn(f"manage_asset: failed to parse properties JSON string: {e}")
# Leave properties as-is; Unity side may handle defaults
# Ensure properties is a dict if None
Expand Down
4 changes: 3 additions & 1 deletion MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ def manage_gameobject(
"List of component names to remove"] | None = None,
component_properties: Annotated[dict[str, dict[str, Any]] | str,
"""Dictionary of component names to their properties to set. For example:
Can also be provided as a JSON string representation of the dict.
`{"MyScript": {"otherObject": {"find": "Player", "method": "by_name"}}}` assigns GameObject
`{"MyScript": {"playerHealth": {"find": "Player", "component": "HealthComponent"}}}` assigns Component
Example set nested property:
Expand Down Expand Up @@ -122,6 +121,9 @@ def _to_vec3(parts):
ctx.info("manage_gameobject: coerced component_properties from JSON string to dict")
except json.JSONDecodeError as e:
return {"success": False, "message": f"Invalid JSON in component_properties: {e}"}
# Ensure final type is a dict (object) if provided
if component_properties is not None and not isinstance(component_properties, dict):
return {"success": False, "message": "component_properties must be a JSON object (dict)."}
try:
# Map tag to search_term when search_method is by_tag for backward compatibility
if action == "find" and search_method == "by_tag" and tag is not None and search_term is None:
Expand Down
8 changes: 8 additions & 0 deletions TestProjects/UnityMCPTests/Assets/Temp/LiveTests.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,21 @@ namespace Tests.EditMode
/// </summary>
public class MCPToolParameterTests
{
private const string TempDir = "Assets/Temp/MCPToolParameterTests";

[SetUp]
public void SetUp()
[Test]
public void Test_ManageAsset_ShouldAcceptJSONProperties()
{
// Arrange: create temp folder
const string tempDir = "Assets/Temp/MCPToolParameterTests";
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
{
AssetDatabase.CreateFolder("Assets", "Temp");
}
if (!AssetDatabase.IsValidFolder(TempDir))
if (!AssetDatabase.IsValidFolder(tempDir))
{
AssetDatabase.CreateFolder("Assets/Temp", "MCPToolParameterTests");
}
}
[Test]
public void Test_ManageAsset_ShouldAcceptJSONProperties()
{
var matPath = $"{TempDir}/JsonMat_{Guid.NewGuid().ToString("N")}.mat";

var matPath = $"{tempDir}/JsonMat_{Guid.NewGuid().ToString("N")}.mat";

// Build params with properties as a JSON string (to be coerced)
var p = new JObject
Expand Down Expand Up @@ -73,7 +70,10 @@ public void Test_ManageAsset_ShouldAcceptJSONProperties()
[Test]
public void Test_ManageGameObject_ShouldAcceptJSONComponentProperties()
{
var matPath = $"{TempDir}/JsonMat_{Guid.NewGuid().ToString("N")}.mat";
const string tempDir = "Assets/Temp/MCPToolParameterTests";
if (!AssetDatabase.IsValidFolder("Assets/Temp")) AssetDatabase.CreateFolder("Assets", "Temp");
if (!AssetDatabase.IsValidFolder(tempDir)) AssetDatabase.CreateFolder("Assets/Temp", "MCPToolParameterTests");
var matPath = $"{tempDir}/JsonMat_{Guid.NewGuid().ToString("N")}.mat";

// Create a material first (object-typed properties)
var createMat = new JObject
Expand Down Expand Up @@ -121,7 +121,10 @@ public void Test_ManageGameObject_ShouldAcceptJSONComponentProperties()
[Test]
public void Test_JSONParsing_ShouldWorkInMCPTools()
{
var matPath = $"{TempDir}/JsonMat_{Guid.NewGuid().ToString("N")}.mat";
const string tempDir = "Assets/Temp/MCPToolParameterTests";
if (!AssetDatabase.IsValidFolder("Assets/Temp")) AssetDatabase.CreateFolder("Assets", "Temp");
if (!AssetDatabase.IsValidFolder(tempDir)) AssetDatabase.CreateFolder("Assets/Temp", "MCPToolParameterTests");
var matPath = $"{tempDir}/JsonMat_{Guid.NewGuid().ToString("N")}.mat";

// manage_asset with JSON string properties (coercion path)
var createMat = new JObject
Expand Down
Loading