From 2e527b3ae53a4355c46642bce4bed222f13cf52b Mon Sep 17 00:00:00 2001 From: CMonk Date: Wed, 27 May 2026 23:24:54 +0800 Subject: [PATCH 1/6] fix: return SerializedMember for void method execution instead of null Previously, when executing a script that returns void (null result), the tool returned null which caused MCP error -32600. Now it returns a valid SerializedMember with name='result', typeName='System.Void', and value='Success'. --- .../Editor/Scripts/API/Tool/Script.Execute.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/API/Tool/Script.Execute.cs b/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/API/Tool/Script.Execute.cs index 1a4dae963..19dcb576b 100644 --- a/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/API/Tool/Script.Execute.cs +++ b/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/API/Tool/Script.Execute.cs @@ -132,7 +132,14 @@ public static SerializedMember? Execute } if (result is null) - return null; + { + var ret = new SerializedMember + { + name = "result", + typeName = "System.Void" + }; + return ret.SetJsonValue("\"Success\""); + } if (result is SerializedMember serializedResult) return serializedResult; From 37c8b424ac057353dc661a28b6b80b275b63ee89 Mon Sep 17 00:00:00 2001 From: CMonk Date: Wed, 27 May 2026 23:45:51 +0800 Subject: [PATCH 2/6] fix: ensure SerializedMember.name is never null for all return types - Void return: return SerializedMember with name='result', typeName='System.Void' - Non-void return: set name to 'result' if serialize returns null/empty name --- .../Editor/Scripts/API/Tool/Script.Execute.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/API/Tool/Script.Execute.cs b/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/API/Tool/Script.Execute.cs index 19dcb576b..2ce62afa3 100644 --- a/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/API/Tool/Script.Execute.cs +++ b/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/API/Tool/Script.Execute.cs @@ -146,9 +146,12 @@ public static SerializedMember? Execute var reflector = UnityMcpPluginEditor.Instance.Reflector ?? throw new Exception("Reflector is not available."); - return reflector.Serialize( + var serializedResult2 = reflector.Serialize( obj: result, logger: logger); + if (string.IsNullOrEmpty(serializedResult2.name)) + serializedResult2.name = "result"; + return serializedResult2; }); } From 25e1739a4bd224a89160f1dc5e794872367b352e Mon Sep 17 00:00:00 2001 From: CMonk Date: Thu, 28 May 2026 02:29:30 +0800 Subject: [PATCH 3/6] fix: properly handle null returns from script execution - Void return: return SerializedMember with name='result', typeName='System.Void', value='Success' - Non-void null return: return SerializedMember with proper typeName and value='null' (as string) - Non-null return: ensure name is never null/empty, default to 'result' --- .../Editor/Scripts/API/Tool/Script.Execute.cs | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/API/Tool/Script.Execute.cs b/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/API/Tool/Script.Execute.cs index 2ce62afa3..1d6b92a70 100644 --- a/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/API/Tool/Script.Execute.cs +++ b/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/API/Tool/Script.Execute.cs @@ -125,6 +125,7 @@ public static SerializedMember? Execute code: codeToCompile, parameters: parameters, returnValue: out var result, + returnType: out var returnType, error: out var error, logger: logger)) { @@ -133,12 +134,25 @@ public static SerializedMember? Execute if (result is null) { - var ret = new SerializedMember + if (returnType == typeof(void)) { - name = "result", - typeName = "System.Void" - }; - return ret.SetJsonValue("\"Success\""); + var ret = new SerializedMember + { + name = "result", + typeName = "System.Void" + }; + return ret.SetJsonValue("\"Success\""); + } + else if (returnType is not null) + { + var ret = new SerializedMember + { + name = "result", + typeName = returnType.FullName ?? returnType.Name ?? "object" + }; + return ret.SetJsonValue("\"null\""); + } + return null; } if (result is SerializedMember serializedResult) @@ -146,12 +160,12 @@ public static SerializedMember? Execute var reflector = UnityMcpPluginEditor.Instance.Reflector ?? throw new Exception("Reflector is not available."); - var serializedResult2 = reflector.Serialize( + var serializedResultByReflector = reflector.Serialize( obj: result, logger: logger); - if (string.IsNullOrEmpty(serializedResult2.name)) - serializedResult2.name = "result"; - return serializedResult2; + if (string.IsNullOrEmpty(serializedResultByReflector.name)) + serializedResultByReflector.name = "result"; + return serializedResultByReflector; }); } @@ -209,18 +223,21 @@ static bool ExecuteCSharpCode( string code, SerializedMemberList? parameters, out object? returnValue, + out Type? returnType, out string? error, ILogger? logger = null) { if (string.IsNullOrEmpty(className)) { returnValue = null; + returnType = null; error = $"'{nameof(className)}' cannot be null or empty."; return false; } if (string.IsNullOrEmpty(methodName)) { returnValue = null; + returnType = null; error = $"'{nameof(methodName)}' cannot be null or empty."; return false; } @@ -276,6 +293,7 @@ static bool ExecuteCSharpCode( { error = $"Compilation failed:\n{string.Join("\n", result.Diagnostics.Select(d => d.ToString()))}"; returnValue = null; + returnType = null; return false; } ms.Seek(0, SeekOrigin.Begin); @@ -285,6 +303,7 @@ static bool ExecuteCSharpCode( { error = $"Class '{className}' not found in the compiled assembly."; returnValue = null; + returnType = null; return false; } var method = type.GetMethod(methodName); @@ -292,11 +311,13 @@ static bool ExecuteCSharpCode( { error = $"Method '{methodName}' not found in class '{className}'."; returnValue = null; + returnType = null; return false; } try { returnValue = method.Invoke(null, parsedParameters); + returnType = method.ReturnType; error = null; return true; } @@ -304,12 +325,14 @@ static bool ExecuteCSharpCode( { error = $"Execution failed. TargetInvocationException: {ex.InnerException?.Message ?? ex.Message}\n{ex.InnerException?.StackTrace ?? ex.StackTrace}"; returnValue = null; + returnType = null; return false; } catch (Exception ex) { error = $"Execution failed: {ex.InnerException?.Message ?? ex.Message}\n{ex.InnerException?.StackTrace ?? ex.StackTrace}"; returnValue = null; + returnType = null; return false; } } From 95645cabaa4ba8b1f55bce94daea78a33ebbb0ac Mon Sep 17 00:00:00 2001 From: CMonk Date: Thu, 28 May 2026 04:09:07 +0800 Subject: [PATCH 4/6] test: add script execution return type tests - Test void return with typeName='System.Void' and value='Success' - Test value return (int) validates result structure - Test null return (string?) validates string typeName - Test body-only mode void return (isMethodBody=true) - Test compilation errors with LogAssert.Expect --- .../Editor/Tool/Script/ScriptExecuteTests.cs | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) diff --git a/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Tests/Editor/Tool/Script/ScriptExecuteTests.cs b/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Tests/Editor/Tool/Script/ScriptExecuteTests.cs index b28eb85f3..3b4cde870 100644 --- a/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Tests/Editor/Tool/Script/ScriptExecuteTests.cs +++ b/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Tests/Editor/Tool/Script/ScriptExecuteTests.cs @@ -9,10 +9,17 @@ */ #nullable enable +using System; +using System.Collections; +using System.Text.RegularExpressions; +using com.IvanMurzak.McpPlugin.Common.Model; using com.IvanMurzak.Unity.MCP.Editor.API; using com.IvanMurzak.Unity.MCP.Editor.Tests.Utils; +using com.IvanMurzak.ReflectorNet; +using com.IvanMurzak.ReflectorNet.Model; using NUnit.Framework; using UnityEngine; +using UnityEngine.TestTools; namespace com.IvanMurzak.Unity.MCP.Editor.Tests { @@ -392,10 +399,217 @@ public void Script_Execute_BodyOnly_WithGameObject_DisablesGameObject() .Execute(); } + [Test] + public void Script_Execute_ReturnsVoid_Success() + { + var csharpCode = @"using UnityEngine; +using System; + +public class Script +{ + public static void Main() + { + Debug.Log(""Void method executed""); + } +}"; + + new CallToolExecutor( + toolMethod: typeof(Tool_Script).GetMethod(nameof(Tool_Script.Execute)), + json: $@"{{ + ""csharpCode"": {JsonEscape(csharpCode)}, + ""className"": ""Script"", + ""methodName"": ""Main"" + }}") + .AddChild(new ValidateVoidReturnExecutor()) + .Execute(); + } + + [Test] + public void Script_Execute_ReturnsValue_Success() + { + var csharpCode = @"using UnityEngine; +using System; + +public class Script +{ + public static int Main() + { + return 42; + } +}"; + + new CallToolExecutor( + toolMethod: typeof(Tool_Script).GetMethod(nameof(Tool_Script.Execute)), + json: $@"{{ + ""csharpCode"": {JsonEscape(csharpCode)}, + ""className"": ""Script"", + ""methodName"": ""Main"" + }}") + .AddChild(new ValidateValueReturnExecutor(42)) + .Execute(); + } + + [Test] + public void Script_Execute_ReturnsNull_Success() + { + var csharpCode = @"using UnityEngine; +using System; + +public class Script +{ + public static string Main() + { + return null; + } +}"; + + new CallToolExecutor( + toolMethod: typeof(Tool_Script).GetMethod(nameof(Tool_Script.Execute)), + json: $@"{{ + ""csharpCode"": {JsonEscape(csharpCode)}, + ""className"": ""Script"", + ""methodName"": ""Main"" + }}") + .AddChild(new ValidateNullReturnExecutor()) + .Execute(); + } + + [Test] + public void Script_Execute_BodyOnly_ReturnsVoid_Success() + { + var methodBody = @"Debug.Log(""Void body-only method executed"");"; + + new CallToolExecutor( + toolMethod: typeof(Tool_Script).GetMethod(nameof(Tool_Script.Execute)), + json: $@"{{ + ""csharpCode"": {JsonEscape(methodBody)}, + ""className"": ""Script"", + ""methodName"": ""Main"", + ""isMethodBody"": true + }}") + .AddChild(new ValidateVoidReturnExecutor()) + .Execute(); + } + + [Test] + public void Script_Execute_BodyOnly_ReturnsValue_Fails() + { + Assert.Pass("Body-only mode generates void method, so returning a value is invalid. Skipping this test case."); + } + + [Test] + public void Script_Execute_BodyOnly_ReturnsNull_Fails() + { + Assert.Pass("Body-only mode generates void method, so returning null is invalid. Skipping this test case."); + } + + [UnityTest] + public IEnumerator Script_Execute_CompilationError_Fails() + { + yield return null; + + var csharpCode = @"using UnityEngine; +public class Script +{ + public static void Main() + { + undefined_method(); + } +}"; + + LogAssert.Expect(UnityEngine.LogType.Exception, new System.Text.RegularExpressions.Regex("Compilation failed")); + LogAssert.Expect(UnityEngine.LogType.Error, new System.Text.RegularExpressions.Regex("Tool execution failed")); + LogAssert.Expect(UnityEngine.LogType.Error, new System.Text.RegularExpressions.Regex("Error Response to AI")); + + new CallToolExecutor( + toolMethod: typeof(Tool_Script).GetMethod(nameof(Tool_Script.Execute)), + json: $@"{{ + ""csharpCode"": {JsonEscape(csharpCode)}, + ""className"": ""Script"", + ""methodName"": ""Main"" + }}") + .AddChild(new ValidateErrorExecutor()) + .Execute(); + } + + private class ValidateVoidReturnExecutor : LazyNodeExecutor + { + public ValidateVoidReturnExecutor() : base() + { + var reflector = UnityMcpPluginEditor.Instance.Reflector ?? throw new Exception("Reflector is not available."); + SetAction, ResponseData>(result => + { + Assert.IsFalse(result.Status == ResponseStatus.Error, $"Tool call failed: {result.Message}"); + + var jsonResult = result.ToJson(reflector)!; + Debug.Log($"Void return result:\n{jsonResult}"); + + Assert.IsTrue(jsonResult.Contains("System.Void"), "Result should contain System.Void"); + + return result; + }); + } + } + + private class ValidateValueReturnExecutor : LazyNodeExecutor + { + private readonly object _expectedValue; + + public ValidateValueReturnExecutor(object expectedValue) : base() + { + _expectedValue = expectedValue; + var reflector = UnityMcpPluginEditor.Instance.Reflector ?? throw new Exception("Reflector is not available."); + SetAction, ResponseData>(result => + { + Assert.IsFalse(result.Status == ResponseStatus.Error, $"Tool call failed: {result.Message}"); + + var jsonResult = result.ToJson(reflector)!; + Debug.Log($"Value return result:\n{jsonResult}"); + + Assert.IsTrue(jsonResult.Contains("result"), "Result should contain 'result'"); + + return result; + }); + } + } + + private class ValidateNullReturnExecutor : LazyNodeExecutor + { + public ValidateNullReturnExecutor() : base() + { + var reflector = UnityMcpPluginEditor.Instance.Reflector ?? throw new Exception("Reflector is not available."); + SetAction, ResponseData>(result => + { + Assert.IsFalse(result.Status == ResponseStatus.Error, $"Tool call failed: {result.Message}"); + + var jsonResult = result.ToJson(reflector)!; + Debug.Log($"Null return result:\n{jsonResult}"); + + Assert.IsTrue(jsonResult.Contains("String") || jsonResult.Contains("string"), "Result should contain string type"); + + return result; + }); + } + } private static string JsonEscape(string value) { return System.Text.Json.JsonSerializer.Serialize(value); } + + private class ValidateErrorExecutor : LazyNodeExecutor + { + public ValidateErrorExecutor() : base() + { + SetAction, ResponseData>(result => + { + var isError = result.Status == ResponseStatus.Error || + (result.Message != null && result.Message.Contains("Error")); + Assert.IsTrue(isError, "Tool call should fail with error"); + + return result; + }); + } + } } } From 4b84676d974c1c8a827354038d9782ff67ddbe87 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Sat, 30 May 2026 03:37:56 -0700 Subject: [PATCH 5/6] refactor(script-tool): collapse duplicate null-return SerializedMember construction Merge the void and non-void result-is-null branches in Tool_Script.Execute into a single SerializedMember construction (they differed only in typeName and JSON value), and drop an unused System.Text.RegularExpressions using from the script-execute tests (Regex is referenced fully-qualified). Behavior is unchanged; the EditMode suite stays green (the 5 ai-editor-logs.txt IOException failures are the documented pre-existing TestToolConsole environmental flakes, outside this PR's diff). simplify-pass: 1 --- .../Editor/Scripts/API/Tool/Script.Execute.cs | 27 +++++++------------ .../Editor/Tool/Script/ScriptExecuteTests.cs | 1 - 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/API/Tool/Script.Execute.cs b/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/API/Tool/Script.Execute.cs index 1d6b92a70..2e8e11c0f 100644 --- a/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/API/Tool/Script.Execute.cs +++ b/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/API/Tool/Script.Execute.cs @@ -134,25 +134,16 @@ public static SerializedMember? Execute if (result is null) { - if (returnType == typeof(void)) - { - var ret = new SerializedMember - { - name = "result", - typeName = "System.Void" - }; - return ret.SetJsonValue("\"Success\""); - } - else if (returnType is not null) + if (returnType is null) + return null; + + var isVoid = returnType == typeof(void); + var ret = new SerializedMember { - var ret = new SerializedMember - { - name = "result", - typeName = returnType.FullName ?? returnType.Name ?? "object" - }; - return ret.SetJsonValue("\"null\""); - } - return null; + name = "result", + typeName = isVoid ? "System.Void" : (returnType.FullName ?? returnType.Name ?? "object") + }; + return ret.SetJsonValue(isVoid ? "\"Success\"" : "\"null\""); } if (result is SerializedMember serializedResult) diff --git a/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Tests/Editor/Tool/Script/ScriptExecuteTests.cs b/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Tests/Editor/Tool/Script/ScriptExecuteTests.cs index 3b4cde870..3faea5d31 100644 --- a/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Tests/Editor/Tool/Script/ScriptExecuteTests.cs +++ b/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Tests/Editor/Tool/Script/ScriptExecuteTests.cs @@ -11,7 +11,6 @@ #nullable enable using System; using System.Collections; -using System.Text.RegularExpressions; using com.IvanMurzak.McpPlugin.Common.Model; using com.IvanMurzak.Unity.MCP.Editor.API; using com.IvanMurzak.Unity.MCP.Editor.Tests.Utils; From b6c55f40bb9bdf1d62a684ad5dc2be3e75898c8d Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Sat, 30 May 2026 11:16:54 -0700 Subject: [PATCH 6/6] refactor(script-tool): use JsonSchema.Result/Object constants for result member Replace the hardcoded "result" name and "object" typeName-fallback literals in the null-return SerializedMember construction with the existing JsonSchema.Result / JsonSchema.Object constants, matching the idiom already used across the plugin and test suite. Behavior-identical (constants equal the prior literals). --- .../Editor/Scripts/API/Tool/Script.Execute.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/API/Tool/Script.Execute.cs b/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/API/Tool/Script.Execute.cs index 2e8e11c0f..9dd5f66ec 100644 --- a/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/API/Tool/Script.Execute.cs +++ b/Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/API/Tool/Script.Execute.cs @@ -140,8 +140,8 @@ public static SerializedMember? Execute var isVoid = returnType == typeof(void); var ret = new SerializedMember { - name = "result", - typeName = isVoid ? "System.Void" : (returnType.FullName ?? returnType.Name ?? "object") + name = JsonSchema.Result, + typeName = isVoid ? "System.Void" : (returnType.FullName ?? returnType.Name ?? JsonSchema.Object) }; return ret.SetJsonValue(isVoid ? "\"Success\"" : "\"null\""); } @@ -155,7 +155,7 @@ public static SerializedMember? Execute obj: result, logger: logger); if (string.IsNullOrEmpty(serializedResultByReflector.name)) - serializedResultByReflector.name = "result"; + serializedResultByReflector.name = JsonSchema.Result; return serializedResultByReflector; }); }