[FEATURE] Custom Tool Fix and Add inspection window for all the tools (#414)
* 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 pull request #1 from Scriptwonder/batching" This reverts commit 55ee76810be161d414e1f5f5abaa5ee30ddd0052, reversing changes made to ae2eedd7fb2c6a66ff008bacac481aefb1b0d176.main
parent
b34e4c8cf7
commit
7f8ca2a3bd
|
|
@ -19,9 +19,11 @@ namespace MCPForUnity.Editor.Tools
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runtime compilation tool for MCP Unity.
|
/// Runtime compilation tool for MCP Unity.
|
||||||
/// Compiles and loads C# code at runtime without triggering domain reload.
|
/// Compiles and loads C# code at runtime without triggering domain reload via Roslyn Runtime Compilation, where in traditional Unity workflow it would take seconds to reload assets and reset script states for each script change.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[McpForUnityTool("runtime_compilation")]
|
[McpForUnityTool(
|
||||||
|
name:"runtime_compilation",
|
||||||
|
Description = "Enable runtime compilation of C# code within Unity without domain reload via Roslyn.")]
|
||||||
public static class ManageRuntimeCompilation
|
public static class ManageRuntimeCompilation
|
||||||
{
|
{
|
||||||
private static readonly Dictionary<string, LoadedAssemblyInfo> LoadedAssemblies = new Dictionary<string, LoadedAssemblyInfo>();
|
private static readonly Dictionary<string, LoadedAssemblyInfo> LoadedAssemblies = new Dictionary<string, LoadedAssemblyInfo>();
|
||||||
|
|
@ -42,7 +44,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(action))
|
if (string.IsNullOrEmpty(action))
|
||||||
{
|
{
|
||||||
return Response.Error("Action parameter is required. Valid actions: compile_and_load, list_loaded, get_types, execute_with_roslyn, get_history, save_history, clear_history");
|
return new ErrorResponse("Action parameter is required. Valid actions: compile_and_load, list_loaded, get_types, execute_with_roslyn, get_history, save_history, clear_history");
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (action)
|
switch (action)
|
||||||
|
|
@ -69,14 +71,14 @@ namespace MCPForUnity.Editor.Tools
|
||||||
return ClearCompilationHistory();
|
return ClearCompilationHistory();
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return Response.Error($"Unknown action '{action}'. Valid actions: compile_and_load, list_loaded, get_types, execute_with_roslyn, get_history, save_history, clear_history");
|
return new ErrorResponse($"Unknown action '{action}'. Valid actions: compile_and_load, list_loaded, get_types, execute_with_roslyn, get_history, save_history, clear_history");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object CompileAndLoad(JObject @params)
|
private static object CompileAndLoad(JObject @params)
|
||||||
{
|
{
|
||||||
#if !USE_ROSLYN
|
#if !USE_ROSLYN
|
||||||
return Response.Error(
|
return new ErrorResponse(
|
||||||
"Runtime compilation requires Roslyn. Please install Microsoft.CodeAnalysis.CSharp NuGet package and add USE_ROSLYN to Scripting Define Symbols. " +
|
"Runtime compilation requires Roslyn. Please install Microsoft.CodeAnalysis.CSharp NuGet package and add USE_ROSLYN to Scripting Define Symbols. " +
|
||||||
"See ManageScript.cs header for installation instructions."
|
"See ManageScript.cs header for installation instructions."
|
||||||
);
|
);
|
||||||
|
|
@ -84,16 +86,13 @@ namespace MCPForUnity.Editor.Tools
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string code = @params["code"]?.ToString();
|
string code = @params["code"]?.ToString();
|
||||||
var assemblyToken = @params["assembly_name"];
|
string assemblyName = @params["assembly_name"]?.ToString() ?? $"DynamicAssembly_{DateTime.Now.Ticks}";
|
||||||
string assemblyName = assemblyToken == null || string.IsNullOrWhiteSpace(assemblyToken.ToString())
|
|
||||||
? $"DynamicAssembly_{DateTime.Now.Ticks}"
|
|
||||||
: assemblyToken.ToString().Trim();
|
|
||||||
string attachTo = @params["attach_to"]?.ToString();
|
string attachTo = @params["attach_to"]?.ToString();
|
||||||
bool loadImmediately = @params["load_immediately"]?.ToObject<bool>() ?? true;
|
bool loadImmediately = @params["load_immediately"]?.ToObject<bool>() ?? true;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(code))
|
if (string.IsNullOrEmpty(code))
|
||||||
{
|
{
|
||||||
return Response.Error("'code' parameter is required");
|
return new ErrorResponse("'code' parameter is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure unique assembly name
|
// Ensure unique assembly name
|
||||||
|
|
@ -104,21 +103,8 @@ namespace MCPForUnity.Editor.Tools
|
||||||
|
|
||||||
// Create output directory
|
// Create output directory
|
||||||
Directory.CreateDirectory(DynamicAssembliesPath);
|
Directory.CreateDirectory(DynamicAssembliesPath);
|
||||||
string basePath = Path.GetFullPath(DynamicAssembliesPath);
|
string dllPath = Path.Combine(DynamicAssembliesPath, $"{assemblyName}.dll");
|
||||||
Directory.CreateDirectory(basePath);
|
|
||||||
string safeFileName = SanitizeAssemblyFileName(assemblyName);
|
|
||||||
string dllPath = Path.GetFullPath(Path.Combine(basePath, $"{safeFileName}.dll"));
|
|
||||||
|
|
||||||
if (!dllPath.StartsWith(basePath, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return Response.Error("Assembly name must resolve inside the dynamic assemblies directory.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (File.Exists(dllPath))
|
|
||||||
{
|
|
||||||
dllPath = Path.GetFullPath(Path.Combine(basePath, $"{safeFileName}_{DateTime.Now.Ticks}.dll"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse code
|
// Parse code
|
||||||
var syntaxTree = CSharpSyntaxTree.ParseText(code);
|
var syntaxTree = CSharpSyntaxTree.ParseText(code);
|
||||||
|
|
||||||
|
|
@ -137,7 +123,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
|
|
||||||
// Emit to file
|
// Emit to file
|
||||||
EmitResult emitResult;
|
EmitResult emitResult;
|
||||||
using (var stream = new FileStream(dllPath, FileMode.Create, FileAccess.Write, FileShare.None))
|
using (var stream = new FileStream(dllPath, FileMode.Create))
|
||||||
{
|
{
|
||||||
emitResult = compilation.Emit(stream);
|
emitResult = compilation.Emit(stream);
|
||||||
}
|
}
|
||||||
|
|
@ -156,7 +142,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return Response.Error("Compilation failed", new
|
return new ErrorResponse("Compilation failed", new
|
||||||
{
|
{
|
||||||
errors = errors,
|
errors = errors,
|
||||||
error_count = errors.Count
|
error_count = errors.Count
|
||||||
|
|
@ -222,7 +208,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response.Success("Runtime compilation completed successfully", new
|
return new SuccessResponse("Runtime compilation completed successfully", new
|
||||||
{
|
{
|
||||||
assembly_name = assemblyName,
|
assembly_name = assemblyName,
|
||||||
dll_path = dllPath,
|
dll_path = dllPath,
|
||||||
|
|
@ -235,7 +221,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return Response.Error($"Runtime compilation failed: {ex.Message}", new
|
return new ErrorResponse($"Runtime compilation failed: {ex.Message}", new
|
||||||
{
|
{
|
||||||
exception = ex.GetType().Name,
|
exception = ex.GetType().Name,
|
||||||
stack_trace = ex.StackTrace
|
stack_trace = ex.StackTrace
|
||||||
|
|
@ -243,7 +229,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object ListLoadedAssemblies()
|
private static object ListLoadedAssemblies()
|
||||||
{
|
{
|
||||||
var assemblies = LoadedAssemblies.Values.Select(info => new
|
var assemblies = LoadedAssemblies.Values.Select(info => new
|
||||||
|
|
@ -254,33 +240,26 @@ namespace MCPForUnity.Editor.Tools
|
||||||
type_count = info.TypeNames.Count,
|
type_count = info.TypeNames.Count,
|
||||||
types = info.TypeNames
|
types = info.TypeNames
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
return Response.Success($"Found {assemblies.Count} loaded dynamic assemblies", new
|
return new SuccessResponse($"Found {assemblies.Count} loaded dynamic assemblies", new
|
||||||
{
|
{
|
||||||
count = assemblies.Count,
|
count = assemblies.Count,
|
||||||
assemblies = assemblies
|
assemblies = assemblies
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string SanitizeAssemblyFileName(string assemblyName)
|
|
||||||
{
|
|
||||||
var invalidChars = Path.GetInvalidFileNameChars();
|
|
||||||
var sanitized = new string(assemblyName.Where(c => !invalidChars.Contains(c)).ToArray());
|
|
||||||
return string.IsNullOrWhiteSpace(sanitized) ? $"DynamicAssembly_{DateTime.Now.Ticks}" : sanitized;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static object GetAssemblyTypes(JObject @params)
|
private static object GetAssemblyTypes(JObject @params)
|
||||||
{
|
{
|
||||||
string assemblyName = @params["assembly_name"]?.ToString();
|
string assemblyName = @params["assembly_name"]?.ToString();
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(assemblyName))
|
if (string.IsNullOrEmpty(assemblyName))
|
||||||
{
|
{
|
||||||
return Response.Error("'assembly_name' parameter is required");
|
return new ErrorResponse("'assembly_name' parameter is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!LoadedAssemblies.TryGetValue(assemblyName, out var info))
|
if (!LoadedAssemblies.TryGetValue(assemblyName, out var info))
|
||||||
{
|
{
|
||||||
return Response.Error($"Assembly '{assemblyName}' not found in loaded assemblies");
|
return new ErrorResponse($"Assembly '{assemblyName}' not found in loaded assemblies");
|
||||||
}
|
}
|
||||||
|
|
||||||
var types = info.Assembly.GetTypes().Select(t => new
|
var types = info.Assembly.GetTypes().Select(t => new
|
||||||
|
|
@ -294,7 +273,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
base_type = t.BaseType?.FullName
|
base_type = t.BaseType?.FullName
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
return Response.Success($"Retrieved {types.Count} types from {assemblyName}", new
|
return new SuccessResponse($"Retrieved {types.Count} types from {assemblyName}", new
|
||||||
{
|
{
|
||||||
assembly_name = assemblyName,
|
assembly_name = assemblyName,
|
||||||
type_count = types.Count,
|
type_count = types.Count,
|
||||||
|
|
@ -318,7 +297,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(code))
|
if (string.IsNullOrEmpty(code))
|
||||||
{
|
{
|
||||||
return Response.Error("'code' parameter is required");
|
return new ErrorResponse("'code' parameter is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get or create the RoslynRuntimeCompiler instance
|
// Get or create the RoslynRuntimeCompiler instance
|
||||||
|
|
@ -336,7 +315,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
|
|
||||||
if (targetObject == null)
|
if (targetObject == null)
|
||||||
{
|
{
|
||||||
return Response.Error($"Target GameObject '{targetObjectName}' not found");
|
return new ErrorResponse($"Target GameObject '{targetObjectName}' not found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -352,7 +331,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
|
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
return Response.Success($"Code compiled and executed successfully", new
|
return new SuccessResponse($"Code compiled and executed successfully", new
|
||||||
{
|
{
|
||||||
class_name = className,
|
class_name = className,
|
||||||
method_name = methodName,
|
method_name = methodName,
|
||||||
|
|
@ -363,7 +342,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return Response.Error($"Execution failed: {errorMessage}", new
|
return new ErrorResponse($"Execution failed: {errorMessage}", new
|
||||||
{
|
{
|
||||||
diagnostics = compiler.lastCompileDiagnostics
|
diagnostics = compiler.lastCompileDiagnostics
|
||||||
});
|
});
|
||||||
|
|
@ -371,7 +350,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return Response.Error($"Failed to execute with Roslyn: {ex.Message}", new
|
return new ErrorResponse($"Failed to execute with Roslyn: {ex.Message}", new
|
||||||
{
|
{
|
||||||
exception = ex.GetType().Name,
|
exception = ex.GetType().Name,
|
||||||
stack_trace = ex.StackTrace
|
stack_trace = ex.StackTrace
|
||||||
|
|
@ -402,7 +381,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
: entry.sourceCode
|
: entry.sourceCode
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
return Response.Success($"Retrieved {historyData.Count} history entries", new
|
return new SuccessResponse($"Retrieved {historyData.Count} history entries", new
|
||||||
{
|
{
|
||||||
count = historyData.Count,
|
count = historyData.Count,
|
||||||
history = historyData
|
history = historyData
|
||||||
|
|
@ -410,7 +389,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return Response.Error($"Failed to get history: {ex.Message}");
|
return new ErrorResponse($"Failed to get history: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -425,7 +404,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
|
|
||||||
if (compiler.SaveHistoryToFile(out string savedPath, out string error))
|
if (compiler.SaveHistoryToFile(out string savedPath, out string error))
|
||||||
{
|
{
|
||||||
return Response.Success($"History saved successfully", new
|
return new SuccessResponse($"History saved successfully", new
|
||||||
{
|
{
|
||||||
path = savedPath,
|
path = savedPath,
|
||||||
entry_count = compiler.CompilationHistory.Count
|
entry_count = compiler.CompilationHistory.Count
|
||||||
|
|
@ -433,12 +412,12 @@ namespace MCPForUnity.Editor.Tools
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return Response.Error($"Failed to save history: {error}");
|
return new ErrorResponse($"Failed to save history: {error}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return Response.Error($"Failed to save history: {ex.Message}");
|
return new ErrorResponse($"Failed to save history: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -453,11 +432,11 @@ namespace MCPForUnity.Editor.Tools
|
||||||
int count = compiler.CompilationHistory.Count;
|
int count = compiler.CompilationHistory.Count;
|
||||||
compiler.ClearHistory();
|
compiler.ClearHistory();
|
||||||
|
|
||||||
return Response.Success($"Cleared {count} history entries");
|
return new SuccessResponse($"Cleared {count} history entries");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return Response.Error($"Failed to clear history: {ex.Message}");
|
return new ErrorResponse($"Failed to clear history: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
# Roslyn Runtime Compilation Tool
|
|
||||||
|
|
||||||
This custom tool uses Roslyn Runtime Compilation to have users run script generation and compilation during Playmode in realtime, where in traditional Unity workflow it would take seconds to reload assets and reset script states for each script change.
|
|
||||||
|
|
@ -1,269 +0,0 @@
|
||||||
"""
|
|
||||||
Runtime compilation tool for MCP Unity.
|
|
||||||
Compiles and loads C# code at runtime without domain reload.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Annotated, Any
|
|
||||||
from fastmcp import Context
|
|
||||||
from registry import mcp_for_unity_tool
|
|
||||||
from unity_connection import send_command_with_retry
|
|
||||||
|
|
||||||
|
|
||||||
async def safe_info(ctx: Context, message: str) -> None:
|
|
||||||
"""Safely send info messages when a request context is available."""
|
|
||||||
try:
|
|
||||||
if ctx and hasattr(ctx, "info"):
|
|
||||||
await ctx.info(message)
|
|
||||||
except RuntimeError as ex:
|
|
||||||
# FastMCP raises this when called outside of an active request
|
|
||||||
if "outside of a request" not in str(ex):
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
def handle_unity_command(command_name: str, params: dict) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Wrapper for Unity commands with better error handling.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
response = send_command_with_retry(command_name, params)
|
|
||||||
return response if isinstance(response, dict) else {"success": False, "message": str(response)}
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = str(e)
|
|
||||||
if "Context is not available" in error_msg or "not available outside of a request" in error_msg:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"message": "Unity is not connected. Please ensure Unity Editor is running and MCP bridge is active.",
|
|
||||||
"error": "connection_error",
|
|
||||||
"details": "This tool requires an active connection to Unity. Make sure the Unity project is open and the MCP bridge is initialized."
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"message": f"Command failed: {error_msg}",
|
|
||||||
"error": "tool_error"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@mcp_for_unity_tool(
|
|
||||||
description="Compile and load C# code at runtime without domain reload. Creates dynamic assemblies that can be attached to GameObjects during Play Mode. Requires Roslyn (Microsoft.CodeAnalysis.CSharp) to be installed in Unity."
|
|
||||||
)
|
|
||||||
async def compile_runtime_code(
|
|
||||||
ctx: Context,
|
|
||||||
code: Annotated[str, "Complete C# code including using statements, namespace, and class definition"],
|
|
||||||
assembly_name: Annotated[str, "Unique name for the dynamic assembly. If not provided, a timestamp-based name will be generated."] | None = None,
|
|
||||||
attach_to_gameobject: Annotated[str, "Name or hierarchy path of GameObject to attach the compiled script to (e.g., 'Player' or 'Canvas/Panel')"] | None = None,
|
|
||||||
load_immediately: Annotated[bool, "Whether to load the assembly immediately after compilation. Default is true."] = True
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Compile C# code at runtime and optionally attach it to a GameObject. Only enable it with Roslyn installed in Unity.
|
|
||||||
|
|
||||||
REQUIREMENTS:
|
|
||||||
- Unity must be running and connected
|
|
||||||
- Roslyn (Microsoft.CodeAnalysis.CSharp) must be installed via NuGet
|
|
||||||
- USE_ROSLYN scripting define symbol must be set
|
|
||||||
|
|
||||||
This tool allows you to:
|
|
||||||
- Compile new C# scripts without restarting Unity
|
|
||||||
- Load compiled assemblies into the running Unity instance
|
|
||||||
- Attach MonoBehaviour scripts to GameObjects dynamically
|
|
||||||
- Preserve game state during script additions
|
|
||||||
|
|
||||||
Example code:
|
|
||||||
```csharp
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace DynamicScripts
|
|
||||||
{
|
|
||||||
public class MyDynamicBehavior : MonoBehaviour
|
|
||||||
{
|
|
||||||
void Start()
|
|
||||||
{
|
|
||||||
Debug.Log("Dynamic script loaded!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
await safe_info(ctx, f"Compiling runtime code for assembly: {assembly_name or 'auto-generated'}")
|
|
||||||
|
|
||||||
params = {
|
|
||||||
"action": "compile_and_load",
|
|
||||||
"code": code,
|
|
||||||
"assembly_name": assembly_name,
|
|
||||||
"attach_to": attach_to_gameobject,
|
|
||||||
"load_immediately": load_immediately,
|
|
||||||
}
|
|
||||||
params = {k: v for k, v in params.items() if v is not None}
|
|
||||||
|
|
||||||
return handle_unity_command("runtime_compilation", params)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp_for_unity_tool(
|
|
||||||
description="List all dynamically loaded assemblies in the current Unity session"
|
|
||||||
)
|
|
||||||
async def list_loaded_assemblies(
|
|
||||||
ctx: Context,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Get a list of all dynamically loaded assemblies created during this session.
|
|
||||||
|
|
||||||
Returns information about:
|
|
||||||
- Assembly names
|
|
||||||
- Number of types in each assembly
|
|
||||||
- Load timestamps
|
|
||||||
- DLL file paths
|
|
||||||
"""
|
|
||||||
await safe_info(ctx, "Retrieving loaded dynamic assemblies...")
|
|
||||||
|
|
||||||
params = {"action": "list_loaded"}
|
|
||||||
return handle_unity_command("runtime_compilation", params)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp_for_unity_tool(
|
|
||||||
description="Get all types (classes) from a dynamically loaded assembly"
|
|
||||||
)
|
|
||||||
async def get_assembly_types(
|
|
||||||
ctx: Context,
|
|
||||||
assembly_name: Annotated[str, "Name of the assembly to query"],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Retrieve all types defined in a specific dynamic assembly.
|
|
||||||
|
|
||||||
This is useful for:
|
|
||||||
- Inspecting what was compiled
|
|
||||||
- Finding MonoBehaviour classes to attach
|
|
||||||
- Debugging compilation results
|
|
||||||
"""
|
|
||||||
await safe_info(ctx, f"Getting types from assembly: {assembly_name}")
|
|
||||||
|
|
||||||
params = {"action": "get_types", "assembly_name": assembly_name}
|
|
||||||
return handle_unity_command("runtime_compilation", params)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp_for_unity_tool(
|
|
||||||
description="Execute C# code using the RoslynRuntimeCompiler with full GUI tool features including history tracking, MonoBehaviour support, and coroutines"
|
|
||||||
)
|
|
||||||
async def execute_with_roslyn(
|
|
||||||
ctx: Context,
|
|
||||||
code: Annotated[str, "Complete C# source code to compile and execute"],
|
|
||||||
class_name: Annotated[str, "Name of the class to instantiate/invoke (default: AIGenerated)"] = "AIGenerated",
|
|
||||||
method_name: Annotated[str, "Name of the static method to call (default: Run)"] = "Run",
|
|
||||||
target_object: Annotated[str, "Name or path of target GameObject (optional)"] | None = None,
|
|
||||||
attach_as_component: Annotated[bool, "If true and type is MonoBehaviour, attach as component (default: false)"] = False,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Execute C# code using Unity's RoslynRuntimeCompiler tool with advanced features:
|
|
||||||
|
|
||||||
- MonoBehaviour attachment: Set attach_as_component=true for classes inheriting MonoBehaviour
|
|
||||||
- Static method execution: Call public static methods (e.g., public static void Run(GameObject host))
|
|
||||||
- Coroutine support: Methods returning IEnumerator will be started as coroutines
|
|
||||||
- History tracking: All compilations are tracked in history for later review
|
|
||||||
|
|
||||||
Supported method signatures:
|
|
||||||
- public static void Run()
|
|
||||||
- public static void Run(GameObject host)
|
|
||||||
- public static void Run(MonoBehaviour host)
|
|
||||||
- public static IEnumerator RunCoroutine(MonoBehaviour host)
|
|
||||||
|
|
||||||
Example MonoBehaviour:
|
|
||||||
```csharp
|
|
||||||
using UnityEngine;
|
|
||||||
public class Rotator : MonoBehaviour {
|
|
||||||
void Update() {
|
|
||||||
transform.Rotate(Vector3.up * 30f * Time.deltaTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Example Static Method:
|
|
||||||
```csharp
|
|
||||||
using UnityEngine;
|
|
||||||
public class AIGenerated {
|
|
||||||
public static void Run(GameObject host) {
|
|
||||||
Debug.Log($"Hello from {host.name}!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
await safe_info(ctx, f"Executing code with RoslynRuntimeCompiler: {class_name}.{method_name}")
|
|
||||||
|
|
||||||
params = {
|
|
||||||
"action": "execute_with_roslyn",
|
|
||||||
"code": code,
|
|
||||||
"class_name": class_name,
|
|
||||||
"method_name": method_name,
|
|
||||||
"target_object": target_object,
|
|
||||||
"attach_as_component": attach_as_component,
|
|
||||||
}
|
|
||||||
params = {k: v for k, v in params.items() if v is not None}
|
|
||||||
|
|
||||||
return handle_unity_command("runtime_compilation", params)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp_for_unity_tool(
|
|
||||||
description="Get the compilation history from RoslynRuntimeCompiler showing all previous compilations and executions"
|
|
||||||
)
|
|
||||||
async def get_compilation_history(
|
|
||||||
ctx: Context,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Retrieve the compilation history from the RoslynRuntimeCompiler.
|
|
||||||
|
|
||||||
History includes:
|
|
||||||
- Timestamp of each compilation
|
|
||||||
- Class and method names
|
|
||||||
- Success/failure status
|
|
||||||
- Compilation diagnostics
|
|
||||||
- Target GameObject names
|
|
||||||
- Source code previews
|
|
||||||
|
|
||||||
This is useful for:
|
|
||||||
- Reviewing what code has been compiled
|
|
||||||
- Debugging failed compilations
|
|
||||||
- Tracking execution flow
|
|
||||||
- Auditing dynamic code changes
|
|
||||||
"""
|
|
||||||
await safe_info(ctx, "Retrieving compilation history...")
|
|
||||||
|
|
||||||
params = {"action": "get_history"}
|
|
||||||
return handle_unity_command("runtime_compilation", params)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp_for_unity_tool(
|
|
||||||
description="Save the compilation history to a JSON file outside the Assets folder"
|
|
||||||
)
|
|
||||||
async def save_compilation_history(
|
|
||||||
ctx: Context,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Save all compilation history to a timestamped JSON file.
|
|
||||||
|
|
||||||
The file is saved to: ProjectRoot/RoslynHistory/RoslynHistory_TIMESTAMP.json
|
|
||||||
|
|
||||||
This allows you to:
|
|
||||||
- Keep a permanent record of dynamic compilations
|
|
||||||
- Review history after Unity restarts
|
|
||||||
- Share compilation sessions with team members
|
|
||||||
- Archive successful code patterns
|
|
||||||
"""
|
|
||||||
await safe_info(ctx, "Saving compilation history to file...")
|
|
||||||
|
|
||||||
params = {"action": "save_history"}
|
|
||||||
return handle_unity_command("runtime_compilation", params)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp_for_unity_tool(
|
|
||||||
description="Clear all compilation history from RoslynRuntimeCompiler"
|
|
||||||
)
|
|
||||||
async def clear_compilation_history(
|
|
||||||
ctx: Context,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Clear all compilation history entries.
|
|
||||||
|
|
||||||
This removes all tracked compilations from memory but does not delete
|
|
||||||
saved history files. Use this to start fresh or reduce memory usage.
|
|
||||||
"""
|
|
||||||
await safe_info(ctx, "Clearing compilation history...")
|
|
||||||
|
|
||||||
params = {"action": "clear_history"}
|
|
||||||
return handle_unity_command("runtime_compilation", params)
|
|
||||||
|
|
@ -25,6 +25,9 @@ namespace MCPForUnity.Editor.Constants
|
||||||
internal const string UseEmbeddedServer = "MCPForUnity.UseEmbeddedServer";
|
internal const string UseEmbeddedServer = "MCPForUnity.UseEmbeddedServer";
|
||||||
internal const string LockCursorConfig = "MCPForUnity.LockCursorConfig";
|
internal const string LockCursorConfig = "MCPForUnity.LockCursorConfig";
|
||||||
internal const string AutoRegisterEnabled = "MCPForUnity.AutoRegisterEnabled";
|
internal const string AutoRegisterEnabled = "MCPForUnity.AutoRegisterEnabled";
|
||||||
|
internal const string ToolEnabledPrefix = "MCPForUnity.ToolEnabled.";
|
||||||
|
internal const string ToolFoldoutStatePrefix = "MCPForUnity.ToolFoldout.";
|
||||||
|
internal const string EditorWindowActivePanel = "MCPForUnity.EditorWindow.ActivePanel";
|
||||||
|
|
||||||
internal const string SetupCompleted = "MCPForUnity.SetupCompleted";
|
internal const string SetupCompleted = "MCPForUnity.SetupCompleted";
|
||||||
internal const string SetupDismissed = "MCPForUnity.SetupDismissed";
|
internal const string SetupDismissed = "MCPForUnity.SetupDismissed";
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,12 @@ namespace MCPForUnity.Editor.Services
|
||||||
public List<ParameterMetadata> Parameters { get; set; }
|
public List<ParameterMetadata> Parameters { get; set; }
|
||||||
public string ClassName { get; set; }
|
public string ClassName { get; set; }
|
||||||
public string Namespace { get; set; }
|
public string Namespace { get; set; }
|
||||||
|
public string AssemblyName { get; set; }
|
||||||
|
public string AssetPath { get; set; }
|
||||||
public bool AutoRegister { get; set; } = true;
|
public bool AutoRegister { get; set; } = true;
|
||||||
public bool RequiresPolling { get; set; } = false;
|
public bool RequiresPolling { get; set; } = false;
|
||||||
public string PollAction { get; set; } = "status";
|
public string PollAction { get; set; } = "status";
|
||||||
|
public bool IsBuiltIn { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -45,6 +48,21 @@ namespace MCPForUnity.Editor.Services
|
||||||
/// </summary>
|
/// </summary>
|
||||||
ToolMetadata GetToolMetadata(string toolName);
|
ToolMetadata GetToolMetadata(string toolName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns only the tools currently enabled for registration
|
||||||
|
/// </summary>
|
||||||
|
List<ToolMetadata> GetEnabledTools();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether a tool is currently enabled for registration
|
||||||
|
/// </summary>
|
||||||
|
bool IsToolEnabled(string toolName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the enabled state for a tool
|
||||||
|
/// </summary>
|
||||||
|
void SetToolEnabled(string toolName, bool enabled);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invalidates the tool discovery cache
|
/// Invalidates the tool discovery cache
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using MCPForUnity.Editor.Constants;
|
||||||
using MCPForUnity.Editor.Helpers;
|
using MCPForUnity.Editor.Helpers;
|
||||||
using MCPForUnity.Editor.Tools;
|
using MCPForUnity.Editor.Tools;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
|
|
@ -11,6 +13,8 @@ namespace MCPForUnity.Editor.Services
|
||||||
public class ToolDiscoveryService : IToolDiscoveryService
|
public class ToolDiscoveryService : IToolDiscoveryService
|
||||||
{
|
{
|
||||||
private Dictionary<string, ToolMetadata> _cachedTools;
|
private Dictionary<string, ToolMetadata> _cachedTools;
|
||||||
|
private readonly Dictionary<Type, string> _scriptPathCache = new();
|
||||||
|
private readonly Dictionary<string, string> _summaryCache = new();
|
||||||
|
|
||||||
public List<ToolMetadata> DiscoverAllTools()
|
public List<ToolMetadata> DiscoverAllTools()
|
||||||
{
|
{
|
||||||
|
|
@ -40,6 +44,7 @@ namespace MCPForUnity.Editor.Services
|
||||||
if (metadata != null)
|
if (metadata != null)
|
||||||
{
|
{
|
||||||
_cachedTools[metadata.Name] = metadata;
|
_cachedTools[metadata.Name] = metadata;
|
||||||
|
EnsurePreferenceInitialized(metadata);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -64,6 +69,41 @@ namespace MCPForUnity.Editor.Services
|
||||||
return _cachedTools.TryGetValue(toolName, out var metadata) ? metadata : null;
|
return _cachedTools.TryGetValue(toolName, out var metadata) ? metadata : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<ToolMetadata> GetEnabledTools()
|
||||||
|
{
|
||||||
|
return DiscoverAllTools()
|
||||||
|
.Where(tool => IsToolEnabled(tool.Name))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsToolEnabled(string toolName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(toolName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = GetToolPreferenceKey(toolName);
|
||||||
|
if (EditorPrefs.HasKey(key))
|
||||||
|
{
|
||||||
|
return EditorPrefs.GetBool(key, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata = GetToolMetadata(toolName);
|
||||||
|
return metadata?.AutoRegister ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetToolEnabled(string toolName, bool enabled)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(toolName))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = GetToolPreferenceKey(toolName);
|
||||||
|
EditorPrefs.SetBool(key, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
private ToolMetadata ExtractToolMetadata(Type type, McpForUnityToolAttribute toolAttr)
|
private ToolMetadata ExtractToolMetadata(Type type, McpForUnityToolAttribute toolAttr)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -82,7 +122,7 @@ namespace MCPForUnity.Editor.Services
|
||||||
// Extract parameters
|
// Extract parameters
|
||||||
var parameters = ExtractParameters(type);
|
var parameters = ExtractParameters(type);
|
||||||
|
|
||||||
return new ToolMetadata
|
var metadata = new ToolMetadata
|
||||||
{
|
{
|
||||||
Name = toolName,
|
Name = toolName,
|
||||||
Description = description,
|
Description = description,
|
||||||
|
|
@ -90,10 +130,24 @@ namespace MCPForUnity.Editor.Services
|
||||||
Parameters = parameters,
|
Parameters = parameters,
|
||||||
ClassName = type.Name,
|
ClassName = type.Name,
|
||||||
Namespace = type.Namespace ?? "",
|
Namespace = type.Namespace ?? "",
|
||||||
|
AssemblyName = type.Assembly.GetName().Name,
|
||||||
|
AssetPath = ResolveScriptAssetPath(type),
|
||||||
AutoRegister = toolAttr.AutoRegister,
|
AutoRegister = toolAttr.AutoRegister,
|
||||||
RequiresPolling = toolAttr.RequiresPolling,
|
RequiresPolling = toolAttr.RequiresPolling,
|
||||||
PollAction = string.IsNullOrEmpty(toolAttr.PollAction) ? "status" : toolAttr.PollAction
|
PollAction = string.IsNullOrEmpty(toolAttr.PollAction) ? "status" : toolAttr.PollAction
|
||||||
};
|
};
|
||||||
|
|
||||||
|
metadata.IsBuiltIn = DetermineIsBuiltIn(type, metadata);
|
||||||
|
if (metadata.IsBuiltIn)
|
||||||
|
{
|
||||||
|
string summaryDescription = ExtractSummaryDescription(type, metadata);
|
||||||
|
if (!string.IsNullOrWhiteSpace(summaryDescription))
|
||||||
|
{
|
||||||
|
metadata.Description = summaryDescription;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return metadata;
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -180,5 +234,173 @@ namespace MCPForUnity.Editor.Services
|
||||||
{
|
{
|
||||||
_cachedTools = null;
|
_cachedTools = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void EnsurePreferenceInitialized(ToolMetadata metadata)
|
||||||
|
{
|
||||||
|
if (metadata == null || string.IsNullOrEmpty(metadata.Name))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = GetToolPreferenceKey(metadata.Name);
|
||||||
|
if (!EditorPrefs.HasKey(key))
|
||||||
|
{
|
||||||
|
bool defaultValue = metadata.AutoRegister || metadata.IsBuiltIn;
|
||||||
|
EditorPrefs.SetBool(key, defaultValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.IsBuiltIn && !metadata.AutoRegister)
|
||||||
|
{
|
||||||
|
bool currentValue = EditorPrefs.GetBool(key, metadata.AutoRegister);
|
||||||
|
if (currentValue == metadata.AutoRegister)
|
||||||
|
{
|
||||||
|
EditorPrefs.SetBool(key, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetToolPreferenceKey(string toolName)
|
||||||
|
{
|
||||||
|
return EditorPrefKeys.ToolEnabledPrefix + toolName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveScriptAssetPath(Type type)
|
||||||
|
{
|
||||||
|
if (type == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_scriptPathCache.TryGetValue(type, out var cachedPath))
|
||||||
|
{
|
||||||
|
return cachedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
string resolvedPath = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string filter = string.IsNullOrEmpty(type.Name) ? "t:MonoScript" : $"{type.Name} t:MonoScript";
|
||||||
|
var guids = AssetDatabase.FindAssets(filter);
|
||||||
|
|
||||||
|
foreach (var guid in guids)
|
||||||
|
{
|
||||||
|
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
|
||||||
|
if (string.IsNullOrEmpty(assetPath))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var script = AssetDatabase.LoadAssetAtPath<MonoScript>(assetPath);
|
||||||
|
if (script == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scriptClass = script.GetClass();
|
||||||
|
if (scriptClass == type)
|
||||||
|
{
|
||||||
|
resolvedPath = assetPath.Replace('\\', '/');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
McpLog.Warn($"Failed to resolve asset path for {type.FullName}: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_scriptPathCache[type] = resolvedPath;
|
||||||
|
return resolvedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DetermineIsBuiltIn(Type type, ToolMetadata metadata)
|
||||||
|
{
|
||||||
|
if (metadata == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(metadata.AssetPath))
|
||||||
|
{
|
||||||
|
string normalizedPath = metadata.AssetPath.Replace("\\", "/");
|
||||||
|
string packageRoot = AssetPathUtility.GetMcpPackageRootPath();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(packageRoot))
|
||||||
|
{
|
||||||
|
string normalizedRoot = packageRoot.Replace("\\", "/");
|
||||||
|
if (!normalizedRoot.EndsWith("/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
normalizedRoot += "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
string builtInRoot = normalizedRoot + "Editor/Tools/";
|
||||||
|
if (normalizedPath.StartsWith(builtInRoot, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(metadata.AssemblyName) && metadata.AssemblyName.Equals("MCPForUnity.Editor", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ExtractSummaryDescription(Type type, ToolMetadata metadata)
|
||||||
|
{
|
||||||
|
if (metadata == null || string.IsNullOrEmpty(metadata.AssetPath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_summaryCache.TryGetValue(metadata.AssetPath, out var cachedSummary))
|
||||||
|
{
|
||||||
|
return cachedSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
string summary = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var monoScript = AssetDatabase.LoadAssetAtPath<MonoScript>(metadata.AssetPath);
|
||||||
|
string scriptText = monoScript?.text;
|
||||||
|
if (string.IsNullOrEmpty(scriptText))
|
||||||
|
{
|
||||||
|
_summaryCache[metadata.AssetPath] = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
string classPattern = $@"///\s*<summary>\s*(?<content>[\s\S]*?)\s*</summary>\s*(?:\[[^\]]*\]\s*)*(?:public\s+)?(?:static\s+)?class\s+{Regex.Escape(type.Name)}";
|
||||||
|
var match = Regex.Match(scriptText, classPattern);
|
||||||
|
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
match = Regex.Match(scriptText, @"///\s*<summary>\s*(?<content>[\s\S]*?)\s*</summary>");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
_summaryCache[metadata.AssetPath] = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary = match.Groups["content"].Value;
|
||||||
|
summary = Regex.Replace(summary, @"^\s*///\s?", string.Empty, RegexOptions.Multiline);
|
||||||
|
summary = Regex.Replace(summary, @"<[^>]+>", string.Empty);
|
||||||
|
summary = Regex.Replace(summary, @"\s+", " ").Trim();
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
McpLog.Warn($"Failed to extract summary description for {type?.FullName}: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_summaryCache[metadata.AssetPath] = summary;
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -421,7 +421,8 @@ namespace MCPForUnity.Editor.Services.Transport.Transports
|
||||||
{
|
{
|
||||||
if (_toolDiscoveryService == null) return;
|
if (_toolDiscoveryService == null) return;
|
||||||
|
|
||||||
var tools = _toolDiscoveryService.DiscoverAllTools();
|
var tools = _toolDiscoveryService.GetEnabledTools();
|
||||||
|
McpLog.Info($"[WebSocket] Preparing to register {tools.Count} tool(s) with the bridge.");
|
||||||
var toolsArray = new JArray();
|
var toolsArray = new JArray();
|
||||||
|
|
||||||
foreach (var tool in tools)
|
foreach (var tool in tools)
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,36 @@ namespace MCPForUnity.Editor.Tools
|
||||||
return handlerInfo.SyncHandler(@params);
|
return handlerInfo.SyncHandler(@params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute a command handler and return its raw result, regardless of sync or async implementation.
|
||||||
|
/// Used internally for features like batch execution where commands need to be composed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="commandName">The registered command to execute.</param>
|
||||||
|
/// <param name="params">Parameters to pass to the command (optional).</param>
|
||||||
|
public static Task<object> InvokeCommandAsync(string commandName, JObject @params)
|
||||||
|
{
|
||||||
|
var handlerInfo = GetHandlerInfo(commandName);
|
||||||
|
var payload = @params ?? new JObject();
|
||||||
|
|
||||||
|
if (handlerInfo.IsAsync)
|
||||||
|
{
|
||||||
|
if (handlerInfo.AsyncHandler == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Async handler for '{commandName}' is not configured correctly");
|
||||||
|
}
|
||||||
|
|
||||||
|
return handlerInfo.AsyncHandler(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handlerInfo.SyncHandler == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Handler for '{commandName}' does not provide a synchronous implementation");
|
||||||
|
}
|
||||||
|
|
||||||
|
object result = handlerInfo.SyncHandler(payload);
|
||||||
|
return Task.FromResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a delegate for an async handler method that returns Task or Task<T>.
|
/// Create a delegate for an async handler method that returns Task or Task<T>.
|
||||||
/// The delegate will invoke the method and await its completion, returning the result.
|
/// The delegate will invoke the method and await its completion, returning the result.
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ using UnityEditor;
|
||||||
namespace MCPForUnity.Editor.Tools
|
namespace MCPForUnity.Editor.Tools
|
||||||
{
|
{
|
||||||
[McpForUnityTool("execute_menu_item", AutoRegister = false)]
|
[McpForUnityTool("execute_menu_item", AutoRegister = false)]
|
||||||
|
/// <summary>
|
||||||
|
/// Tool to execute a Unity Editor menu item by its path.
|
||||||
|
/// </summary>
|
||||||
public static class ExecuteMenuItem
|
public static class ExecuteMenuItem
|
||||||
{
|
{
|
||||||
// Basic blacklist to prevent execution of disruptive menu items.
|
// Basic blacklist to prevent execution of disruptive menu items.
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@ namespace MCPForUnity.Editor.Tools
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles CRUD operations for C# scripts within the Unity project.
|
/// Handles CRUD operations for C# scripts within the Unity project.
|
||||||
///
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
/// ROSLYN INSTALLATION GUIDE:
|
/// ROSLYN INSTALLATION GUIDE:
|
||||||
/// To enable advanced syntax validation with Roslyn compiler services:
|
/// To enable advanced syntax validation with Roslyn compiler services:
|
||||||
///
|
///
|
||||||
|
|
@ -49,7 +50,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
///
|
///
|
||||||
/// Note: Without Roslyn, the system falls back to basic structural validation.
|
/// Note: Without Roslyn, the system falls back to basic structural validation.
|
||||||
/// Roslyn provides full C# compiler diagnostics with line numbers and detailed error messages.
|
/// Roslyn provides full C# compiler diagnostics with line numbers and detailed error messages.
|
||||||
/// </summary>
|
/// </remarks>
|
||||||
[McpForUnityTool("manage_script", AutoRegister = false)]
|
[McpForUnityTool("manage_script", AutoRegister = false)]
|
||||||
public static class ManageScript
|
public static class ManageScript
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ using UnityEngine.SceneManagement;
|
||||||
namespace MCPForUnity.Editor.Tools.Prefabs
|
namespace MCPForUnity.Editor.Tools.Prefabs
|
||||||
{
|
{
|
||||||
[McpForUnityTool("manage_prefabs", AutoRegister = false)]
|
[McpForUnityTool("manage_prefabs", AutoRegister = false)]
|
||||||
|
/// <summary>
|
||||||
|
/// Tool to manage Unity Prefab stages and create prefabs from GameObjects.
|
||||||
|
/// </summary>
|
||||||
public static class ManagePrefabs
|
public static class ManagePrefabs
|
||||||
{
|
{
|
||||||
private const string SupportedActions = "open_stage, close_stage, save_open_stage, create_from_gameobject";
|
private const string SupportedActions = "open_stage, close_stage, save_open_stage, create_from_gameobject";
|
||||||
|
|
|
||||||
|
|
@ -293,6 +293,75 @@
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tools Section */
|
||||||
|
.tool-actions {
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-action-button {
|
||||||
|
flex-grow: 1;
|
||||||
|
min-width: 0;
|
||||||
|
height: 26px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-category-container {
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-item {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.04);
|
||||||
|
border-radius: 4px;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-item-header {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-item-toggle {
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-tags {
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-left: auto;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-top: 2px;
|
||||||
|
background-color: rgba(100, 100, 100, 0.25);
|
||||||
|
border-radius: 3px;
|
||||||
|
color: rgba(40, 40, 40, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-item-description,
|
||||||
|
.tool-parameters {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(120, 120, 120, 1);
|
||||||
|
white-space: normal;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-parameters {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
/* Advanced Settings */
|
/* Advanced Settings */
|
||||||
.advanced-settings-foldout {
|
.advanced-settings-foldout {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
|
@ -400,6 +469,22 @@
|
||||||
border-color: rgba(0, 0, 0, 0.15);
|
border-color: rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.unity-theme-dark .tool-tag {
|
||||||
|
color: rgba(220, 220, 220, 1);
|
||||||
|
background-color: rgba(80, 80, 80, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.unity-theme-dark .tool-item {
|
||||||
|
background-color: rgba(255, 255, 255, 0.04);
|
||||||
|
border-color: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.unity-theme-dark .tool-item-description,
|
||||||
|
.unity-theme-dark .tool-parameters {
|
||||||
|
color: rgba(200, 200, 200, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.unity-theme-light .validation-description {
|
.unity-theme-light .validation-description {
|
||||||
background-color: rgba(100, 150, 200, 0.1);
|
background-color: rgba(100, 150, 200, 0.1);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: a3b463767742cdf43b366f68a656e42e
|
guid: c2f853b1b3974f829a2cc09d52d3d7ad
|
||||||
NativeFormatImporter:
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
mainObjectFileID: 11400000
|
|
||||||
userData:
|
userData:
|
||||||
assetBundleName:
|
assetBundleName:
|
||||||
assetBundleVariant:
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,270 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using MCPForUnity.Editor.Constants;
|
||||||
|
using MCPForUnity.Editor.Helpers;
|
||||||
|
using MCPForUnity.Editor.Services;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine.UIElements;
|
||||||
|
|
||||||
|
namespace MCPForUnity.Editor.Windows.Components.Tools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Controller for the Tools section inside the MCP For Unity editor window.
|
||||||
|
/// Provides discovery, filtering, and per-tool enablement toggles.
|
||||||
|
/// </summary>
|
||||||
|
public class McpToolsSection
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, Toggle> toolToggleMap = new();
|
||||||
|
private Label summaryLabel;
|
||||||
|
private Label noteLabel;
|
||||||
|
private Button enableAllButton;
|
||||||
|
private Button disableAllButton;
|
||||||
|
private Button rescanButton;
|
||||||
|
private VisualElement categoryContainer;
|
||||||
|
private List<ToolMetadata> allTools = new();
|
||||||
|
|
||||||
|
public VisualElement Root { get; }
|
||||||
|
|
||||||
|
public McpToolsSection(VisualElement root)
|
||||||
|
{
|
||||||
|
Root = root;
|
||||||
|
CacheUIElements();
|
||||||
|
RegisterCallbacks();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CacheUIElements()
|
||||||
|
{
|
||||||
|
summaryLabel = Root.Q<Label>("tools-summary");
|
||||||
|
noteLabel = Root.Q<Label>("tools-note");
|
||||||
|
enableAllButton = Root.Q<Button>("enable-all-button");
|
||||||
|
disableAllButton = Root.Q<Button>("disable-all-button");
|
||||||
|
rescanButton = Root.Q<Button>("rescan-button");
|
||||||
|
categoryContainer = Root.Q<VisualElement>("tool-category-container");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterCallbacks()
|
||||||
|
{
|
||||||
|
if (enableAllButton != null)
|
||||||
|
{
|
||||||
|
enableAllButton.AddToClassList("tool-action-button");
|
||||||
|
enableAllButton.style.marginRight = 4;
|
||||||
|
enableAllButton.clicked += () => SetAllToolsState(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disableAllButton != null)
|
||||||
|
{
|
||||||
|
disableAllButton.AddToClassList("tool-action-button");
|
||||||
|
disableAllButton.style.marginRight = 4;
|
||||||
|
disableAllButton.clicked += () => SetAllToolsState(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rescanButton != null)
|
||||||
|
{
|
||||||
|
rescanButton.AddToClassList("tool-action-button");
|
||||||
|
rescanButton.clicked += () =>
|
||||||
|
{
|
||||||
|
McpLog.Info("Rescanning MCP tools from the editor window.");
|
||||||
|
MCPServiceLocator.ToolDiscovery.InvalidateCache();
|
||||||
|
Refresh();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rebuilds the tool list and synchronises toggle states.
|
||||||
|
/// </summary>
|
||||||
|
public void Refresh()
|
||||||
|
{
|
||||||
|
toolToggleMap.Clear();
|
||||||
|
categoryContainer?.Clear();
|
||||||
|
|
||||||
|
var service = MCPServiceLocator.ToolDiscovery;
|
||||||
|
allTools = service.DiscoverAllTools()
|
||||||
|
.OrderBy(tool => IsBuiltIn(tool) ? 0 : 1)
|
||||||
|
.ThenBy(tool => tool.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
bool hasTools = allTools.Count > 0;
|
||||||
|
enableAllButton?.SetEnabled(hasTools);
|
||||||
|
disableAllButton?.SetEnabled(hasTools);
|
||||||
|
|
||||||
|
if (noteLabel != null)
|
||||||
|
{
|
||||||
|
noteLabel.style.display = hasTools ? DisplayStyle.Flex : DisplayStyle.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasTools)
|
||||||
|
{
|
||||||
|
AddInfoLabel("No MCP tools found. Add classes decorated with [McpForUnityTool] to expose tools.");
|
||||||
|
UpdateSummary();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildCategory("Built-in Tools", "built-in", allTools.Where(IsBuiltIn));
|
||||||
|
|
||||||
|
var customTools = allTools.Where(tool => !IsBuiltIn(tool)).ToList();
|
||||||
|
if (customTools.Count > 0)
|
||||||
|
{
|
||||||
|
BuildCategory("Custom Tools", "custom", customTools);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AddInfoLabel("No custom tools detected in loaded assemblies.");
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildCategory(string title, string prefsSuffix, IEnumerable<ToolMetadata> tools)
|
||||||
|
{
|
||||||
|
var toolList = tools.ToList();
|
||||||
|
if (toolList.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var foldout = new Foldout
|
||||||
|
{
|
||||||
|
text = $"{title} ({toolList.Count})",
|
||||||
|
value = EditorPrefs.GetBool(EditorPrefKeys.ToolFoldoutStatePrefix + prefsSuffix, true)
|
||||||
|
};
|
||||||
|
|
||||||
|
foldout.RegisterValueChangedCallback(evt =>
|
||||||
|
{
|
||||||
|
EditorPrefs.SetBool(EditorPrefKeys.ToolFoldoutStatePrefix + prefsSuffix, evt.newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var tool in toolList)
|
||||||
|
{
|
||||||
|
foldout.Add(CreateToolRow(tool));
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryContainer?.Add(foldout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private VisualElement CreateToolRow(ToolMetadata tool)
|
||||||
|
{
|
||||||
|
var row = new VisualElement();
|
||||||
|
row.AddToClassList("tool-item");
|
||||||
|
|
||||||
|
var header = new VisualElement();
|
||||||
|
header.AddToClassList("tool-item-header");
|
||||||
|
|
||||||
|
var toggle = new Toggle(tool.Name)
|
||||||
|
{
|
||||||
|
value = MCPServiceLocator.ToolDiscovery.IsToolEnabled(tool.Name)
|
||||||
|
};
|
||||||
|
toggle.AddToClassList("tool-item-toggle");
|
||||||
|
toggle.tooltip = string.IsNullOrWhiteSpace(tool.Description) ? tool.Name : tool.Description;
|
||||||
|
|
||||||
|
toggle.RegisterValueChangedCallback(evt =>
|
||||||
|
{
|
||||||
|
HandleToggleChange(tool, evt.newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
toolToggleMap[tool.Name] = toggle;
|
||||||
|
header.Add(toggle);
|
||||||
|
|
||||||
|
var tagsContainer = new VisualElement();
|
||||||
|
tagsContainer.AddToClassList("tool-tags");
|
||||||
|
|
||||||
|
bool defaultEnabled = tool.AutoRegister || tool.IsBuiltIn;
|
||||||
|
tagsContainer.Add(CreateTag(defaultEnabled ? "On by default" : "Off by default"));
|
||||||
|
|
||||||
|
tagsContainer.Add(CreateTag(tool.StructuredOutput ? "Structured output" : "Free-form"));
|
||||||
|
|
||||||
|
if (tool.RequiresPolling)
|
||||||
|
{
|
||||||
|
tagsContainer.Add(CreateTag($"Polling: {tool.PollAction}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
header.Add(tagsContainer);
|
||||||
|
row.Add(header);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(tool.Description))
|
||||||
|
{
|
||||||
|
var description = new Label(tool.Description);
|
||||||
|
description.AddToClassList("tool-item-description");
|
||||||
|
row.Add(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tool.Parameters != null && tool.Parameters.Count > 0)
|
||||||
|
{
|
||||||
|
var paramSummary = string.Join(", ", tool.Parameters.Select(p =>
|
||||||
|
$"{p.Name}{(p.Required ? string.Empty : " (optional)")}: {p.Type}"));
|
||||||
|
|
||||||
|
var parametersLabel = new Label(paramSummary);
|
||||||
|
parametersLabel.AddToClassList("tool-parameters");
|
||||||
|
row.Add(parametersLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleToggleChange(ToolMetadata tool, bool enabled, bool updateSummary = true)
|
||||||
|
{
|
||||||
|
MCPServiceLocator.ToolDiscovery.SetToolEnabled(tool.Name, enabled);
|
||||||
|
|
||||||
|
if (updateSummary)
|
||||||
|
{
|
||||||
|
UpdateSummary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetAllToolsState(bool enabled)
|
||||||
|
{
|
||||||
|
foreach (var tool in allTools)
|
||||||
|
{
|
||||||
|
if (!toolToggleMap.TryGetValue(tool.Name, out var toggle))
|
||||||
|
{
|
||||||
|
MCPServiceLocator.ToolDiscovery.SetToolEnabled(tool.Name, enabled);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toggle.value == enabled)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle.SetValueWithoutNotify(enabled);
|
||||||
|
HandleToggleChange(tool, enabled, updateSummary: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSummary()
|
||||||
|
{
|
||||||
|
if (summaryLabel == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allTools.Count == 0)
|
||||||
|
{
|
||||||
|
summaryLabel.text = "No MCP tools discovered.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int enabledCount = allTools.Count(tool => MCPServiceLocator.ToolDiscovery.IsToolEnabled(tool.Name));
|
||||||
|
summaryLabel.text = $"{enabledCount} of {allTools.Count} tools will register with connected clients.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddInfoLabel(string message)
|
||||||
|
{
|
||||||
|
var label = new Label(message);
|
||||||
|
label.AddToClassList("help-text");
|
||||||
|
categoryContainer?.Add(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Label CreateTag(string text)
|
||||||
|
{
|
||||||
|
var tag = new Label(text);
|
||||||
|
tag.AddToClassList("tool-tag");
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsBuiltIn(ToolMetadata tool) => tool?.IsBuiltIn ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c6b3eaf7efb642e89b9b9548458f72d6
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {fileID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
|
||||||
|
<ui:VisualElement name="tools-section" class="section">
|
||||||
|
<ui:Label text="Tools" class="section-title" />
|
||||||
|
<ui:VisualElement class="section-content">
|
||||||
|
<ui:Label name="tools-summary" class="help-text" text="Discovering tools..." />
|
||||||
|
<ui:VisualElement name="tools-actions" class="tool-actions">
|
||||||
|
<ui:Button name="enable-all-button" text="Enable All" class="tool-action-button" />
|
||||||
|
<ui:Button name="disable-all-button" text="Disable All" class="tool-action-button" />
|
||||||
|
<ui:Button name="rescan-button" text="Rescan" class="tool-action-button" />
|
||||||
|
</ui:VisualElement>
|
||||||
|
<ui:Label name="tools-note" class="help-text" text="Changes apply after reconnecting or re-registering tools." />
|
||||||
|
<ui:VisualElement name="tool-category-container" class="tool-category-container" />
|
||||||
|
</ui:VisualElement>
|
||||||
|
</ui:VisualElement>
|
||||||
|
</ui:UXML>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 3934c3a018e9eb540a1b39056c193f71
|
guid: 5a94c7afd72c4dcf9f8a611d85c9a1e4
|
||||||
ScriptedImporter:
|
ScriptedImporter:
|
||||||
internalIDToNameTable: []
|
internalIDToNameTable: []
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
|
|
@ -7,4 +7,4 @@ ScriptedImporter:
|
||||||
userData:
|
userData:
|
||||||
assetBundleName:
|
assetBundleName:
|
||||||
assetBundleVariant:
|
assetBundleVariant:
|
||||||
script: {fileID: 11500000, guid: d68ef794590944f1ea7ee102c91887c7, type: 3}
|
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MCPForUnity.Editor.Helpers;
|
using MCPForUnity.Editor.Helpers;
|
||||||
using MCPForUnity.Editor.Services;
|
using MCPForUnity.Editor.Services;
|
||||||
|
|
@ -7,8 +8,16 @@ using MCPForUnity.Editor.Windows.Components.ClientConfig;
|
||||||
using MCPForUnity.Editor.Windows.Components.Connection;
|
using MCPForUnity.Editor.Windows.Components.Connection;
|
||||||
using MCPForUnity.Editor.Windows.Components.Settings;
|
using MCPForUnity.Editor.Windows.Components.Settings;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
|
using UnityEditor.UIElements;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UIElements;
|
using UnityEngine.UIElements;
|
||||||
|
using MCPForUnity.Editor.Constants;
|
||||||
|
using MCPForUnity.Editor.Helpers;
|
||||||
|
using MCPForUnity.Editor.Services;
|
||||||
|
using MCPForUnity.Editor.Windows.Components.Settings;
|
||||||
|
using MCPForUnity.Editor.Windows.Components.Connection;
|
||||||
|
using MCPForUnity.Editor.Windows.Components.ClientConfig;
|
||||||
|
using MCPForUnity.Editor.Windows.Components.Tools;
|
||||||
|
|
||||||
namespace MCPForUnity.Editor.Windows
|
namespace MCPForUnity.Editor.Windows
|
||||||
{
|
{
|
||||||
|
|
@ -18,12 +27,33 @@ namespace MCPForUnity.Editor.Windows
|
||||||
private McpSettingsSection settingsSection;
|
private McpSettingsSection settingsSection;
|
||||||
private McpConnectionSection connectionSection;
|
private McpConnectionSection connectionSection;
|
||||||
private McpClientConfigSection clientConfigSection;
|
private McpClientConfigSection clientConfigSection;
|
||||||
|
private McpToolsSection toolsSection;
|
||||||
|
|
||||||
|
private ToolbarToggle settingsTabToggle;
|
||||||
|
private ToolbarToggle toolsTabToggle;
|
||||||
|
private VisualElement settingsPanel;
|
||||||
|
private VisualElement toolsPanel;
|
||||||
|
|
||||||
private static readonly HashSet<MCPForUnityEditorWindow> OpenWindows = new();
|
private static readonly HashSet<MCPForUnityEditorWindow> OpenWindows = new();
|
||||||
private bool guiCreated = false;
|
private bool guiCreated = false;
|
||||||
private double lastRefreshTime = 0;
|
private double lastRefreshTime = 0;
|
||||||
private const double RefreshDebounceSeconds = 0.5;
|
private const double RefreshDebounceSeconds = 0.5;
|
||||||
|
|
||||||
|
private enum ActivePanel
|
||||||
|
{
|
||||||
|
Settings,
|
||||||
|
Tools
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void CloseAllWindows()
|
||||||
|
{
|
||||||
|
var windows = OpenWindows.Where(window => window != null).ToArray();
|
||||||
|
foreach (var window in windows)
|
||||||
|
{
|
||||||
|
window.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void ShowWindow()
|
public static void ShowWindow()
|
||||||
{
|
{
|
||||||
var window = GetWindow<MCPForUnityEditorWindow>("MCP For Unity");
|
var window = GetWindow<MCPForUnityEditorWindow>("MCP For Unity");
|
||||||
|
|
@ -98,14 +128,31 @@ namespace MCPForUnity.Editor.Windows
|
||||||
rootVisualElement.styleSheets.Add(commonStyleSheet);
|
rootVisualElement.styleSheets.Add(commonStyleSheet);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get sections container
|
settingsPanel = rootVisualElement.Q<VisualElement>("settings-panel");
|
||||||
var sectionsContainer = rootVisualElement.Q<VisualElement>("sections-container");
|
toolsPanel = rootVisualElement.Q<VisualElement>("tools-panel");
|
||||||
if (sectionsContainer == null)
|
var settingsContainer = rootVisualElement.Q<VisualElement>("settings-container");
|
||||||
|
var toolsContainer = rootVisualElement.Q<VisualElement>("tools-container");
|
||||||
|
|
||||||
|
if (settingsPanel == null || toolsPanel == null)
|
||||||
{
|
{
|
||||||
McpLog.Error("Failed to find sections-container in UXML");
|
McpLog.Error("Failed to find tab panels in UXML");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settingsContainer == null)
|
||||||
|
{
|
||||||
|
McpLog.Error("Failed to find settings-container in UXML");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolsContainer == null)
|
||||||
|
{
|
||||||
|
McpLog.Error("Failed to find tools-container in UXML");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetupTabs();
|
||||||
|
|
||||||
// Load and initialize Settings section
|
// Load and initialize Settings section
|
||||||
var settingsTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
|
var settingsTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
|
||||||
$"{basePath}/Editor/Windows/Components/Settings/McpSettingsSection.uxml"
|
$"{basePath}/Editor/Windows/Components/Settings/McpSettingsSection.uxml"
|
||||||
|
|
@ -113,7 +160,7 @@ namespace MCPForUnity.Editor.Windows
|
||||||
if (settingsTree != null)
|
if (settingsTree != null)
|
||||||
{
|
{
|
||||||
var settingsRoot = settingsTree.Instantiate();
|
var settingsRoot = settingsTree.Instantiate();
|
||||||
sectionsContainer.Add(settingsRoot);
|
settingsContainer.Add(settingsRoot);
|
||||||
settingsSection = new McpSettingsSection(settingsRoot);
|
settingsSection = new McpSettingsSection(settingsRoot);
|
||||||
settingsSection.OnGitUrlChanged += () =>
|
settingsSection.OnGitUrlChanged += () =>
|
||||||
clientConfigSection?.UpdateManualConfiguration();
|
clientConfigSection?.UpdateManualConfiguration();
|
||||||
|
|
@ -128,7 +175,7 @@ namespace MCPForUnity.Editor.Windows
|
||||||
if (connectionTree != null)
|
if (connectionTree != null)
|
||||||
{
|
{
|
||||||
var connectionRoot = connectionTree.Instantiate();
|
var connectionRoot = connectionTree.Instantiate();
|
||||||
sectionsContainer.Add(connectionRoot);
|
settingsContainer.Add(connectionRoot);
|
||||||
connectionSection = new McpConnectionSection(connectionRoot);
|
connectionSection = new McpConnectionSection(connectionRoot);
|
||||||
connectionSection.OnManualConfigUpdateRequested += () =>
|
connectionSection.OnManualConfigUpdateRequested += () =>
|
||||||
clientConfigSection?.UpdateManualConfiguration();
|
clientConfigSection?.UpdateManualConfiguration();
|
||||||
|
|
@ -141,10 +188,25 @@ namespace MCPForUnity.Editor.Windows
|
||||||
if (clientConfigTree != null)
|
if (clientConfigTree != null)
|
||||||
{
|
{
|
||||||
var clientConfigRoot = clientConfigTree.Instantiate();
|
var clientConfigRoot = clientConfigTree.Instantiate();
|
||||||
sectionsContainer.Add(clientConfigRoot);
|
settingsContainer.Add(clientConfigRoot);
|
||||||
clientConfigSection = new McpClientConfigSection(clientConfigRoot);
|
clientConfigSection = new McpClientConfigSection(clientConfigRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load and initialize Tools section
|
||||||
|
var toolsTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
|
||||||
|
$"{basePath}/Editor/Windows/Components/Tools/McpToolsSection.uxml"
|
||||||
|
);
|
||||||
|
if (toolsTree != null)
|
||||||
|
{
|
||||||
|
var toolsRoot = toolsTree.Instantiate();
|
||||||
|
toolsContainer.Add(toolsRoot);
|
||||||
|
toolsSection = new McpToolsSection(toolsRoot);
|
||||||
|
toolsSection.Refresh();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
McpLog.Warn("Failed to load tools section UXML. Tool configuration will be unavailable.");
|
||||||
|
}
|
||||||
guiCreated = true;
|
guiCreated = true;
|
||||||
|
|
||||||
// Initial updates
|
// Initial updates
|
||||||
|
|
@ -202,6 +264,77 @@ namespace MCPForUnity.Editor.Windows
|
||||||
clientConfigSection?.RefreshSelectedClient();
|
clientConfigSection?.RefreshSelectedClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SetupTabs()
|
||||||
|
{
|
||||||
|
settingsTabToggle = rootVisualElement.Q<ToolbarToggle>("settings-tab");
|
||||||
|
toolsTabToggle = rootVisualElement.Q<ToolbarToggle>("tools-tab");
|
||||||
|
|
||||||
|
settingsPanel?.RemoveFromClassList("hidden");
|
||||||
|
toolsPanel?.RemoveFromClassList("hidden");
|
||||||
|
|
||||||
|
if (settingsTabToggle != null)
|
||||||
|
{
|
||||||
|
settingsTabToggle.RegisterValueChangedCallback(evt =>
|
||||||
|
{
|
||||||
|
if (!evt.newValue)
|
||||||
|
{
|
||||||
|
if (toolsTabToggle != null && !toolsTabToggle.value)
|
||||||
|
{
|
||||||
|
settingsTabToggle.SetValueWithoutNotify(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SwitchPanel(ActivePanel.Settings);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolsTabToggle != null)
|
||||||
|
{
|
||||||
|
toolsTabToggle.RegisterValueChangedCallback(evt =>
|
||||||
|
{
|
||||||
|
if (!evt.newValue)
|
||||||
|
{
|
||||||
|
if (settingsTabToggle != null && !settingsTabToggle.value)
|
||||||
|
{
|
||||||
|
toolsTabToggle.SetValueWithoutNotify(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SwitchPanel(ActivePanel.Tools);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var savedPanel = EditorPrefs.GetString(EditorPrefKeys.EditorWindowActivePanel, ActivePanel.Settings.ToString());
|
||||||
|
if (!Enum.TryParse(savedPanel, out ActivePanel initialPanel))
|
||||||
|
{
|
||||||
|
initialPanel = ActivePanel.Settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
SwitchPanel(initialPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SwitchPanel(ActivePanel panel)
|
||||||
|
{
|
||||||
|
bool showSettings = panel == ActivePanel.Settings;
|
||||||
|
|
||||||
|
if (settingsPanel != null)
|
||||||
|
{
|
||||||
|
settingsPanel.style.display = showSettings ? DisplayStyle.Flex : DisplayStyle.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolsPanel != null)
|
||||||
|
{
|
||||||
|
toolsPanel.style.display = showSettings ? DisplayStyle.None : DisplayStyle.Flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsTabToggle?.SetValueWithoutNotify(showSettings);
|
||||||
|
toolsTabToggle?.SetValueWithoutNotify(!showSettings);
|
||||||
|
|
||||||
|
EditorPrefs.SetString(EditorPrefKeys.EditorWindowActivePanel, panel.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
internal static void RequestHealthVerification()
|
internal static void RequestHealthVerification()
|
||||||
{
|
{
|
||||||
foreach (var window in OpenWindows)
|
foreach (var window in OpenWindows)
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,41 @@
|
||||||
/* Root Container */
|
/* Root Layout */
|
||||||
#root-container {
|
#root-container {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
flex-shrink: 1;
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Title */
|
/* Title */
|
||||||
.title {
|
.title {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
-unity-font-style: bold;
|
-unity-font-style: bold;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 16px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
.tab-toolbar {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-toolbar .unity-toolbar-button {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Panels */
|
||||||
|
.panel-scroll {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.section-stack {
|
.section-stack {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,15 @@
|
||||||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
|
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
|
||||||
<ui:ScrollView name="root-container" style="padding: 16px;">
|
<ui:VisualElement name="root-container" class="root-layout">
|
||||||
<ui:Label text="MCP For Unity" name="title" class="title" />
|
<ui:Label text="MCP For Unity" name="title" class="title" />
|
||||||
<ui:VisualElement name="sections-container" class="section-stack" />
|
<uie:Toolbar name="tab-toolbar" class="tab-toolbar">
|
||||||
</ui:ScrollView>
|
<uie:ToolbarToggle name="settings-tab" text="Settings" value="true" />
|
||||||
|
<uie:ToolbarToggle name="tools-tab" text="Tools" />
|
||||||
|
</uie:Toolbar>
|
||||||
|
<ui:ScrollView name="settings-panel" class="panel-scroll" style="flex-grow: 1;">
|
||||||
|
<ui:VisualElement name="settings-container" class="section-stack" />
|
||||||
|
</ui:ScrollView>
|
||||||
|
<ui:ScrollView name="tools-panel" class="panel-scroll hidden" style="flex-grow: 1;">
|
||||||
|
<ui:VisualElement name="tools-container" class="section-stack" />
|
||||||
|
</ui:ScrollView>
|
||||||
|
</ui:VisualElement>
|
||||||
</ui:UXML>
|
</ui:UXML>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
"""Defines the batch_execute tool for orchestrating multiple Unity MCP commands."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Annotated, Any
|
||||||
|
|
||||||
|
from fastmcp import Context
|
||||||
|
|
||||||
|
from services.registry import mcp_for_unity_tool
|
||||||
|
from services.tools import get_unity_instance_from_context
|
||||||
|
from transport.unity_transport import send_with_unity_instance
|
||||||
|
from transport.legacy.unity_connection import async_send_command_with_retry
|
||||||
|
|
||||||
|
MAX_COMMANDS_PER_BATCH = 25
|
||||||
|
|
||||||
|
|
||||||
|
@mcp_for_unity_tool(
|
||||||
|
name="batch_execute",
|
||||||
|
description=(
|
||||||
|
"Runs a list of MCP tool calls as one batch. Use it to send a full sequence of commands, "
|
||||||
|
"inspect the results, then submit the next batch for the following step."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def batch_execute(
|
||||||
|
ctx: Context,
|
||||||
|
commands: Annotated[list[dict[str, Any]], "List of commands with 'tool' and 'params' keys."],
|
||||||
|
parallel: Annotated[bool | None, "Attempt to run read-only commands in parallel"] = None,
|
||||||
|
fail_fast: Annotated[bool | None, "Stop processing after the first failure"] = None,
|
||||||
|
max_parallelism: Annotated[int | None, "Hint for the maximum number of parallel workers"] = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Proxy the batch_execute tool to the Unity Editor transporter."""
|
||||||
|
unity_instance = get_unity_instance_from_context(ctx)
|
||||||
|
|
||||||
|
if not isinstance(commands, list) or not commands:
|
||||||
|
raise ValueError("'commands' must be a non-empty list of command specifications")
|
||||||
|
|
||||||
|
if len(commands) > MAX_COMMANDS_PER_BATCH:
|
||||||
|
raise ValueError(
|
||||||
|
f"batch_execute currently supports up to {MAX_COMMANDS_PER_BATCH} commands; received {len(commands)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
normalized_commands: list[dict[str, Any]] = []
|
||||||
|
for index, command in enumerate(commands):
|
||||||
|
if not isinstance(command, dict):
|
||||||
|
raise ValueError(f"Command at index {index} must be an object with 'tool' and 'params' keys")
|
||||||
|
|
||||||
|
tool_name = command.get("tool")
|
||||||
|
params = command.get("params", {})
|
||||||
|
|
||||||
|
if not tool_name or not isinstance(tool_name, str):
|
||||||
|
raise ValueError(f"Command at index {index} is missing a valid 'tool' name")
|
||||||
|
|
||||||
|
if params is None:
|
||||||
|
params = {}
|
||||||
|
if not isinstance(params, dict):
|
||||||
|
raise ValueError(f"Command '{tool_name}' must specify parameters as an object/dict")
|
||||||
|
|
||||||
|
normalized_commands.append({
|
||||||
|
"tool": tool_name,
|
||||||
|
"params": params,
|
||||||
|
})
|
||||||
|
|
||||||
|
payload: dict[str, Any] = {
|
||||||
|
"commands": normalized_commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
if parallel is not None:
|
||||||
|
payload["parallel"] = bool(parallel)
|
||||||
|
if fail_fast is not None:
|
||||||
|
payload["failFast"] = bool(fail_fast)
|
||||||
|
if max_parallelism is not None:
|
||||||
|
payload["maxParallelism"] = int(max_parallelism)
|
||||||
|
|
||||||
|
return await send_with_unity_instance(
|
||||||
|
async_send_command_with_retry,
|
||||||
|
unity_instance,
|
||||||
|
"batch_execute",
|
||||||
|
payload,
|
||||||
|
)
|
||||||
Loading…
Reference in New Issue