unity-mcp/MCPForUnity/Editor/Tools/BatchExecute.cs

206 lines
6.7 KiB
C#
Raw Normal View History

[FEATURE] Batch Commands (#418) * Update .Bat file and Bug fix on ManageScript * Update the .Bat file to include runtime folder * Fix the inconsistent EditorPrefs variable so the GUI change on Script Validation could cause real change. * Further changes String to Int for consistency * [Custom Tool] Roslyn Runtime Compilation Allows users to generate/compile codes during Playmode * Fix based on CR * Create claude_skill_unity.zip Upload the unity_claude_skill that can be uploaded to Claude for a combo of unity-mcp-skill. * Update for Custom_Tool Fix and Detection 1. Fix Original Roslyn Compilation Custom Tool to fit the V8 standard 2. Add a new panel in the GUI to see and toggle/untoggle the tools. The toggle feature will be implemented in the future, right now its implemented here to discuss with the team if this is a good feature to add; 3. Add few missing summary in certain tools * Revert "Update for Custom_Tool Fix and Detection" This reverts commit ae8cfe5e256c70ac4a16c79d50341a39cbac18ba. * Update README.md * Reapply "Update for Custom_Tool Fix and Detection" This reverts commit f423c2f25e9ccff4f3b89d1d360ee9cf13143733. * Update ManageScript.cs Fix the layout problem of manage_script in the panel * Update To comply with the current server setting * Update on Batch Tested object generation/modification with batch and it works perfectly! We should push and let users test for a while and see PS: I tried both VS Copilot and Claude Desktop. Claude Desktop works but VS Copilot does not due to the nested structure of batch. Will look into it more. * Revert "Merge branch 'main' into batching" This reverts commit 51fc4b4deb9e907cab3404d8c702131e3da85122, reversing changes made to 318c824e1b78ca74701a1721a5a94f5dc567035f.
2025-12-08 08:36:44 +08:00
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
{
/// <summary>
/// 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.
/// </summary>
[McpForUnityTool("batch_execute", AutoRegister = false)]
public static class BatchExecute
{
private const int MaxCommandsPerBatch = 25;
public static async Task<object> 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<bool?>("failFast") ?? false;
bool parallelRequested = @params.Value<bool?>("parallel") ?? false;
int? maxParallel = @params.Value<int?>("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<object>(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);
invocationSuccessCount++;
commandResults.Add(new
{
tool = toolName,
callSucceeded = true,
result
});
}
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 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();
}
}
}