[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.main
parent
b09e48a395
commit
b34e4c8cf7
|
|
@ -0,0 +1,205 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4e1e2d8f3a454a37b18d06a7a7b6c3fb
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -163,7 +163,9 @@ namespace MCPForUnity.Editor.Tools
|
||||||
private static object CreateAsset(JObject @params)
|
private static object CreateAsset(JObject @params)
|
||||||
{
|
{
|
||||||
string path = @params["path"]?.ToString();
|
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;
|
JObject properties = @params["properties"] as JObject;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(path))
|
if (string.IsNullOrEmpty(path))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue