diff --git a/MCPForUnity/Editor/Tools/BatchExecute.cs b/MCPForUnity/Editor/Tools/BatchExecute.cs index 655d196a..fa46dd31 100644 --- a/MCPForUnity/Editor/Tools/BatchExecute.cs +++ b/MCPForUnity/Editor/Tools/BatchExecute.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text; using System.Threading.Tasks; using MCPForUnity.Editor.Helpers; using Newtonsoft.Json.Linq; @@ -43,18 +44,20 @@ public static async Task HandleCommand(JObject @params) } var commandResults = new List(commandsToken.Count); - int successCount = 0; - int failureCount = 0; + int invocationSuccessCount = 0; + int invocationFailureCount = 0; + bool anyCommandFailed = false; foreach (var token in commandsToken) { if (token is not JObject commandObj) { - failureCount++; + invocationFailureCount++; + anyCommandFailed = true; commandResults.Add(new { - success = false, tool = (string)null, + callSucceeded = false, error = "Command entries must be JSON objects." }); if (failFast) @@ -65,15 +68,17 @@ public static async Task HandleCommand(JObject @params) } string toolName = commandObj["tool"]?.ToString(); - var commandParams = commandObj["params"] as JObject ?? new JObject(); + var rawParams = commandObj["params"] as JObject ?? new JObject(); + var commandParams = NormalizeParameterKeys(rawParams); if (string.IsNullOrWhiteSpace(toolName)) { - failureCount++; + invocationFailureCount++; + anyCommandFailed = true; commandResults.Add(new { - success = false, tool = toolName, + callSucceeded = false, error = "Each command must include a non-empty 'tool' field." }); if (failFast) @@ -86,21 +91,23 @@ public static async Task HandleCommand(JObject @params) try { var result = await CommandRegistry.InvokeCommandAsync(toolName, commandParams).ConfigureAwait(true); - successCount++; + invocationSuccessCount++; + commandResults.Add(new { - success = true, tool = toolName, + callSucceeded = true, result }); } catch (Exception ex) { - failureCount++; + invocationFailureCount++; + anyCommandFailed = true; commandResults.Add(new { - success = false, tool = toolName, + callSucceeded = false, error = ex.Message }); @@ -111,12 +118,12 @@ public static async Task HandleCommand(JObject @params) } } - bool overallSuccess = failureCount == 0; + bool overallSuccess = !anyCommandFailed; var data = new { results = commandResults, - successCount, - failureCount, + callSuccessCount = invocationSuccessCount, + callFailureCount = invocationFailureCount, parallelRequested, parallelApplied = false, maxParallelism = maxParallel @@ -126,5 +133,73 @@ public static async Task HandleCommand(JObject @params) ? new SuccessResponse("Batch execution completed.", data) : new ErrorResponse("One or more commands failed.", data); } + + private static JObject NormalizeParameterKeys(JObject source) + { + if (source == null) + { + return new JObject(); + } + + var normalized = new JObject(); + foreach (var property in source.Properties()) + { + string normalizedName = ToCamelCase(property.Name); + normalized[normalizedName] = NormalizeToken(property.Value); + } + return normalized; + } + + private static JArray NormalizeArray(JArray source) + { + var normalized = new JArray(); + foreach (var token in source) + { + normalized.Add(NormalizeToken(token)); + } + return normalized; + } + + private static JToken NormalizeToken(JToken token) + { + return token switch + { + JObject obj => NormalizeParameterKeys(obj), + JArray arr => NormalizeArray(arr), + _ => token.DeepClone() + }; + } + + private static string ToCamelCase(string key) + { + if (string.IsNullOrEmpty(key) || key.IndexOf('_') < 0) + { + return key; + } + + var parts = key.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0) + { + return key; + } + + var builder = new StringBuilder(parts[0]); + for (int i = 1; i < parts.Length; i++) + { + var part = parts[i]; + if (string.IsNullOrEmpty(part)) + { + continue; + } + + builder.Append(char.ToUpperInvariant(part[0])); + if (part.Length > 1) + { + builder.Append(part.AsSpan(1)); + } + } + + return builder.ToString(); + } } } diff --git a/MCPForUnity/Editor/Tools/BatchExecute.cs.meta b/MCPForUnity/Editor/Tools/BatchExecute.cs.meta new file mode 100644 index 00000000..491cc79a --- /dev/null +++ b/MCPForUnity/Editor/Tools/BatchExecute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4e1e2d8f3a454a37b18d06a7a7b6c3fb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Tools/ManageAsset.cs b/MCPForUnity/Editor/Tools/ManageAsset.cs index 36480a24..aa771c01 100644 --- a/MCPForUnity/Editor/Tools/ManageAsset.cs +++ b/MCPForUnity/Editor/Tools/ManageAsset.cs @@ -163,7 +163,9 @@ private static object ReimportAsset(string path, JObject properties) private static object CreateAsset(JObject @params) { string path = @params["path"]?.ToString(); - string assetType = @params["assetType"]?.ToString(); + string assetType = + @params["assetType"]?.ToString() + ?? @params["asset_type"]?.ToString(); // tolerate snake_case payloads from batched commands JObject properties = @params["properties"] as JObject; if (string.IsNullOrEmpty(path)) diff --git a/Server/uv.lock b/Server/uv.lock index 26152be7..44a0ef88 100644 --- a/Server/uv.lock +++ b/Server/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.10" [[package]] @@ -694,7 +694,7 @@ wheels = [ [[package]] name = "mcpforunityserver" -version = "7.0.0" +version = "8.1.4" source = { editable = "." } dependencies = [ { name = "fastapi" }, @@ -715,7 +715,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "fastapi", specifier = ">=0.104.0" }, - { name = "fastmcp", specifier = ">=2.13.0" }, + { name = "fastmcp", specifier = ">=2.13.0,<2.13.2" }, { name = "httpx", specifier = ">=0.27.2" }, { name = "mcp", specifier = ">=1.16.0" }, { name = "pydantic", specifier = ">=2.12.0" },