Skip to content

Commit 318c824

Browse files
committed
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.
1 parent 84f7b85 commit 318c824

File tree

4 files changed

+222
-4
lines changed

4 files changed

+222
-4
lines changed
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using System.Threading.Tasks;
5+
using MCPForUnity.Editor.Helpers;
6+
using Newtonsoft.Json.Linq;
7+
8+
namespace MCPForUnity.Editor.Tools
9+
{
10+
/// <summary>
11+
/// Executes multiple MCP commands within a single Unity-side handler. Commands are executed sequentially
12+
/// on the main thread to preserve determinism and Unity API safety.
13+
/// </summary>
14+
[McpForUnityTool("batch_execute", AutoRegister = false)]
15+
public static class BatchExecute
16+
{
17+
private const int MaxCommandsPerBatch = 25;
18+
19+
public static async Task<object> HandleCommand(JObject @params)
20+
{
21+
if (@params == null)
22+
{
23+
return new ErrorResponse("'commands' payload is required.");
24+
}
25+
26+
var commandsToken = @params["commands"] as JArray;
27+
if (commandsToken == null || commandsToken.Count == 0)
28+
{
29+
return new ErrorResponse("Provide at least one command entry in 'commands'.");
30+
}
31+
32+
if (commandsToken.Count > MaxCommandsPerBatch)
33+
{
34+
return new ErrorResponse($"A maximum of {MaxCommandsPerBatch} commands are allowed per batch.");
35+
}
36+
37+
bool failFast = @params.Value<bool?>("failFast") ?? false;
38+
bool parallelRequested = @params.Value<bool?>("parallel") ?? false;
39+
int? maxParallel = @params.Value<int?>("maxParallelism");
40+
41+
if (parallelRequested)
42+
{
43+
McpLog.Warn("batch_execute parallel mode requested, but commands will run sequentially on the main thread for safety.");
44+
}
45+
46+
var commandResults = new List<object>(commandsToken.Count);
47+
int invocationSuccessCount = 0;
48+
int invocationFailureCount = 0;
49+
bool anyCommandFailed = false;
50+
51+
foreach (var token in commandsToken)
52+
{
53+
if (token is not JObject commandObj)
54+
{
55+
invocationFailureCount++;
56+
anyCommandFailed = true;
57+
commandResults.Add(new
58+
{
59+
tool = (string)null,
60+
callSucceeded = false,
61+
error = "Command entries must be JSON objects."
62+
});
63+
if (failFast)
64+
{
65+
break;
66+
}
67+
continue;
68+
}
69+
70+
string toolName = commandObj["tool"]?.ToString();
71+
var rawParams = commandObj["params"] as JObject ?? new JObject();
72+
var commandParams = NormalizeParameterKeys(rawParams);
73+
74+
if (string.IsNullOrWhiteSpace(toolName))
75+
{
76+
invocationFailureCount++;
77+
anyCommandFailed = true;
78+
commandResults.Add(new
79+
{
80+
tool = toolName,
81+
callSucceeded = false,
82+
error = "Each command must include a non-empty 'tool' field."
83+
});
84+
if (failFast)
85+
{
86+
break;
87+
}
88+
continue;
89+
}
90+
91+
try
92+
{
93+
var result = await CommandRegistry.InvokeCommandAsync(toolName, commandParams).ConfigureAwait(true);
94+
invocationSuccessCount++;
95+
96+
commandResults.Add(new
97+
{
98+
tool = toolName,
99+
callSucceeded = true,
100+
result
101+
});
102+
}
103+
catch (Exception ex)
104+
{
105+
invocationFailureCount++;
106+
anyCommandFailed = true;
107+
commandResults.Add(new
108+
{
109+
tool = toolName,
110+
callSucceeded = false,
111+
error = ex.Message
112+
});
113+
114+
if (failFast)
115+
{
116+
break;
117+
}
118+
}
119+
}
120+
121+
bool overallSuccess = !anyCommandFailed;
122+
var data = new
123+
{
124+
results = commandResults,
125+
callSuccessCount = invocationSuccessCount,
126+
callFailureCount = invocationFailureCount,
127+
parallelRequested,
128+
parallelApplied = false,
129+
maxParallelism = maxParallel
130+
};
131+
132+
return overallSuccess
133+
? new SuccessResponse("Batch execution completed.", data)
134+
: new ErrorResponse("One or more commands failed.", data);
135+
}
136+
137+
private static JObject NormalizeParameterKeys(JObject source)
138+
{
139+
if (source == null)
140+
{
141+
return new JObject();
142+
}
143+
144+
var normalized = new JObject();
145+
foreach (var property in source.Properties())
146+
{
147+
string normalizedName = ToCamelCase(property.Name);
148+
normalized[normalizedName] = NormalizeToken(property.Value);
149+
}
150+
return normalized;
151+
}
152+
153+
private static JArray NormalizeArray(JArray source)
154+
{
155+
var normalized = new JArray();
156+
foreach (var token in source)
157+
{
158+
normalized.Add(NormalizeToken(token));
159+
}
160+
return normalized;
161+
}
162+
163+
private static JToken NormalizeToken(JToken token)
164+
{
165+
return token switch
166+
{
167+
JObject obj => NormalizeParameterKeys(obj),
168+
JArray arr => NormalizeArray(arr),
169+
_ => token.DeepClone()
170+
};
171+
}
172+
173+
private static string ToCamelCase(string key)
174+
{
175+
if (string.IsNullOrEmpty(key) || key.IndexOf('_') < 0)
176+
{
177+
return key;
178+
}
179+
180+
var parts = key.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries);
181+
if (parts.Length == 0)
182+
{
183+
return key;
184+
}
185+
186+
var builder = new StringBuilder(parts[0]);
187+
for (int i = 1; i < parts.Length; i++)
188+
{
189+
var part = parts[i];
190+
if (string.IsNullOrEmpty(part))
191+
{
192+
continue;
193+
}
194+
195+
builder.Append(char.ToUpperInvariant(part[0]));
196+
if (part.Length > 1)
197+
{
198+
builder.Append(part.AsSpan(1));
199+
}
200+
}
201+
202+
return builder.ToString();
203+
}
204+
}
205+
}

MCPForUnity/Editor/Tools/BatchExecute.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

MCPForUnity/Editor/Tools/ManageAsset.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,9 @@ private static object ReimportAsset(string path, JObject properties)
163163
private static object CreateAsset(JObject @params)
164164
{
165165
string path = @params["path"]?.ToString();
166-
string assetType = @params["assetType"]?.ToString();
166+
string assetType =
167+
@params["assetType"]?.ToString()
168+
?? @params["asset_type"]?.ToString(); // tolerate snake_case payloads from batched commands
167169
JObject properties = @params["properties"] as JObject;
168170

169171
if (string.IsNullOrEmpty(path))

Server/uv.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)