using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using MCPForUnity.Editor.Helpers; using Newtonsoft.Json.Linq; namespace MCPForUnity.Editor.Tools { /// /// Executes multiple MCP commands within a single Unity-side handler. Commands are executed sequentially /// on the main thread to preserve determinism and Unity API safety. /// [McpForUnityTool("batch_execute", AutoRegister = false)] public static class BatchExecute { private const int MaxCommandsPerBatch = 25; public static async Task HandleCommand(JObject @params) { if (@params == null) { return new ErrorResponse("'commands' payload is required."); } var commandsToken = @params["commands"] as JArray; if (commandsToken == null || commandsToken.Count == 0) { return new ErrorResponse("Provide at least one command entry in 'commands'."); } if (commandsToken.Count > MaxCommandsPerBatch) { return new ErrorResponse($"A maximum of {MaxCommandsPerBatch} commands are allowed per batch."); } bool failFast = @params.Value("failFast") ?? false; bool parallelRequested = @params.Value("parallel") ?? false; int? maxParallel = @params.Value("maxParallelism"); if (parallelRequested) { McpLog.Warn("batch_execute parallel mode requested, but commands will run sequentially on the main thread for safety."); } var commandResults = new List(commandsToken.Count); int invocationSuccessCount = 0; int invocationFailureCount = 0; bool anyCommandFailed = false; foreach (var token in commandsToken) { if (token is not JObject commandObj) { invocationFailureCount++; anyCommandFailed = true; commandResults.Add(new { tool = (string)null, callSucceeded = false, error = "Command entries must be JSON objects." }); if (failFast) { break; } continue; } string toolName = commandObj["tool"]?.ToString(); var rawParams = commandObj["params"] as JObject ?? new JObject(); var commandParams = NormalizeParameterKeys(rawParams); if (string.IsNullOrWhiteSpace(toolName)) { invocationFailureCount++; anyCommandFailed = true; commandResults.Add(new { tool = toolName, callSucceeded = false, error = "Each command must include a non-empty 'tool' field." }); if (failFast) { break; } continue; } try { var result = await CommandRegistry.InvokeCommandAsync(toolName, commandParams).ConfigureAwait(true); bool callSucceeded = DetermineCallSucceeded(result); if (callSucceeded) { invocationSuccessCount++; } else { invocationFailureCount++; anyCommandFailed = true; } commandResults.Add(new { tool = toolName, callSucceeded, result }); if (!callSucceeded && failFast) { break; } } catch (Exception ex) { invocationFailureCount++; anyCommandFailed = true; commandResults.Add(new { tool = toolName, callSucceeded = false, error = ex.Message }); if (failFast) { break; } } } bool overallSuccess = !anyCommandFailed; var data = new { results = commandResults, callSuccessCount = invocationSuccessCount, callFailureCount = invocationFailureCount, parallelRequested, parallelApplied = false, maxParallelism = maxParallel }; return overallSuccess ? new SuccessResponse("Batch execution completed.", data) : new ErrorResponse("One or more commands failed.", data); } private static bool DetermineCallSucceeded(object result) { if (result == null) { return true; } if (result is IMcpResponse response) { return response.Success; } if (result is JObject obj) { var successToken = obj["success"]; if (successToken != null && successToken.Type == JTokenType.Boolean) { return successToken.Value(); } } if (result is JToken token) { var successToken = token["success"]; if (successToken != null && successToken.Type == JTokenType.Boolean) { return successToken.Value(); } } return true; } 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(); } } }