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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Editor/MCPBridgeServer.Routes.g.cs text eol=lf
33 changes: 33 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Checks

on:
push:
branches: ["**"]
paths:
- "Editor/**"
- "tools~/**"
- ".github/workflows/checks.yml"
pull_request:
paths:
- "Editor/**"
- "tools~/**"
- ".github/workflows/checks.yml"

concurrency:
group: checks-${{ github.ref }}
cancel-in-progress: true

jobs:
route-registry:
name: route registry drift check
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
- name: Verify MCPBridgeServer.Routes.g.cs matches the dispatch switch
run: node tools~/generate-routes.mjs --check
121 changes: 121 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

13 changes: 11 additions & 2 deletions Editor/AnkleBreaker.UnityMCP.Editor.asmdef
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
"UnityEngine.TestRunner",
"UnityEditor.TestRunner",
"UMA_Core",
"UMA_Core_Editor"
"UMA_Core_Editor",
"Unity.ProBuilder",
"Unity.ProBuilder.Editor",
"Unity.ProBuilder.Csg"
],
"includePlatforms": [
"Editor"
Expand All @@ -16,6 +19,12 @@
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"versionDefines": [
{
"name": "com.unity.probuilder",
"expression": "4.0.0",
"define": "PROBUILDER_INSTALLED"
}
],
"noEngineReferences": false
}
4 changes: 2 additions & 2 deletions Editor/MCPActionHistory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ private static void SaveToDisk()
status = r.Status ?? "",
executionTimeMs = r.ExecutionTimeMs,
errorMessage = r.ErrorMessage ?? "",
targetInstanceId = r.TargetInstanceId,
targetInstanceId = r.TargetInstanceId ?? "",
targetPath = r.TargetPath ?? "",
targetType = r.TargetType ?? "",
undoGroup = r.UndoGroup,
Expand Down Expand Up @@ -289,7 +289,7 @@ private class HistoryEntry
public string status;
public long executionTimeMs;
public string errorMessage;
public int targetInstanceId;
public string targetInstanceId; // string since 64-bit EntityId support. JsonUtility tolerates the old int→string scalar mismatch (parses to ""); LoadFromDisk is try/catch-guarded so a hard failure would drop the whole history file, not one field.
public string targetPath;
public string targetType;
public int undoGroup;
Expand Down
8 changes: 4 additions & 4 deletions Editor/MCPActionHistoryWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ private void DrawDetailPanel()
}

// Select button
if (r.TargetInstanceId != 0)
if (!string.IsNullOrEmpty(r.TargetInstanceId))
{
if (GUILayout.Button("Select", EditorStyles.miniButton, GUILayout.Width(45)))
SelectTargetObject(r);
Expand All @@ -453,8 +453,8 @@ private void DrawDetailPanel()
DrawDetailRow("Target", r.TargetPath);
if (!string.IsNullOrEmpty(r.TargetType))
DrawDetailRow("Target Type", r.TargetType);
if (r.TargetInstanceId != 0)
DrawDetailRow("Instance ID", r.TargetInstanceId.ToString());
if (!string.IsNullOrEmpty(r.TargetInstanceId))
DrawDetailRow("Instance ID", r.TargetInstanceId);
if (r.UndoGroup >= 0)
DrawDetailRow("Undo Group", r.UndoGroup.ToString());

Expand Down Expand Up @@ -508,7 +508,7 @@ private void DrawDetailRow(string label, string value, Color? valueColor = null)

private void SelectTargetObject(MCPActionRecord record)
{
if (record.TargetInstanceId != 0)
if (!string.IsNullOrEmpty(record.TargetInstanceId))
{
var obj = MCPObjectId.ToObject(record.TargetInstanceId);
if (obj != null)
Expand Down
24 changes: 12 additions & 12 deletions Editor/MCPActionRecord.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ public class MCPActionRecord
public long ExecutionTimeMs { get; set; }
public string ErrorMessage { get; set; }

// Target object tracking
public int TargetInstanceId { get; set; } // 0 = no target
// Target object tracking. String because Unity 6.5 EntityIds are 64-bit
// values carried as opaque decimal strings on the wire (see MCPObjectId) —
// an int here silently truncated them. null/empty = no target. Old int-typed
// persisted entries: JsonUtility parses the scalar-type mismatch to "".
public string TargetInstanceId { get; set; }
public string TargetPath { get; set; }
public string TargetType { get; set; } // GameObject, Component, Asset, Script, Scene, etc.

Expand Down Expand Up @@ -62,15 +65,12 @@ public void ExtractTargetFromResult(object result)
{
if (!(result is Dictionary<string, object> dict)) return;

// Instance ID
if (dict.TryGetValue("instanceId", out var idObj))
// Instance ID — stored verbatim as a string (lossless for 64-bit EntityIds)
if (dict.TryGetValue("instanceId", out var idObj) && idObj != null)
{
if (idObj is int intId)
TargetInstanceId = intId;
else if (idObj is long longId)
TargetInstanceId = (int)longId;
else if (int.TryParse(idObj?.ToString(), out int parsed))
TargetInstanceId = parsed;
string id = idObj.ToString();
if (!string.IsNullOrEmpty(id))
TargetInstanceId = id;
}

// Path
Expand Down Expand Up @@ -124,7 +124,7 @@ public string ToCopyString()

if (!string.IsNullOrEmpty(TargetPath))
sb.AppendLine($"Target: {TargetPath}");
if (TargetInstanceId != 0)
if (!string.IsNullOrEmpty(TargetInstanceId))
sb.AppendLine($"InstanceId: {TargetInstanceId}");
if (!string.IsNullOrEmpty(ErrorMessage))
sb.AppendLine($"Error: {ErrorMessage}");
Expand Down Expand Up @@ -154,7 +154,7 @@ public Dictionary<string, object> ToDict()
{ "status", Status ?? "" },
{ "executionTimeMs", ExecutionTimeMs },
{ "errorMessage", ErrorMessage ?? "" },
{ "targetInstanceId", TargetInstanceId },
{ "targetInstanceId", TargetInstanceId ?? "" },
{ "targetPath", TargetPath ?? "" },
{ "targetType", TargetType ?? "" },
{ "undoGroup", UndoGroup },
Expand Down
10 changes: 10 additions & 0 deletions Editor/MCPAnimationCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ public static object CreateController(Dictionary<string, object> args)
}
}

// Don't silently replace an existing controller (all states/transitions lost).
var controllerOverwrite = MCPAssetSafety.OverwriteGuard(path, args);
if (controllerOverwrite != null)
return controllerOverwrite;

var controller = AnimatorController.CreateAnimatorControllerAtPath(path);
if (controller == null)
return new { error = "Failed to create animator controller" };
Expand Down Expand Up @@ -387,6 +392,11 @@ public static object CreateClip(Dictionary<string, object> args)
}
}

