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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@

All notable changes to this package will be documented in this file.

## [2.39.5] - 2026-07-27

Community-reported fixes. Each claim was verified against the shipped Unity assemblies before being acted on — two held up, one did not (documented below).

### Fixed (ShaderGraph — community PR #23 by @mrooney)
- **`get_edges` reported every edge with blank ids.** The `m_OutputSlot`/`m_InputSlot` patterns used `.*?` **without** `RegexOptions.Singleline`, so `.` could not cross the newline between the key and the nested `m_Id` — and a `.shadergraph` writes one field per line. Every id came back `""` and every slot `0`, while the on-disk edges were perfectly correct (read-only path, nothing was ever corrupted). Verified live: `connect` → `get_edges` now returns the real objectId and slot.
- **Property nodes were still added unbound (issue #18 bug 3).** The `GraphData` rewrite fixed the other three bugs but added Property nodes type-only, so they serialized with an empty `m_Property` and no slots and the next import threw in `PropertyNode.AddOutputSlot`, failing the whole asset. `add_node` now accepts `propertyId` (alias `property`) and binds through `PropertyNode.property`, which rebuilds the output slot from the property's concrete type. An unbound or unknown Property node is now **refused before anything is written** — verified live, target graph intact.
- **The version-mismatch guard now names names.** It reported "One or more ShaderGraph API methods not found", which made any report against this path unfalsifiable in both directions. It now lists exactly which members are missing and asks for the Unity + ShaderGraph versions.
- **`AddNode` accepts either arity.** PR #23's premise was that ShaderGraph 17.3.0 only exposes `AddNode(node, bool)`, disabling the whole suite on Unity 6.3. Reflecting the shipped `Unity.ShaderGraph.Editor` on 17.3.0 shows the opposite — only the 1-arg `AddNode(AbstractMaterialNode)` exists — so we could not reproduce it. Accepting both shapes costs nothing and removes the question. Property-binding members are resolved **optionally**, so a build lacking `PropertyNode` can never disable the entire ShaderGraph surface (the all-or-nothing brittleness the PR would otherwise have introduced).

### Fixed (community PR #19 by @JetNik)
- **`execute_code` still failed on macOS.** Unity 6000.3+ re-roots the scripting assemblies under `Contents/Resources/Scripting`, and probing only that bare folder missed the nested `MonoBleedingEdge/…` and `DotNetSdkRoslyn` layout inside it. Each Roslyn subpath is now mirrored there. Inert off macOS — the existing `Directory.Exists` guard skips absent paths — and `execute_code` verified still resolving Roslyn on Windows.

### Added (issue #30 by @VM233 — 2D sprite animation)
- **`animation/set-object-reference-curve`** — object-reference (PPtr) curves, the type Unity uses for `SpriteRenderer.m_Sprite`. `clip.SetCurve`/`AnimationCurve` can only express **float** curves, so sprite-frame animation had no route through the MCP at all and had to be hand-written as `.anim` YAML (version-fragile, and it produced `curve type is invalid` import errors). Routes through `AnimationUtility.SetObjectReferenceCurve` + `EditorCurveBinding.PPtrCurve` so Unity writes the binding itself. Keyframes take an asset path plus an optional sub-asset `name` to select one sprite from a sliced sheet, and the call **fails closed** if any keyframe is unresolvable rather than leaving a clip animating to the wrong frames.
- **`animation/clip-info` now reports object-reference curves.** `GetCurveBindings` returns only float bindings, so a correct sprite clip reported `curveCount: 0` and looked empty — the reporter had no way to verify their own work. Adds `objectReferenceCurves` with binding path, property, type, and each keyframe's time + resolved asset path.
- Verified end to end: a 3-frame sprite clip built through the real MCP tool, read back correctly, and re-imported by Unity with **0 console errors** and all references resolved. Route registry 337 → 338.

## [2.39.4] - 2026-07-24

Findings from a 33-dimension + 10-blind-spot audit (127 + 40 agents, every CRITICAL/HIGH adversarially verified). The recurring pattern: a correct pattern existed but was never propagated, and guards failed OPEN.
Expand Down
123 changes: 123 additions & 0 deletions Editor/MCPAnimationCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,31 @@ public static object GetClipInfo(Dictionary<string, object> args)
});
}

// Object-reference (PPtr) curves are a SEPARATE binding set — GetCurveBindings only
// returns float curves, so a sprite-frame animation previously reported curveCount:0
// and looked empty through the MCP even when the clip was correct (issue #30).
var objectCurves = new List<Dictionary<string, object>>();
foreach (var binding in AnimationUtility.GetObjectReferenceCurveBindings(clip))
{
var frames = AnimationUtility.GetObjectReferenceCurve(clip, binding);
var keys = new List<Dictionary<string, object>>();
foreach (var f in frames)
keys.Add(new Dictionary<string, object>
{
{ "time", f.time },
{ "value", f.value != null ? f.value.name : null },
{ "assetPath", f.value != null ? AssetDatabase.GetAssetPath(f.value) : null },
});
objectCurves.Add(new Dictionary<string, object>
{
{ "path", binding.path },
{ "propertyName", binding.propertyName },
{ "type", binding.type.Name },
{ "keyframeCount", frames.Length },
{ "keyframes", keys },
});
}

var settings = AnimationUtility.GetAnimationClipSettings(clip);

return new Dictionary<string, object>
Expand All @@ -457,11 +482,109 @@ public static object GetClipInfo(Dictionary<string, object> args)
{ "wrapMode", clip.wrapMode.ToString() },
{ "curveCount", curves.Count },
{ "curves", curves },
{ "objectReferenceCurveCount", objectCurves.Count },
{ "objectReferenceCurves", objectCurves },
{ "events", clip.events.Length },
{ "isHumanMotion", clip.humanMotion },
};
}

/// <summary>
/// Set an object-reference (PPtr) curve — the curve type Unity uses for sprite-frame
/// animation (SpriteRenderer.m_Sprite) and any other UnityEngine.Object-valued property.
/// clip.SetCurve/AnimationCurve can only express FLOAT curves, so this whole class of
/// clip was previously impossible through the MCP and had to be hand-written as .anim
/// YAML — which is version-fragile and produced "curve type is invalid" import errors
/// (issue #30). Routes through AnimationUtility.SetObjectReferenceCurve so Unity itself
/// writes the binding.
/// </summary>
public static object SetObjectReferenceCurve(Dictionary<string, object> args)
{
string path = args.ContainsKey("clipPath") ? args["clipPath"].ToString() : "";
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(path);
if (clip == null)
return new { error = $"Animation clip not found at '{path}'" };

string relativePath = args.ContainsKey("relativePath") ? args["relativePath"].ToString() : "";
string propertyName = args.ContainsKey("propertyName") ? args["propertyName"].ToString() : "m_Sprite";
string typeName = args.ContainsKey("type") ? args["type"].ToString() : "SpriteRenderer";

var type = ResolveComponentType(typeName);
if (type == null)
return new { error = $"Component type '{typeName}' not found. Use a UnityEngine type name such as 'SpriteRenderer' or 'MeshRenderer'." };

if (!args.ContainsKey("keyframes") || !(args["keyframes"] is List<object> kfList) || kfList.Count == 0)
return new { error = "keyframes is required: an array of { time, assetPath } (assetPath may target a sub-asset, e.g. a sliced sprite)." };

var frames = new List<ObjectReferenceKeyframe>();
var unresolved = new List<string>();
foreach (var kfObj in kfList)
{
if (!(kfObj is Dictionary<string, object> kf)) continue;
float time = kf.ContainsKey("time") ? Convert.ToSingle(kf["time"]) : 0f;
string assetPath = kf.ContainsKey("assetPath") && kf["assetPath"] != null ? kf["assetPath"].ToString() : null;
string subName = kf.ContainsKey("name") && kf["name"] != null ? kf["name"].ToString() : null;
if (string.IsNullOrEmpty(assetPath)) { unresolved.Add($"t={time}: missing assetPath"); continue; }

// A sliced sprite sheet holds many Sprite sub-assets under ONE path, so load them
// all and pick by name when the caller names one; otherwise take the first.
UnityEngine.Object value = null;
foreach (var sub in AssetDatabase.LoadAllAssetsAtPath(assetPath))
{
if (sub == null || !typeof(Sprite).IsAssignableFrom(sub.GetType())) continue;
if (subName == null) { value = sub; break; }
if (sub.name == subName) { value = sub; break; }
}
if (value == null) value = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath);
if (value == null) { unresolved.Add($"t={time}: '{assetPath}'" + (subName != null ? $" (sprite '{subName}')" : "")); continue; }

frames.Add(new ObjectReferenceKeyframe { time = time, value = value });
}

// Fail closed: a partially-resolved curve would silently animate to the wrong frames.
if (unresolved.Count > 0)
return new
{
error = $"{unresolved.Count} keyframe(s) could not be resolved: {string.Join("; ", unresolved)}",
unresolvedKeyframes = unresolved,
};

