[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
Shutong Wu 2025-12-07 19:36:44 -05:00 committed by GitHub
parent b09e48a395
commit b34e4c8cf7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 219 additions and 1 deletions

View File

@ -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();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4e1e2d8f3a454a37b18d06a7a7b6c3fb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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))