// Don't silently wipe an existing clip's curves.
var clipOverwrite = MCPAssetSafety.OverwriteGuard(path, args);
if (clipOverwrite != null)
return clipOverwrite;

var clip = new AnimationClip();
clip.name = Path.GetFileNameWithoutExtension(path);

Expand Down
85 changes: 85 additions & 0 deletions Editor/MCPArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Globalization;

namespace UnityMCP.Editor
{
/// <summary>
/// Typed-first argument readers for command handlers.
///
/// MiniJson delivers JSON numbers as boxed <c>double</c> / <c>long</c>. The old
/// pattern <c>value.ToString()</c> + invariant TryParse silently DROPPED every
/// non-integer number on machines whose current culture writes a decimal comma
/// (18.8 → "18,8" → parse fail → default) — the exact "floats silently ignored"
/// bug from the battle-test report. Readers here unbox the typed value directly
/// and only fall back to invariant string parsing for string inputs.
///
/// Design rule (also from the report): a parameter that is PRESENT but not
/// interpretable throws instead of silently falling back to the default —
/// a loud failure costs seconds, a silent wrong default costs a debugging session.
/// </summary>
internal static class MCPArgs
{
public static float GetFloat(Dictionary<string, object> args, string key, float defaultValue)
{
if (args == null || !args.TryGetValue(key, out object value) || value == null)
return defaultValue;

switch (value)
{
case double d: return (float)d;
case float f: return f;
case long l: return l;
case int i: return i;
case string s:
if (float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsed))
return parsed;
throw new ArgumentException($"Parameter '{key}' is not a valid number: '{s}'.");
default:
throw new ArgumentException($"Parameter '{key}' must be a number (got {value.GetType().Name}).");
}
}

public static int GetInt(Dictionary<string, object> args, string key, int defaultValue)
{
if (args == null || !args.TryGetValue(key, out object value) || value == null)
return defaultValue;

switch (value)
{
case long l: return checked((int)l);
case int i: return i;
case double d:
// Accept integral doubles (8.0) — reject genuinely fractional values loudly.
double rounded = Math.Round(d);
if (Math.Abs(d - rounded) < 1e-6) return checked((int)rounded);
throw new ArgumentException($"Parameter '{key}' must be an integer (got {d.ToString(CultureInfo.InvariantCulture)}).");
case string s:
if (int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed))
return parsed;
throw new ArgumentException($"Parameter '{key}' is not a valid integer: '{s}'.");
default:
throw new ArgumentException($"Parameter '{key}' must be an integer (got {value.GetType().Name}).");
}
}

public static bool GetBool(Dictionary<string, object> args, string key, bool defaultValue)
{
if (args == null || !args.TryGetValue(key, out object value) || value == null)
return defaultValue;

switch (value)
{
case bool b: return b;
case string s:
string lower = s.ToLowerInvariant();
if (lower == "true" || lower == "1") return true;
if (lower == "false" || lower == "0") return false;
throw new ArgumentException($"Parameter '{key}' is not a valid boolean: '{s}'.");
case long l: return l != 0;
default:
throw new ArgumentException($"Parameter '{key}' must be a boolean (got {value.GetType().Name}).");
}
}
}
}
11 changes: 11 additions & 0 deletions Editor/MCPArgs.cs.meta

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

Loading