Skip to content

Commit ae2eedd

Browse files
committed
Update
To comply with the current server setting
1 parent 5bcb6f4 commit ae2eedd

File tree

3 files changed

+238
-0
lines changed

3 files changed

+238
-0
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
4+
using MCPForUnity.Editor.Helpers;
5+
using Newtonsoft.Json.Linq;
6+
7+
namespace MCPForUnity.Editor.Tools
8+
{
9+
/// <summary>
10+
/// Executes multiple MCP commands within a single Unity-side handler. Commands are executed sequentially
11+
/// on the main thread to preserve determinism and Unity API safety.
12+
/// </summary>
13+
[McpForUnityTool("batch_execute", AutoRegister = false)]
14+
public static class BatchExecute
15+
{
16+
private const int MaxCommandsPerBatch = 25;
17+
18+
public static async Task<object> HandleCommand(JObject @params)
19+
{
20+
if (@params == null)
21+
{
22+
return new ErrorResponse("'commands' payload is required.");
23+
}
24+
25+
var commandsToken = @params["commands"] as JArray;
26+
if (commandsToken == null || commandsToken.Count == 0)
27+
{
28+
return new ErrorResponse("Provide at least one command entry in 'commands'.");
29+
}
30+
31+
if (commandsToken.Count > MaxCommandsPerBatch)
32+
{
33+
return new ErrorResponse($"A maximum of {MaxCommandsPerBatch} commands are allowed per batch.");
34+
}
35+
36+
bool failFast = @params.Value<bool?>("failFast") ?? false;
37+
bool parallelRequested = @params.Value<bool?>("parallel") ?? false;
38+
int? maxParallel = @params.Value<int?>("maxParallelism");
39+
40+
if (parallelRequested)
41+
{
42+
McpLog.Warn("batch_execute parallel mode requested, but commands will run sequentially on the main thread for safety.");
43+
}
44+
45+
var commandResults = new List<object>(commandsToken.Count);
46+
int successCount = 0;
47+
int failureCount = 0;
48+
49+
foreach (var token in commandsToken)
50+
{
51+
if (token is not JObject commandObj)
52+
{
53+
failureCount++;
54+
commandResults.Add(new
55+
{
56+
success = false,
57+
tool = (string)null,
58+
error = "Command entries must be JSON objects."
59+
});
60+
if (failFast)
61+
{
62+
break;
63+
}
64+
continue;
65+
}
66+
67+
string toolName = commandObj["tool"]?.ToString();
68+
var commandParams = commandObj["params"] as JObject ?? new JObject();
69+
70+
if (string.IsNullOrWhiteSpace(toolName))
71+
{
72+
failureCount++;
73+
commandResults.Add(new
74+
{
75+
success = false,
76+
tool = toolName,
77+
error = "Each command must include a non-empty 'tool' field."
78+
});
79+
if (failFast)
80+
{
81+
break;
82+
}
83+
continue;
84+
}
85+
86+
try
87+
{
88+
var result = await CommandRegistry.InvokeCommandAsync(toolName, commandParams).ConfigureAwait(true);
89+
successCount++;
90+
commandResults.Add(new
91+
{
92+
success = true,
93+
tool = toolName,
94+
result
95+
});
96+
}
97+
catch (Exception ex)
98+
{
99+
failureCount++;
100+
commandResults.Add(new
101+
{
102+
success = false,
103+
tool = toolName,
104+
error = ex.Message
105+
});
106+
107+
if (failFast)
108+
{
109+
break;
110+
}
111+
}
112+
}
113+
114+
bool overallSuccess = failureCount == 0;
115+
var data = new
116+
{
117+
results = commandResults,
118+
successCount,
119+
failureCount,
120+
parallelRequested,
121+
parallelApplied = false,
122+
maxParallelism = maxParallel
123+
};
124+
125+
return overallSuccess
126+
? new SuccessResponse("Batch execution completed.", data)
127+
: new ErrorResponse("One or more commands failed.", data);
128+
}
129+
}
130+
}

MCPForUnity/Editor/Tools/CommandRegistry.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,36 @@ public static object ExecuteCommand(string commandName, JObject @params, TaskCom
248248
return handlerInfo.SyncHandler(@params);
249249
}
250250