var binding = EditorCurveBinding.PPtrCurve(relativePath, type, propertyName);
Undo.RecordObject(clip, "Set Object Reference Curve");
AnimationUtility.SetObjectReferenceCurve(clip, binding, frames.ToArray());
EditorUtility.SetDirty(clip);
AssetDatabase.SaveAssets();

return new Dictionary<string, object>
{
{ "success", true },
{ "clipPath", path },
{ "relativePath", relativePath },
{ "propertyName", propertyName },
{ "type", type.Name },
{ "keyframeCount", frames.Count },
{ "clipLength", clip.length },
};
}

/// <summary>Resolve a UnityEngine component type by short name across the split modules.</summary>
private static Type ResolveComponentType(string typeName)
{
var direct = Type.GetType($"UnityEngine.{typeName}, UnityEngine")
?? Type.GetType($"UnityEngine.{typeName}, UnityEngine.CoreModule")
?? Type.GetType(typeName);
if (direct != null) return direct;
// UnityEngine is split into modules (SpriteRenderer lives in UnityEngine.SpriteShapeModule
// / CoreModule depending on version) — scan rather than hardcode the module list.
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
if (!asm.GetName().Name.StartsWith("UnityEngine")) continue;
var t = asm.GetType($"UnityEngine.{typeName}");
if (t != null) return t;
}
return null;
}

public static object SetClipCurve(Dictionary<string, object> args)
{
string path = args.ContainsKey("clipPath") ? args["clipPath"].ToString() : "";
Expand Down
3 changes: 2 additions & 1 deletion Editor/MCPBridgeServer.Routes.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace UnityMCP.Editor
{
public static partial class MCPBridgeServer
{
/// <summary>Every route the bridge can dispatch (337 routes).</summary>
/// <summary>Every route the bridge can dispatch (338 routes).</summary>
internal static readonly string[] GeneratedRoutes = new string[]
{
"_meta/routes",
Expand Down Expand Up @@ -62,6 +62,7 @@ public static partial class MCPBridgeServer
"animation/remove-transition",
"animation/set-clip-curve",
"animation/set-clip-settings",
"animation/set-object-reference-curve",
"asmdef/add-references",
"asmdef/create",
"asmdef/create-ref",
Expand Down
2 changes: 2 additions & 0 deletions Editor/MCPBridgeServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,8 @@ private static object RouteRequest(string path, string method, string body)
return MCPAnimationCommands.GetClipInfo(ParseJson(body));
case "animation/set-clip-curve":
return MCPAnimationCommands.SetClipCurve(ParseJson(body));
case "animation/set-object-reference-curve":
return MCPAnimationCommands.SetObjectReferenceCurve(ParseJson(body));
case "animation/add-layer":
return MCPAnimationCommands.AddLayer(ParseJson(body));
case "animation/assign-controller":
Expand Down
9 changes: 8 additions & 1 deletion Editor/MCPEditorCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,17 @@ private static bool TryLoadRoslyn()
// ScriptUpdater also ships Roslyn (Mono-compatible)
searchDirs.Add(Path.Combine(data, "Tools", "ScriptUpdater"));
// macOS: recent editors ship Roslyn under Contents/Resources/Scripting
// (community PR #19 by JetNik — ExecuteCode was broken on macOS without it)
// (community PR #19 by JetNik — ExecuteCode was broken on macOS without it).
// The bare folder alone isn't enough: on Unity 6000.3+ the assemblies live in
// the SAME nested layout as on Windows, just re-rooted under Resources/Scripting,
// so mirror each Mono/Roslyn subpath there too. Non-existent directories are
// skipped by the Directory.Exists guard below, so this is inert off macOS.
searchDirs.Add(Path.Combine(data, "Resources", "Scripting"));
searchDirs.Add(Path.Combine(data, "Resources", "Scripting", "MonoBleedingEdge", "lib", "mono", "4.5"));
searchDirs.Add(Path.Combine(data, "Resources", "Scripting", "MonoBleedingEdge", "lib", "mono", "msbuild", "Current", "bin", "Roslyn"));
// DotNetSdkRoslyn contains .NET Core assemblies — may fail on Mono, tried last
searchDirs.Add(Path.Combine(data, "DotNetSdkRoslyn"));
searchDirs.Add(Path.Combine(data, "Resources", "Scripting", "DotNetSdkRoslyn"));
}
if (!string.IsNullOrEmpty(editorDir))
searchDirs.Add(editorDir);
Expand Down
Loading