251+
/// <summary>
252+
/// Execute a command handler and return its raw result, regardless of sync or async implementation.
253+
/// Used internally for features like batch execution where commands need to be composed.
254+
/// </summary>
255+
/// <param name="commandName">The registered command to execute.</param>
256+
/// <param name="params">Parameters to pass to the command (optional).</param>
257+
public static Task<object> InvokeCommandAsync(string commandName, JObject @params)
258+
{
259+
var handlerInfo = GetHandlerInfo(commandName);
260+
var payload = @params ?? new JObject();
261+
262+
if (handlerInfo.IsAsync)
263+
{
264+
if (handlerInfo.AsyncHandler == null)
265+
{
266+
throw new InvalidOperationException($"Async handler for '{commandName}' is not configured correctly");
267+
}
268+
269+
return handlerInfo.AsyncHandler(payload);
270+
}
271+
272+
if (handlerInfo.SyncHandler == null)
273+
{
274+
throw new InvalidOperationException($"Handler for '{commandName}' does not provide a synchronous implementation");
275+
}
276+
277+
object result = handlerInfo.SyncHandler(payload);
278+
return Task.FromResult(result);
279+
}
280+
251281
/// <summary>
252282
/// Create a delegate for an async handler method that returns Task or Task<T>.
253283
/// The delegate will invoke the method and await its completion, returning the result.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Defines the batch_execute tool for orchestrating multiple Unity MCP commands."""
2+
from __future__ import annotations
3+
4+
from typing import Annotated, Any
5+
6+
from fastmcp import Context
7+
8+
from services.registry import mcp_for_unity_tool
9+
from services.tools import get_unity_instance_from_context
10+
from transport.unity_transport import send_with_unity_instance
11+
from transport.legacy.unity_connection import async_send_command_with_retry
12+
13+
MAX_COMMANDS_PER_BATCH = 25
14+
15+
16+
@mcp_for_unity_tool(
17+
name="batch_execute",
18+
description=(
19+
"Runs a list of MCP tool calls as one batch. Use it to send a full sequence of commands, "
20+
"inspect the results, then submit the next batch for the following step."
21+
),
22+
)
23+
async def batch_execute(
24+
ctx: Context,
25+
commands: Annotated[list[dict[str, Any]], "List of commands with 'tool' and 'params' keys."],
26+
parallel: Annotated[bool | None, "Attempt to run read-only commands in parallel"] = None,
27+
fail_fast: Annotated[bool | None, "Stop processing after the first failure"] = None,
28+
max_parallelism: Annotated[int | None, "Hint for the maximum number of parallel workers"] = None,
29+
) -> dict[str, Any]:
30+
"""Proxy the batch_execute tool to the Unity Editor transporter."""
31+
unity_instance = get_unity_instance_from_context(ctx)
32+
33+
if not isinstance(commands, list) or not commands:
34+
raise ValueError("'commands' must be a non-empty list of command specifications")
35+
36+
if len(commands) > MAX_COMMANDS_PER_BATCH:
37+
raise ValueError(
38+
f"batch_execute currently supports up to {MAX_COMMANDS_PER_BATCH} commands; received {len(commands)}"
39+
)
40+
41+
normalized_commands: list[dict[str, Any]] = []
42+
for index, command in enumerate(commands):
43+
if not isinstance(command, dict):
44+
raise ValueError(f"Command at index {index} must be an object with 'tool' and 'params' keys")
45+
46+
tool_name = command.get("tool")
47+
params = command.get("params", {})
48+
49+
if not tool_name or not isinstance(tool_name, str):
50+
raise ValueError(f"Command at index {index} is missing a valid 'tool' name")
51+
52+
if params is None:
53+
params = {}
54+
if not isinstance(params, dict):
55+
raise ValueError(f"Command '{tool_name}' must specify parameters as an object/dict")
56+
57+
normalized_commands.append({
58+
"tool": tool_name,
59+
"params": params,
60+
})
61+
62+
payload: dict[str, Any] = {
63+
"commands": normalized_commands,
64+
}
65+
66+
if parallel is not None:
67+
payload["parallel"] = bool(parallel)
68+
if fail_fast is not None:
69+
payload["failFast"] = bool(fail_fast)
70+
if max_parallelism is not None:
71+
payload["maxParallelism"] = int(max_parallelism)
72+
73+
return await send_with_unity_instance(
74+
async_send_command_with_retry,
75+
unity_instance,
76+
"batch_execute",
77+
payload,
78+
)

0 commit comments

Comments
 (0)