chore(unity): 固定Unity CLI Loop本地包并移除UnitySkills依赖
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dcc9f571c2a194f2785077de39c4f31c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a66a2fc90232147fda7ad7d724b6ba38
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Exception thrown when tool parameter JSON fails type validation.
|
||||
/// </summary>
|
||||
public class ParameterValidationException : Exception
|
||||
{
|
||||
public ParameterValidationException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public ParameterValidationException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7bc2a292b0560427bb23904f40141e37
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,447 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Current client context for JSON-RPC processing
|
||||
/// </summary>
|
||||
public class ClientExecutionContext
|
||||
{
|
||||
public string Endpoint { get; }
|
||||
|
||||
public ClientExecutionContext(string endpoint)
|
||||
{
|
||||
Endpoint = endpoint;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class specialized in handling JSON-RPC 2.0 processing
|
||||
///
|
||||
/// Design document reference: Packages/src/Editor/ARCHITECTURE.md
|
||||
///
|
||||
/// Related classes:
|
||||
/// - UnityApiHandler: Executes Unity commands based on JSON-RPC requests
|
||||
/// - McpBridgeServer: TCP server that receives JSON-RPC messages from TypeScript
|
||||
/// - MainThreadSwitcher: Ensures Unity API calls run on the main thread
|
||||
/// - JsonRpcRequest: Request model for JSON-RPC 2.0 protocol
|
||||
/// - ClientExecutionContext: Thread-local context for tracking current client
|
||||
///
|
||||
/// Processing flow:
|
||||
/// 1. Receives JSON message from McpBridgeServer
|
||||
/// 2. Parses and validates JSON-RPC 2.0 format
|
||||
/// 3. Sets client context for the current thread
|
||||
/// 4. Delegates to UnityApiHandler for command execution
|
||||
/// 5. Formats response according to JSON-RPC 2.0 specification
|
||||
/// 6. Returns JSON response to be sent back to client
|
||||
/// </summary>
|
||||
public static class JsonRpcProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Current client context for async operations
|
||||
/// </summary>
|
||||
private static readonly AsyncLocal<ClientExecutionContext> _currentClientContext = new();
|
||||
|
||||
/// <summary>
|
||||
/// Get current client context (ProcessID and Endpoint)
|
||||
/// </summary>
|
||||
public static ClientExecutionContext CurrentClientContext => _currentClientContext.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Process JSON-RPC request and generate response with client context
|
||||
/// </summary>
|
||||
public static async Task<string> ProcessRequest(string jsonRequest, string clientEndpoint)
|
||||
{
|
||||
var context = new ClientExecutionContext(clientEndpoint);
|
||||
_currentClientContext.Value = context;
|
||||
|
||||
try
|
||||
{
|
||||
return await ProcessRequest(jsonRequest);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_currentClientContext.Value = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process JSON-RPC request and generate response
|
||||
/// </summary>
|
||||
public static async Task<string> ProcessRequest(string jsonRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
JsonRpcRequest request = ParseRequest(jsonRequest);
|
||||
|
||||
if (request.IsNotification)
|
||||
{
|
||||
ProcessNotification(request);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await ProcessRpcRequest(request, jsonRequest);
|
||||
}
|
||||
catch (JsonReaderException ex)
|
||||
{
|
||||
return CreateErrorResponse(null, ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return CreateErrorResponse(null, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse JSON-RPC request string into structured object
|
||||
/// </summary>
|
||||
private static JsonRpcRequest ParseRequest(string jsonRequest)
|
||||
{
|
||||
JObject request = JObject.Parse(jsonRequest);
|
||||
return new JsonRpcRequest
|
||||
{
|
||||
Method = request["method"]?.ToString(),
|
||||
Params = request["params"],
|
||||
Id = request["id"]?.ToObject<object>(),
|
||||
UloopMetadata = ParseUloopMetadata(request["x-uloop"])
|
||||
};
|
||||
}
|
||||
|
||||
private static JsonRpcRequestUloopMetadata ParseUloopMetadata(JToken metadataToken)
|
||||
{
|
||||
if (metadataToken == null || metadataToken.Type == JTokenType.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new JsonRpcRequestUloopMetadata
|
||||
{
|
||||
ExpectedProjectRoot = metadataToken["expectedProjectRoot"]?.ToString(),
|
||||
ExpectedServerSessionId = metadataToken["expectedServerSessionId"]?.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process notification (fire-and-forget)
|
||||
///
|
||||
/// Note: Notifications are one-way messages without response.
|
||||
/// Currently handles:
|
||||
/// - focus-window: Brings Unity Editor window to foreground (used after request timeout)
|
||||
/// </summary>
|
||||
private static void ProcessNotification(JsonRpcRequest request)
|
||||
{
|
||||
if (string.IsNullOrEmpty(request.Method))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (request.Method)
|
||||
{
|
||||
case "focus-window":
|
||||
HandleFocusWindowNotification();
|
||||
break;
|
||||
default:
|
||||
// Unknown notification - ignore silently
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle focus-window notification.
|
||||
/// Note: focus-window is now handled at OS level by TypeScript MCP server using launch-unity package.
|
||||
/// This notification handler remains for protocol compatibility but does nothing.
|
||||
/// </summary>
|
||||
private static void HandleFocusWindowNotification()
|
||||
{
|
||||
// Intentionally empty - focus-window is handled by TypeScript MCP server at OS level
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process RPC request and return response JSON
|
||||
/// </summary>
|
||||
private static async Task<string> ProcessRpcRequest(JsonRpcRequest request, string originalJson)
|
||||
{
|
||||
Stopwatch requestStopwatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
ValidateClientIdentityIfNeeded(request);
|
||||
|
||||
Stopwatch mainThreadWaitStopwatch = Stopwatch.StartNew();
|
||||
await MainThreadSwitcher.SwitchToMainThread();
|
||||
mainThreadWaitStopwatch.Stop();
|
||||
|
||||
Stopwatch toolStopwatch = Stopwatch.StartNew();
|
||||
BaseToolResponse result = await ExecuteMethod(request.Method, request.Params);
|
||||
toolStopwatch.Stop();
|
||||
|
||||
AppendExecuteDynamicCodeTimingsIfSupported(
|
||||
result,
|
||||
mainThreadWaitStopwatch.Elapsed.TotalMilliseconds,
|
||||
toolStopwatch.Elapsed.TotalMilliseconds,
|
||||
requestStopwatch.Elapsed.TotalMilliseconds);
|
||||
string response = CreateSuccessResponse(request.Id, result);
|
||||
return response;
|
||||
}
|
||||
catch (JsonSerializationException ex)
|
||||
{
|
||||
UnityEngine.Debug.LogError($"[JsonRpcProcessor] JSON serialization error: {ex.Message}\nStack trace: {ex.StackTrace}");
|
||||
return CreateErrorResponse(request.Id, ex);
|
||||
}
|
||||
catch (ParameterValidationException ex)
|
||||
{
|
||||
LogParameterValidationException(ex);
|
||||
return CreateErrorResponse(request.Id, ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UnityEngine.Debug.LogError($"[JsonRpcProcessor] Error: {ex.Message}\nStack trace: {ex.StackTrace}");
|
||||
return CreateErrorResponse(request.Id, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateClientIdentityIfNeeded(JsonRpcRequest request)
|
||||
{
|
||||
JsonRpcRequestIdentityValidator.Validate(
|
||||
request?.UloopMetadata,
|
||||
McpEditorSettings.GetProjectRootPath(),
|
||||
McpEditorSettings.GetServerSessionId());
|
||||
}
|
||||
|
||||
private static void LogParameterValidationException(ParameterValidationException exception)
|
||||
{
|
||||
if (JsonRpcRequestIdentityValidator.IsExpectedRetryableFailure(exception))
|
||||
{
|
||||
#if ULOOPMCP_DEBUG
|
||||
UnityEngine.Debug.LogWarning(
|
||||
$"[JsonRpcProcessor] Expected retryable parameter validation event: {exception.Message}");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
UnityEngine.Debug.LogError(
|
||||
$"[JsonRpcProcessor] Parameter validation error: {exception.Message}\nStack trace: {exception.StackTrace}");
|
||||
}
|
||||
|
||||
private static void AppendExecuteDynamicCodeTimingsIfSupported(
|
||||
BaseToolResponse result,
|
||||
double mainThreadWaitMilliseconds,
|
||||
double toolTotalMilliseconds,
|
||||
double requestTotalMilliseconds)
|
||||
{
|
||||
if (result is not ExecuteDynamicCodeResponse executeDynamicCodeResponse)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ExecuteDynamicCodeResponseTimingAugmenter.AppendTimingEntries(
|
||||
executeDynamicCodeResponse,
|
||||
mainThreadWaitMilliseconds,
|
||||
toolTotalMilliseconds,
|
||||
requestTotalMilliseconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create JSON-RPC success response
|
||||
/// </summary>
|
||||
/// <param name="id">Request ID - must be same type as received (string/number/null per JSON-RPC spec)</param>
|
||||
/// <param name="result">Command execution result</param>
|
||||
private static string CreateSuccessResponse(object id, BaseToolResponse result)
|
||||
{
|
||||
JsonSerializerSettings settings = new JsonSerializerSettings
|
||||
{
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||
MaxDepth = McpServerConfig.DEFAULT_JSON_MAX_DEPTH
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
JsonRpcSuccessResponse response = new JsonRpcSuccessResponse(
|
||||
McpServerConfig.JSONRPC_VERSION,
|
||||
id,
|
||||
result
|
||||
);
|
||||
return JsonConvert.SerializeObject(response, Formatting.None, settings);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Return safe fallback response for any serialization errors
|
||||
object fallbackResult = new
|
||||
{
|
||||
error = "Serialization failed - returning safe fallback",
|
||||
commandType = result != null ? result.GetType().Name : "unknown"
|
||||
};
|
||||
|
||||
JsonRpcSuccessResponse fallbackResponse = new JsonRpcSuccessResponse(
|
||||
McpServerConfig.JSONRPC_VERSION,
|
||||
id,
|
||||
fallbackResult
|
||||
);
|
||||
return JsonConvert.SerializeObject(fallbackResponse, Formatting.None);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create JSON-RPC error response
|
||||
/// </summary>
|
||||
/// <param name="id">Request ID - must be same type as received (string/number/null per JSON-RPC spec)</param>
|
||||
/// <param name="ex">Exception to convert to error response</param>
|
||||
private static string CreateErrorResponse(object id, Exception ex)
|
||||
{
|
||||
// Centralize exception -> user-facing message via UserFriendlyErrorConverter
|
||||
UserFriendlyErrorConverter handler = new UserFriendlyErrorConverter();
|
||||
UserFriendlyErrorDto exceptionResponse = handler.ProcessException(ex);
|
||||
|
||||
// Map UserFriendlyErrorDto to JsonRpcError
|
||||
string errorMessage = exceptionResponse.FriendlyMessage;
|
||||
|
||||
JsonRpcErrorData errorData;
|
||||
if (ex is McpSecurityException secEx)
|
||||
{
|
||||
errorData = new SecurityBlockedErrorData(secEx.ToolName, secEx.SecurityReason, exceptionResponse.Explanation ?? ex.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
errorData = new InternalErrorData(exceptionResponse.Explanation ?? ex.Message);
|
||||
}
|
||||
|
||||
JsonRpcErrorResponse errorResponse = new JsonRpcErrorResponse(
|
||||
McpServerConfig.JSONRPC_VERSION,
|
||||
id,
|
||||
new JsonRpcError(McpServerConfig.INTERNAL_ERROR_CODE, errorMessage, errorData)
|
||||
);
|
||||
|
||||
JsonSerializerSettings settings = new JsonSerializerSettings
|
||||
{
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||
MaxDepth = McpServerConfig.DEFAULT_JSON_MAX_DEPTH
|
||||
};
|
||||
|
||||
return JsonConvert.SerializeObject(errorResponse, Formatting.None, settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute appropriate handler according to method name
|
||||
/// Use new command-based structure
|
||||
/// </summary>
|
||||
private static async Task<BaseToolResponse> ExecuteMethod(string method, JToken paramsToken)
|
||||
{
|
||||
return await UnityApiHandler.ExecuteCommandAsync(method, paramsToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for JSON-RPC error types
|
||||
/// </summary>
|
||||
public static class JsonRpcErrorTypes
|
||||
{
|
||||
public const string SecurityBlocked = "security_blocked";
|
||||
public const string InternalError = "internal_error";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for JSON-RPC error data
|
||||
/// </summary>
|
||||
public abstract class JsonRpcErrorData
|
||||
{
|
||||
public abstract string type { get; }
|
||||
|
||||
public string message { get; protected set; }
|
||||
|
||||
protected JsonRpcErrorData(string message)
|
||||
{
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Error data for security blocked commands
|
||||
/// </summary>
|
||||
public class SecurityBlockedErrorData : JsonRpcErrorData
|
||||
{
|
||||
public override string type => JsonRpcErrorTypes.SecurityBlocked;
|
||||
|
||||
public string command { get; }
|
||||
|
||||
public string reason { get; }
|
||||
|
||||
public SecurityBlockedErrorData(string command, string reason, string message) : base(message)
|
||||
{
|
||||
this.command = command;
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Error data for internal errors
|
||||
/// </summary>
|
||||
public class InternalErrorData : JsonRpcErrorData
|
||||
{
|
||||
public override string type => JsonRpcErrorTypes.InternalError;
|
||||
|
||||
public InternalErrorData(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON-RPC error object
|
||||
/// </summary>
|
||||
public class JsonRpcError
|
||||
{
|
||||
public int code { get; }
|
||||
|
||||
public string message { get; }
|
||||
|
||||
public JsonRpcErrorData data { get; }
|
||||
|
||||
public JsonRpcError(int code, string message, JsonRpcErrorData data)
|
||||
{
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON-RPC success response
|
||||
/// </summary>
|
||||
public class JsonRpcSuccessResponse
|
||||
{
|
||||
public string jsonrpc { get; }
|
||||
|
||||
public object id { get; }
|
||||
|
||||
public object result { get; }
|
||||
|
||||
public JsonRpcSuccessResponse(string jsonRpc, object id, object result)
|
||||
{
|
||||
this.jsonrpc = jsonRpc;
|
||||
this.id = id;
|
||||
this.result = result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON-RPC error response
|
||||
/// </summary>
|
||||
public class JsonRpcErrorResponse
|
||||
{
|
||||
public string jsonrpc { get; }
|
||||
|
||||
public object id { get; }
|
||||
|
||||
public JsonRpcError error { get; }
|
||||
|
||||
public JsonRpcErrorResponse(string jsonRpc, object id, JsonRpcError error)
|
||||
{
|
||||
this.jsonrpc = jsonRpc;
|
||||
this.id = id;
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3c8feac02c96413ba4dde4ba5990ef8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,37 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a parsed JSON-RPC request
|
||||
/// </summary>
|
||||
internal class JsonRpcRequest
|
||||
{
|
||||
public string Method { get; set; }
|
||||
/// <summary>
|
||||
/// JSON-RPC 2.0 spec allows params to be object, array, or null.
|
||||
/// We use JToken to accept any format, then convert to strongly-typed
|
||||
/// schema classes (e.g. PingSchema) in AbstractUnityCommand.ConvertToSchema.
|
||||
/// This provides flexibility at the protocol layer and type safety at the command layer.
|
||||
/// </summary>
|
||||
public JToken Params { get; set; }
|
||||
/// <summary>
|
||||
/// JSON-RPC 2.0 spec requires id type to match the request.
|
||||
/// Must be string, number, or null - same as received.
|
||||
/// </summary>
|
||||
public object Id { get; set; }
|
||||
public JsonRpcRequestUloopMetadata UloopMetadata { get; set; }
|
||||
/// <summary>
|
||||
/// JSON-RPC 2.0 notification flag. True when id is null/missing.
|
||||
/// Notifications are fire-and-forget messages that don't expect a response.
|
||||
/// Regular requests (with id) expect a response, notifications do not.
|
||||
/// </summary>
|
||||
public bool IsNotification => Id == null;
|
||||
}
|
||||
|
||||
internal class JsonRpcRequestUloopMetadata
|
||||
{
|
||||
public string ExpectedProjectRoot { get; set; }
|
||||
public string ExpectedServerSessionId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3307d57ac9144e00842cc0e4a068b06
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal static class JsonRpcRequestIdentityValidator
|
||||
{
|
||||
internal const string ServerSessionChangedMessage = "Unity CLI Loop server session changed. Retry the command.";
|
||||
|
||||
public static void Validate(
|
||||
JsonRpcRequestUloopMetadata metadata,
|
||||
string actualProjectRoot,
|
||||
string actualServerSessionId)
|
||||
{
|
||||
if (metadata == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(metadata.ExpectedProjectRoot))
|
||||
{
|
||||
throw new ParameterValidationException("Invalid x-uloop metadata: expectedProjectRoot is required.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(metadata.ExpectedServerSessionId))
|
||||
{
|
||||
throw new ParameterValidationException("Invalid x-uloop metadata: expectedServerSessionId is required.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(actualProjectRoot))
|
||||
{
|
||||
throw new ParameterValidationException("Fast project validation is unavailable. Restart Unity CLI Loop and retry.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(actualServerSessionId))
|
||||
{
|
||||
throw new ParameterValidationException(ServerSessionChangedMessage);
|
||||
}
|
||||
|
||||
if (!string.Equals(metadata.ExpectedProjectRoot, actualProjectRoot, StringComparison.Ordinal))
|
||||
{
|
||||
throw new ParameterValidationException("Connected Unity instance belongs to a different project.");
|
||||
}
|
||||
|
||||
if (!string.Equals(metadata.ExpectedServerSessionId, actualServerSessionId, StringComparison.Ordinal))
|
||||
{
|
||||
throw new ParameterValidationException(ServerSessionChangedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsExpectedRetryableFailure(ParameterValidationException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return string.Equals(
|
||||
exception.Message,
|
||||
ServerSessionChangedMessage,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 105ea7a6425e04852b1f41d84dabc8ec
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 66830065e21e249c0876bf2ef4f0776b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1511443bffd3044d8a38e727e688c81f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Response schema for ClearConsole command
|
||||
/// Provides type-safe response structure for console clearing operation
|
||||
/// </summary>
|
||||
public class ClearConsoleResponse : BaseToolResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the console clear operation was successful
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of logs that were cleared from the console
|
||||
/// </summary>
|
||||
public int ClearedLogCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Breakdown of cleared logs by type
|
||||
/// </summary>
|
||||
public ClearedLogCounts ClearedCounts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Message describing the clear operation result
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message if the operation failed
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new ClearConsoleResponse for successful operation
|
||||
/// </summary>
|
||||
public ClearConsoleResponse(bool success, int clearedLogCount, ClearedLogCounts clearedCounts, string message)
|
||||
{
|
||||
Success = success;
|
||||
ClearedLogCount = clearedLogCount;
|
||||
ClearedCounts = clearedCounts ?? new ClearedLogCounts();
|
||||
Message = message ?? string.Empty;
|
||||
ErrorMessage = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new ClearConsoleResponse for failed operation
|
||||
/// </summary>
|
||||
public ClearConsoleResponse(string errorMessage)
|
||||
{
|
||||
Success = false;
|
||||
ClearedLogCount = 0;
|
||||
ClearedCounts = new ClearedLogCounts();
|
||||
Message = string.Empty;
|
||||
ErrorMessage = errorMessage ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameterless constructor for JSON deserialization
|
||||
/// </summary>
|
||||
public ClearConsoleResponse()
|
||||
{
|
||||
ClearedCounts = new ClearedLogCounts();
|
||||
Message = string.Empty;
|
||||
ErrorMessage = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Breakdown of cleared logs by type
|
||||
/// </summary>
|
||||
public class ClearedLogCounts
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of error logs that were cleared
|
||||
/// </summary>
|
||||
public int ErrorCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of warning logs that were cleared
|
||||
/// </summary>
|
||||
public int WarningCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of info logs that were cleared
|
||||
/// </summary>
|
||||
public int LogCount { get; set; }
|
||||
|
||||
public ClearedLogCounts()
|
||||
{
|
||||
}
|
||||
|
||||
public ClearedLogCounts(int errorCount, int warningCount, int logCount)
|
||||
{
|
||||
ErrorCount = errorCount;
|
||||
WarningCount = warningCount;
|
||||
LogCount = logCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dbbf8d96e00c54171b03c9c0d42822dc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Schema for ClearConsole command parameters
|
||||
/// Provides type-safe parameter access for clearing Unity Console
|
||||
/// Related classes:
|
||||
/// - ConsoleUtility: Service layer for console operations
|
||||
/// - ClearConsoleResponse: Type-safe response structure
|
||||
/// - ClearConsoleCommand: Command implementation
|
||||
/// </summary>
|
||||
public class ClearConsoleSchema : BaseToolSchema
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to add a confirmation log message after clearing
|
||||
/// </summary>
|
||||
[Description("Whether to add a confirmation log message after clearing")]
|
||||
public bool AddConfirmationMessage { get; set; } = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 94a230c702f0e4d75b03dc706886b935
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// ClearConsole tool handler - Type-safe implementation using Schema and Response
|
||||
/// Clears Unity console logs for clean development workflow
|
||||
/// Related classes: ClearConsoleUseCase, ConsoleUtility, ClearConsoleSchema, ClearConsoleResponse
|
||||
/// </summary>
|
||||
[McpTool(Description = "Clear Unity console logs")]
|
||||
public class ClearConsoleTool : AbstractUnityTool<ClearConsoleSchema, ClearConsoleResponse>
|
||||
{
|
||||
public override string ToolName => "clear-console";
|
||||
|
||||
/// <summary>
|
||||
/// Execute console clear tool
|
||||
/// </summary>
|
||||
/// <param name="parameters">Type-safe parameters</param>
|
||||
/// <param name="cancellationToken">Cancellation token for timeout control</param>
|
||||
/// <returns>Clear operation result</returns>
|
||||
protected override async Task<ClearConsoleResponse> ExecuteAsync(ClearConsoleSchema parameters, CancellationToken cancellationToken)
|
||||
{
|
||||
// Create and execute ClearConsoleUseCase instance
|
||||
ClearConsoleUseCase useCase = new();
|
||||
return await useCase.ExecuteAsync(parameters, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b72387c853bad490fb5cb20f551f0781
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fc3fddf04059a4303be8a7f27aa53052
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
name: uloop-clear-console
|
||||
description: "Clear Unity Console entries. Use before compile, tests, or debugging when stale logs would hide the current result."
|
||||
---
|
||||
|
||||
# uloop clear-console
|
||||
|
||||
Clear Unity console logs.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
uloop clear-console [--add-confirmation-message]
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `--add-confirmation-message` | boolean | `false` | Add confirmation message after clearing |
|
||||
|
||||
## Global Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--project-path <path>` | Optional. Use only when the target Unity project is not the current directory. |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Clear console
|
||||
uloop clear-console
|
||||
|
||||
# Clear with confirmation
|
||||
uloop clear-console --add-confirmation-message
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Returns JSON with:
|
||||
- `Success` (boolean): Whether the clear operation succeeded
|
||||
- `ClearedLogCount` (number): Total number of log entries that were cleared
|
||||
- `ClearedCounts` (object): Breakdown by log type
|
||||
- `ErrorCount` (number): Errors cleared
|
||||
- `WarningCount` (number): Warnings cleared
|
||||
- `LogCount` (number): Info logs cleared
|
||||
- `Message` (string): Description of the result; carries the failure summary when the operation fails (e.g. `"Failed to clear console: ..."`)
|
||||
- `ErrorMessage` (string): Currently always empty for this tool — read `Message` for failure details
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f1d3cbc9cba645b398b9f4bbe9537c4
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 90328140d69f84f1e925caf7f710c0eb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents information about a Unity component
|
||||
/// </summary>
|
||||
public class ComponentInfo
|
||||
{
|
||||
public string type { get; set; }
|
||||
public string fullTypeName { get; set; }
|
||||
public ComponentPropertyInfo[] properties { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a property of a Unity component
|
||||
/// </summary>
|
||||
public class ComponentPropertyInfo
|
||||
{
|
||||
public string name { get; set; }
|
||||
public string type { get; set; }
|
||||
public object value { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8f54071c76614440b5775885e181d95
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a8a3c04e7ca654807956364da02c4e85
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,208 @@
|
||||
#nullable enable
|
||||
#if ULOOPMCP_HAS_INPUT_SYSTEM
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
// Shared helper for applying Input System state changes at the correct update phase.
|
||||
// Both keyboard and mouse simulation need frame-precise timing so that
|
||||
// wasPressedThisFrame / wasReleasedThisFrame detect the injected state.
|
||||
internal static class InputSystemUpdateHelper
|
||||
{
|
||||
public static Task ApplyOnNextConfiguredUpdate(Action apply, CancellationToken ct)
|
||||
{
|
||||
InputUpdateType targetUpdateType = InputUpdateTypeResolver.Resolve();
|
||||
if (InputUpdateTypeResolver.RequiresExplicitUpdate())
|
||||
{
|
||||
return ApplyOnExplicitUpdate(apply, targetUpdateType, ct);
|
||||
}
|
||||
|
||||
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
|
||||
CancellationTokenRegistration registration = default;
|
||||
Action? callback = null;
|
||||
|
||||
callback = () =>
|
||||
{
|
||||
InputUpdateType currentUpdateType = InputState.currentUpdateType;
|
||||
if (!InputUpdateTypeResolver.IsMatch(currentUpdateType, targetUpdateType))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Assert(callback != null, "callback must be assigned before subscription");
|
||||
InputSystem.onBeforeUpdate -= callback;
|
||||
registration.Dispose();
|
||||
apply();
|
||||
tcs.TrySetResult(true);
|
||||
};
|
||||
|
||||
InputSystem.onBeforeUpdate += callback;
|
||||
if (ct.CanBeCanceled)
|
||||
{
|
||||
registration = ct.Register(() =>
|
||||
{
|
||||
Debug.Assert(callback != null, "callback must be assigned before cancellation");
|
||||
InputSystem.onBeforeUpdate -= callback;
|
||||
tcs.TrySetCanceled(ct);
|
||||
});
|
||||
}
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
public static int GetMinimumObservationFrameCount()
|
||||
{
|
||||
if (!InputUpdateTypeResolver.RequiresExplicitUpdate())
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
InputUpdateType targetUpdateType = InputUpdateTypeResolver.Resolve();
|
||||
if (targetUpdateType != InputUpdateType.Manual)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Manual-mode projects often call InputSystem.Update from their own Update loop,
|
||||
// so zero-duration taps need one extra frame to remain visible to gameplay code.
|
||||
return 3;
|
||||
}
|
||||
|
||||
public static async Task WaitForObservationFrames(CancellationToken ct)
|
||||
{
|
||||
await WaitForRuntimeFrames(GetMinimumObservationFrameCount(), ct);
|
||||
}
|
||||
|
||||
public static async Task WaitForPressLifetime(float duration, CancellationToken ct)
|
||||
{
|
||||
int minimumObservationFrames = GetMinimumObservationFrameCount();
|
||||
int startFrameCount = Time.frameCount;
|
||||
float startTime = Time.realtimeSinceStartup;
|
||||
float elapsed = 0f;
|
||||
int observedFrames = 0;
|
||||
|
||||
while (observedFrames < minimumObservationFrames || elapsed < duration)
|
||||
{
|
||||
await EditorDelay.DelayFrame(1, ct);
|
||||
observedFrames = Time.frameCount - startFrameCount;
|
||||
elapsed = Time.realtimeSinceStartup - startTime;
|
||||
}
|
||||
}
|
||||
|
||||
public static void RunExplicitUpdate(InputUpdateType targetUpdateType)
|
||||
{
|
||||
InputSettings? settings = InputSystem.settings;
|
||||
if (settings == null)
|
||||
{
|
||||
InputSystem.Update();
|
||||
return;
|
||||
}
|
||||
|
||||
InputSettings.UpdateMode originalUpdateMode = settings.updateMode;
|
||||
InputSettings.UpdateMode targetUpdateMode = GetExplicitUpdateMode(targetUpdateType, originalUpdateMode);
|
||||
if (targetUpdateMode == originalUpdateMode)
|
||||
{
|
||||
InputSystem.Update();
|
||||
return;
|
||||
}
|
||||
|
||||
settings.updateMode = targetUpdateMode;
|
||||
try
|
||||
{
|
||||
InputSystem.Update();
|
||||
}
|
||||
finally
|
||||
{
|
||||
settings.updateMode = originalUpdateMode;
|
||||
}
|
||||
}
|
||||
|
||||
private static Task ApplyOnExplicitUpdate(Action apply, InputUpdateType targetUpdateType, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
|
||||
CancellationTokenRegistration registration = default;
|
||||
Action? callback = null;
|
||||
|
||||
callback = () =>
|
||||
{
|
||||
InputUpdateType currentUpdateType = InputState.currentUpdateType;
|
||||
if (!InputUpdateTypeResolver.IsMatch(currentUpdateType, targetUpdateType))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Assert(callback != null, "callback must be assigned before subscription");
|
||||
InputSystem.onBeforeUpdate -= callback;
|
||||
registration.Dispose();
|
||||
apply();
|
||||
tcs.TrySetResult(true);
|
||||
};
|
||||
|
||||
InputSystem.onBeforeUpdate += callback;
|
||||
if (ct.CanBeCanceled)
|
||||
{
|
||||
registration = ct.Register(() =>
|
||||
{
|
||||
Debug.Assert(callback != null, "callback must be assigned before cancellation");
|
||||
InputSystem.onBeforeUpdate -= callback;
|
||||
tcs.TrySetCanceled(ct);
|
||||
});
|
||||
}
|
||||
|
||||
RunExplicitUpdate(targetUpdateType);
|
||||
if (!tcs.Task.IsCompleted)
|
||||
{
|
||||
Debug.Assert(callback != null, "callback must be assigned before explicit update fallback");
|
||||
InputSystem.onBeforeUpdate -= callback;
|
||||
registration.Dispose();
|
||||
apply();
|
||||
RunExplicitUpdate(targetUpdateType);
|
||||
tcs.TrySetResult(true);
|
||||
}
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private static InputSettings.UpdateMode GetExplicitUpdateMode(
|
||||
InputUpdateType targetUpdateType,
|
||||
InputSettings.UpdateMode fallbackUpdateMode)
|
||||
{
|
||||
if (targetUpdateType == InputUpdateType.Dynamic)
|
||||
{
|
||||
return InputSettings.UpdateMode.ProcessEventsInDynamicUpdate;
|
||||
}
|
||||
|
||||
if (targetUpdateType == InputUpdateType.Fixed)
|
||||
{
|
||||
return InputSettings.UpdateMode.ProcessEventsInFixedUpdate;
|
||||
}
|
||||
|
||||
if (targetUpdateType == InputUpdateType.Manual)
|
||||
{
|
||||
return InputSettings.UpdateMode.ProcessEventsManually;
|
||||
}
|
||||
|
||||
return fallbackUpdateMode;
|
||||
}
|
||||
|
||||
private static async Task WaitForRuntimeFrames(int frameCount, CancellationToken ct)
|
||||
{
|
||||
int startFrameCount = Time.frameCount;
|
||||
int observedFrames = 0;
|
||||
|
||||
while (observedFrames < frameCount)
|
||||
{
|
||||
await EditorDelay.DelayFrame(1, ct);
|
||||
observedFrames = Time.frameCount - startFrameCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d2cdea3342ed49e2a971e1766cbafa7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,64 @@
|
||||
#nullable enable
|
||||
#if ULOOPMCP_HAS_INPUT_SYSTEM
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
// Projects can process Input System events in dynamic, fixed, or manual mode.
|
||||
// Input simulation must follow that configured loop to avoid frame mismatches.
|
||||
internal static class InputUpdateTypeResolver
|
||||
{
|
||||
public static InputUpdateType Resolve()
|
||||
{
|
||||
InputSettings? settings = InputSystem.settings;
|
||||
if (settings == null)
|
||||
{
|
||||
return InputUpdateType.Dynamic;
|
||||
}
|
||||
|
||||
InputSettings.UpdateMode updateMode = settings.updateMode;
|
||||
switch (updateMode)
|
||||
{
|
||||
case InputSettings.UpdateMode.ProcessEventsInFixedUpdate:
|
||||
// Paused screens commonly set timeScale to 0, which stops fixed ticks entirely.
|
||||
// Falling back to Dynamic keeps input simulation responsive for those menus.
|
||||
return IsPausedFixedUpdate(settings) ? InputUpdateType.Dynamic : InputUpdateType.Fixed;
|
||||
|
||||
case InputSettings.UpdateMode.ProcessEventsManually:
|
||||
return InputUpdateType.Manual;
|
||||
|
||||
default:
|
||||
return InputUpdateType.Dynamic;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsMatch(InputUpdateType current, InputUpdateType expected)
|
||||
{
|
||||
return (current & expected) == expected;
|
||||
}
|
||||
|
||||
public static bool RequiresExplicitUpdate()
|
||||
{
|
||||
InputSettings? settings = InputSystem.settings;
|
||||
if (settings == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (settings.updateMode == InputSettings.UpdateMode.ProcessEventsManually)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return IsPausedFixedUpdate(settings);
|
||||
}
|
||||
|
||||
private static bool IsPausedFixedUpdate(InputSettings settings)
|
||||
{
|
||||
return settings.updateMode == InputSettings.UpdateMode.ProcessEventsInFixedUpdate && Time.timeScale <= 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 67072e4564d6345d58ceff0440cbfeda
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: acb202afcf62f40b88d846c3f1e0c373
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,102 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Compile error or warning information
|
||||
/// </summary>
|
||||
public class CompileIssue
|
||||
{
|
||||
public string Message { get; set; }
|
||||
public string File { get; set; }
|
||||
public int Line { get; set; }
|
||||
|
||||
public CompileIssue(string message, string file, int line)
|
||||
{
|
||||
Message = message;
|
||||
File = file;
|
||||
Line = line;
|
||||
}
|
||||
|
||||
public CompileIssue() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response schema for Compile command
|
||||
/// Provides type-safe response structure
|
||||
/// </summary>
|
||||
public class CompileResponse : BaseToolResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether compilation was successful.
|
||||
/// Null is used when the tool cannot reliably determine the result yet (e.g., forced recompile/domain reload),
|
||||
/// to avoid misleading clients into treating "0 errors" as a confirmed success.
|
||||
/// </summary>
|
||||
public bool? Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of compilation errors.
|
||||
/// Null is used when the tool intentionally does not provide details (e.g., forced recompile),
|
||||
/// because Unity reports errors/warnings after domain reload and clients should fetch logs later.
|
||||
/// </summary>
|
||||
public int? ErrorCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of compilation warnings.
|
||||
/// Null is used when the tool intentionally does not provide details (e.g., forced recompile),
|
||||
/// because Unity reports errors/warnings after domain reload and clients should fetch logs later.
|
||||
/// </summary>
|
||||
public int? WarningCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Compilation errors.
|
||||
/// Null is used when the tool intentionally does not provide details (e.g., forced recompile),
|
||||
/// because Unity reports errors/warnings after domain reload and clients should fetch logs later.
|
||||
/// </summary>
|
||||
public CompileIssue[] Errors { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Compilation warnings.
|
||||
/// Null is used when the tool intentionally does not provide details (e.g., forced recompile),
|
||||
/// because Unity reports errors/warnings after domain reload and clients should fetch logs later.
|
||||
/// </summary>
|
||||
public CompileIssue[] Warnings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional message for additional information
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unity project root path (from Application.dataPath).
|
||||
/// Set only when WaitForDomainReload=true so that TS/CLI can locate lock files
|
||||
/// in the correct project even when connected to a remote Unity instance.
|
||||
/// </summary>
|
||||
public string ProjectRoot { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new CompileResponse
|
||||
/// </summary>
|
||||
public CompileResponse(
|
||||
bool? success,
|
||||
int? errorCount,
|
||||
int? warningCount,
|
||||
CompileIssue[] errors,
|
||||
CompileIssue[] warnings,
|
||||
string message = null
|
||||
)
|
||||
{
|
||||
Success = success;
|
||||
ErrorCount = errorCount;
|
||||
WarningCount = warningCount;
|
||||
Errors = errors;
|
||||
Warnings = warnings;
|
||||
Message = message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameterless constructor for JSON deserialization
|
||||
/// </summary>
|
||||
public CompileResponse()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 56090f2379ae3428bbe733a4db41e344
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Schema for Compile command parameters
|
||||
/// Provides type-safe parameter access with default values
|
||||
/// </summary>
|
||||
public class CompileSchema : BaseToolSchema
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to perform forced recompilation
|
||||
/// </summary>
|
||||
[Description("Whether to perform forced recompilation. Note: Force compilation results do not include error/warning messages in the response. Use get-logs tool after execution to retrieve compilation messages.")]
|
||||
public bool ForceRecompile { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to wait for domain reload completion before the caller returns.
|
||||
/// </summary>
|
||||
[Description("Whether to wait for domain reload completion before returning.")]
|
||||
public bool WaitForDomainReload { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Internal request identifier used for delayed result recovery across domain reload.
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public string RequestId { get; set; } = "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2581c0d32a90546fd9978a2f938e2f6f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Compile tool handler - Type-safe implementation using Schema and Response
|
||||
/// Handles Unity project compilation with optional force recompile
|
||||
/// Related classes: CompileUseCase, CompilationStateValidationService, CompilationExecutionService
|
||||
/// </summary>
|
||||
[McpTool(Description = "Execute Unity project compilation")]
|
||||
public class CompileTool : AbstractUnityTool<CompileSchema, CompileResponse>
|
||||
{
|
||||
public override string ToolName => "compile";
|
||||
|
||||
/// <summary>
|
||||
/// Execute compile tool - delegates to CompileUseCase
|
||||
/// </summary>
|
||||
/// <param name="parameters">Type-safe parameters</param>
|
||||
/// <param name="cancellationToken">Cancellation token for timeout control</param>
|
||||
/// <returns>Compile result</returns>
|
||||
protected override async Task<CompileResponse> ExecuteAsync(CompileSchema parameters, CancellationToken cancellationToken)
|
||||
{
|
||||
// Create and execute CompileUseCase instance
|
||||
CompileUseCase useCase = new();
|
||||
return await useCase.ExecuteAsync(parameters, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 60e9cabfcc7d047cbab5dfd6e9b06904
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1186b025586e04dd794dace399e9ce35
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
name: uloop-compile
|
||||
description: "Compile the Unity project and report errors/warnings. Use after C# edits or when a full Domain Reload compile is needed."
|
||||
---
|
||||
|
||||
# uloop compile
|
||||
|
||||
Execute Unity project compilation.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
uloop compile [--force-recompile] [--wait-for-domain-reload]
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `--force-recompile` | boolean | `false` | Force full recompilation (triggers Domain Reload) |
|
||||
| `--wait-for-domain-reload` | boolean | `false` | Wait until Domain Reload completes before returning |
|
||||
|
||||
## Global Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--project-path <path>` | Optional. Use only when the target Unity project is not the current directory. |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Check compilation
|
||||
uloop compile
|
||||
|
||||
# Force full recompilation
|
||||
uloop compile --force-recompile
|
||||
|
||||
# Force recompilation and wait for Domain Reload completion
|
||||
uloop compile --force-recompile true --wait-for-domain-reload true
|
||||
|
||||
# Wait for Domain Reload completion even without force recompilation
|
||||
uloop compile --force-recompile false --wait-for-domain-reload true
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Returns JSON:
|
||||
- `Success`: boolean
|
||||
- `ErrorCount`: number
|
||||
- `WarningCount`: number
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Diagnose the failure mode before retrying.
|
||||
|
||||
**Stale lock files** (CLI hangs or shows "Unity is busy" while Unity Editor *is* running):
|
||||
|
||||
```bash
|
||||
uloop fix
|
||||
```
|
||||
|
||||
This removes any leftover lock files (`compiling.lock`, `domainreload.lock`, `serverstarting.lock`) from the Unity project's Temp directory. Then retry `uloop compile`.
|
||||
|
||||
**Unity Editor not running** (CLI returns a connection failure and no Unity process is alive):
|
||||
|
||||
```bash
|
||||
uloop launch
|
||||
```
|
||||
|
||||
`uloop launch` auto-detects the project at the current working directory and opens it in the matching Unity Editor version. After Unity finishes launching, retry `uloop compile`.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3e7fbf71d98ab425486f6bd68b7a6612
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a76e166c9de484f20b07f88305dd4b6b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
public class ControlPlayModeResponse : BaseToolResponse
|
||||
{
|
||||
public bool IsPlaying { get; set; }
|
||||
public bool IsPaused { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 752e3f7c18a5649d8b6a555add78f1d7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
public enum PlayModeAction
|
||||
{
|
||||
Play = 0,
|
||||
Stop = 1,
|
||||
Pause = 2
|
||||
}
|
||||
|
||||
public class ControlPlayModeSchema : BaseToolSchema
|
||||
{
|
||||
[Description("Action to perform: Play(0) - Start play mode, Stop(1) - Stop play mode, Pause(2) - Pause play mode")]
|
||||
public PlayModeAction Action { get; set; } = PlayModeAction.Play;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5634451b383ae4ff8b0a1cd00500f342
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
[McpTool(Description = "Control Unity Editor play mode (play/stop/pause)")]
|
||||
public class ControlPlayModeTool : AbstractUnityTool<ControlPlayModeSchema, ControlPlayModeResponse>
|
||||
{
|
||||
public override string ToolName => "control-play-mode";
|
||||
|
||||
protected override async Task<ControlPlayModeResponse> ExecuteAsync(ControlPlayModeSchema parameters, CancellationToken cancellationToken)
|
||||
{
|
||||
ControlPlayModeUseCase useCase = new();
|
||||
return await useCase.ExecuteAsync(parameters, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bdce84d8140ea44e9a5fbc8b1e1cf5d6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8809b197b1dbb42fbbabc04674f5baa9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: uloop-control-play-mode
|
||||
description: "Control Unity Editor Play Mode. Use to start, stop, or pause Play Mode for runtime behavior checks and frame inspection."
|
||||
---
|
||||
|
||||
# uloop control-play-mode
|
||||
|
||||
Control Unity Editor play mode (play/stop/pause).
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
uloop control-play-mode [options]
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `--action` | string | `Play` | Action to perform: `Play`, `Stop`, `Pause` |
|
||||
|
||||
## Global Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--project-path <path>` | Optional. Use only when the target Unity project is not the current directory. |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Start play mode
|
||||
uloop control-play-mode --action Play
|
||||
|
||||
# Stop play mode
|
||||
uloop control-play-mode --action Stop
|
||||
|
||||
# Pause play mode
|
||||
uloop control-play-mode --action Pause
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Returns JSON with the current play mode state:
|
||||
- `IsPlaying`: Whether Unity is currently in play mode
|
||||
- `IsPaused`: Whether play mode is paused
|
||||
- `Message`: Description of the action performed
|
||||
|
||||
## Notes
|
||||
|
||||
- Play action starts the game in the Unity Editor (also resumes from pause)
|
||||
- Stop action exits play mode and returns to edit mode
|
||||
- Pause action pauses the game while remaining in play mode
|
||||
- Useful for automated testing workflows
|
||||
|
||||
- PlayMode entry may complete on the next editor frame. If a PlayMode-dependent command reports "PlayMode is not active" immediately after `--action Play`, wait briefly and retry.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1e1425714444f43d1aab052bf7101893
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4d88782b067f473a8c1bf230936d8f0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,122 @@
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using System.Threading;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
// Related classes:
|
||||
// - IUnityTool: The interface that this class implements.
|
||||
// - UnityToolRegistry: Registers and manages instances of tool implementations.
|
||||
// - ToolParameterSchemaGenerator: Generates the JSON schema for tool parameters.
|
||||
/// <summary>
|
||||
/// Abstract base class for type-safe Unity tools using Schema and Response types
|
||||
/// </summary>
|
||||
/// <typeparam name="TSchema">Schema type for tool parameters</typeparam>
|
||||
/// <typeparam name="TResponse">Response type for tool results</typeparam>
|
||||
public abstract class AbstractUnityTool<TSchema, TResponse> : IUnityTool
|
||||
where TSchema : BaseToolSchema, new()
|
||||
where TResponse : BaseToolResponse
|
||||
{
|
||||
public abstract string ToolName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Automatically generates parameter schema from TSchema type
|
||||
/// </summary>
|
||||
public virtual ToolParameterSchema ParameterSchema =>
|
||||
ToolParameterSchemaGenerator.FromDto<TSchema>();
|
||||
|
||||
/// <summary>
|
||||
/// Execute tool with type-safe Schema parameters
|
||||
/// Note: This method is called from the main thread context.
|
||||
/// MainThreadSwitcher.SwitchToMainThread() is already handled by the upper layer (JsonRpcProcessor),
|
||||
/// so individual tools do not need to call it again.
|
||||
/// </summary>
|
||||
/// <param name="parameters">Strongly typed parameters</param>
|
||||
/// <param name="cancellationToken">Cancellation token for timeout control</param>
|
||||
/// <returns>Strongly typed tool execution result</returns>
|
||||
protected abstract Task<TResponse> ExecuteAsync(TSchema parameters, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// IUnityTool implementation - converts JToken to Schema and returns BaseToolResponse
|
||||
/// </summary>
|
||||
public async Task<BaseToolResponse> ExecuteAsync(JToken paramsToken)
|
||||
{
|
||||
// Convert JToken to strongly typed Schema
|
||||
TSchema parameters = ConvertToSchema(paramsToken);
|
||||
|
||||
// Execute with type-safe parameters
|
||||
TResponse response = await ExecuteAsync(parameters, CancellationToken.None);
|
||||
|
||||
// Return as BaseToolResponse for IUnityTool interface compatibility
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert JToken to strongly typed Schema with default value fallback
|
||||
/// </summary>
|
||||
private TSchema ConvertToSchema(JToken paramsToken)
|
||||
{
|
||||
if (paramsToken == null || paramsToken.Type == JTokenType.Null)
|
||||
{
|
||||
// Return default instance if no parameters provided
|
||||
return new TSchema();
|
||||
}
|
||||
|
||||
// Create JsonSerializerSettings with CamelCasePropertyNamesContractResolver
|
||||
// This allows TypeScript side to use camelCase while C# uses PascalCase
|
||||
JsonSerializerSettings settings = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
|
||||
};
|
||||
|
||||
// Create JsonSerializer with custom settings
|
||||
JsonSerializer serializer = JsonSerializer.Create(settings);
|
||||
|
||||
// Try to deserialize from JToken with custom serializer
|
||||
TSchema schema;
|
||||
try
|
||||
{
|
||||
schema = paramsToken.ToObject<TSchema>(serializer);
|
||||
}
|
||||
catch (JsonSerializationException ex)
|
||||
{
|
||||
// Create detailed error message for type mismatches
|
||||
string errorMessage = $"Parameter type mismatch for tool '{ToolName}': {ex.Message}";
|
||||
|
||||
// Check for specific Dictionary<string, object> conversion errors
|
||||
if (ex.Message.Contains("Dictionary") && ex.Message.Contains("Error converting value"))
|
||||
{
|
||||
string received = paramsToken["parameters"]?.ToString() ?? paramsToken["Parameters"]?.ToString() ?? "null";
|
||||
errorMessage =
|
||||
"Parameter 'Parameters' must be an object, not a string. " +
|
||||
"Do: omit 'Parameters' or use {}. " +
|
||||
"Don't: \"{}\" (string). " +
|
||||
$"Received: {received}";
|
||||
}
|
||||
|
||||
throw new ParameterValidationException(errorMessage, ex);
|
||||
}
|
||||
|
||||
// If deserialization returns null, create default instance
|
||||
if (schema == null)
|
||||
{
|
||||
schema = new TSchema();
|
||||
}
|
||||
|
||||
// Apply default values for null properties
|
||||
return ApplyDefaultValues(schema);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply default values to Schema properties if they are null
|
||||
/// Override this method to provide custom default value logic
|
||||
/// </summary>
|
||||
protected virtual TSchema ApplyDefaultValues(TSchema schema)
|
||||
{
|
||||
// Default implementation - return as is
|
||||
// Subclasses can override to apply specific default values
|
||||
return schema;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d4b53392372a454e954428caf82258a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// UseCase base class - Responsible for temporal cohesion
|
||||
/// New instances are created each time and disposed after Execute completion
|
||||
/// Related classes: AbstractUnityTool, BaseToolSchema, BaseToolResponse
|
||||
/// </summary>
|
||||
/// <typeparam name="TSchema">Schema type for tool parameters</typeparam>
|
||||
/// <typeparam name="TResponse">Response type for tool results</typeparam>
|
||||
public abstract class AbstractUseCase<TSchema, TResponse>
|
||||
where TSchema : BaseToolSchema
|
||||
where TResponse : BaseToolResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// UseCase execution method - The only public method
|
||||
/// Responsible for temporal cohesion (executing multiple operations in sequence)
|
||||
/// </summary>
|
||||
/// <param name="parameters">Type-safe parameters</param>
|
||||
/// <param name="cancellationToken">Cancellation control token</param>
|
||||
/// <returns>Type-safe execution result</returns>
|
||||
public abstract Task<TResponse> ExecuteAsync(TSchema parameters, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9a74e8d5c037f4277a284f8f686edc05
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Base response class for all Unity MCP tool responses
|
||||
/// </summary>
|
||||
public abstract class BaseToolResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// uLoopMCP server version for CLI version compatibility check
|
||||
/// </summary>
|
||||
public string Ver => McpVersion.VERSION;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a54be1e158eda4064aa08ab9da92d980
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Base schema class for all Unity MCP tool parameters
|
||||
/// </summary>
|
||||
public abstract class BaseToolSchema
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c705d28d63c1b4fb5aa6d7797c95631d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,114 @@
|
||||
using UnityEditor;
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// SessionState management class for forced recompilation
|
||||
/// Preserves request information even when assembly reload occurs
|
||||
/// </summary>
|
||||
public static class CompileSessionState
|
||||
{
|
||||
// Note: Compile requests are now managed via McpSessionManager
|
||||
|
||||
/// <summary>
|
||||
/// Forced recompilation request information
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class CompileRequestInfo
|
||||
{
|
||||
public string requestId;
|
||||
public bool forceRecompile;
|
||||
public string clientEndpoint;
|
||||
public DateTime requestTime;
|
||||
public bool isCompleted;
|
||||
|
||||
public CompileRequestInfo(string requestId, bool forceRecompile, string clientEndpoint)
|
||||
{
|
||||
this.requestId = requestId;
|
||||
this.forceRecompile = forceRecompile;
|
||||
this.clientEndpoint = clientEndpoint;
|
||||
this.requestTime = DateTime.Now;
|
||||
this.isCompleted = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save forced recompilation request
|
||||
/// </summary>
|
||||
public static void SaveCompileRequest(string requestId, bool forceRecompile, string clientEndpoint = "unknown")
|
||||
{
|
||||
if (!forceRecompile) return; // Don't save normal compilation
|
||||
|
||||
CompileRequestInfo requestInfo = new CompileRequestInfo(requestId, forceRecompile, clientEndpoint);
|
||||
string json = JsonConvert.SerializeObject(requestInfo);
|
||||
|
||||
McpEditorSettings.SetCompileRequestJson(requestId, json);
|
||||
McpEditorSettings.AddPendingCompileRequest(requestId);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get pending request IDs
|
||||
/// </summary>
|
||||
public static string[] GetPendingRequestIds()
|
||||
{
|
||||
return McpEditorSettings.GetPendingCompileRequestIds();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get saved request information
|
||||
/// </summary>
|
||||
public static CompileRequestInfo GetCompileRequest(string requestId)
|
||||
{
|
||||
string json = McpEditorSettings.GetCompileRequestJson(requestId);
|
||||
if (string.IsNullOrEmpty(json)) return null;
|
||||
|
||||
try
|
||||
{
|
||||
return JsonConvert.DeserializeObject<CompileRequestInfo>(json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark request as completed
|
||||
/// </summary>
|
||||
public static void MarkRequestCompleted(string requestId)
|
||||
{
|
||||
CompileRequestInfo requestInfo = GetCompileRequest(requestId);
|
||||
if (requestInfo == null) return;
|
||||
|
||||
requestInfo.isCompleted = true;
|
||||
string json = JsonConvert.SerializeObject(requestInfo);
|
||||
McpEditorSettings.SetCompileRequestJson(requestId, json);
|
||||
McpEditorSettings.RemovePendingCompileRequest(requestId);
|
||||
|
||||
}
|
||||
|
||||
// Note: RemoveFromPendingRequests is now handled by McpSessionManager
|
||||
|
||||
/// <summary>
|
||||
/// Start forced recompilation
|
||||
/// </summary>
|
||||
public static void StartForceRecompile()
|
||||
{
|
||||
UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(
|
||||
UnityEditor.Compilation.RequestScriptCompilationOptions.CleanBuildCache
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear SessionState
|
||||
/// </summary>
|
||||
public static void ClearAll()
|
||||
{
|
||||
McpEditorSettings.ClearAllCompileRequests();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 43fb73081d49d4a309e18955e541493f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Static class for managing custom tools
|
||||
/// Allows users to register and manage their own tools
|
||||
/// </summary>
|
||||
public static class CustomToolManager
|
||||
{
|
||||
private static UnityToolRegistry _sharedRegistry;
|
||||
|
||||
internal static event Action OnToolsChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Get shared registry (lazy initialization)
|
||||
/// </summary>
|
||||
private static UnityToolRegistry SharedRegistry
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_sharedRegistry == null)
|
||||
{
|
||||
_sharedRegistry = new UnityToolRegistry();
|
||||
// Standard tools are automatically registered in UnityToolRegistry constructor
|
||||
}
|
||||
return _sharedRegistry;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register custom tool
|
||||
/// </summary>
|
||||
/// <param name="tool">Tool to register</param>
|
||||
public static void RegisterCustomTool(IUnityTool tool)
|
||||
{
|
||||
SharedRegistry.RegisterTool(tool);
|
||||
|
||||
// Notify tool changes for manual registration
|
||||
NotifyToolChanges();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregister custom tool
|
||||
/// </summary>
|
||||
/// <param name="toolName">Name of tool to unregister</param>
|
||||
public static void UnregisterCustomTool(string toolName)
|
||||
{
|
||||
SharedRegistry.UnregisterTool(toolName);
|
||||
|
||||
// Notify tool changes for manual unregistration
|
||||
NotifyToolChanges();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get list of all registered tools (standard + custom)
|
||||
/// </summary>
|
||||
/// <returns>Array of tool information</returns>
|
||||
public static ToolInfo[] GetRegisteredCustomTools()
|
||||
{
|
||||
return SharedRegistry.GetRegisteredTools();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if specified tool is registered
|
||||
/// </summary>
|
||||
/// <param name="toolName">Tool name</param>
|
||||
/// <returns>True if registered</returns>
|
||||
public static bool IsCustomToolRegistered(string toolName)
|
||||
{
|
||||
return SharedRegistry.IsToolRegistered(toolName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get internal registry (for MCP server)
|
||||
/// </summary>
|
||||
/// <returns>UnityToolRegistry instance</returns>
|
||||
internal static UnityToolRegistry GetRegistry()
|
||||
{
|
||||
return SharedRegistry;
|
||||
}
|
||||
|
||||
internal static UnityToolRegistry TryGetRegistry()
|
||||
{
|
||||
return _sharedRegistry;
|
||||
}
|
||||
|
||||
internal static void WarmupRegistry()
|
||||
{
|
||||
_ = SharedRegistry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Debug: Get detailed registry information
|
||||
/// </summary>
|
||||
/// <returns>Debug information</returns>
|
||||
public static string GetDebugInfo()
|
||||
{
|
||||
ToolInfo[] tools = SharedRegistry.GetRegisteredTools();
|
||||
string[] toolNames = new string[tools.Length];
|
||||
for (int i = 0; i < tools.Length; i++)
|
||||
{
|
||||
toolNames[i] = tools[i].Name;
|
||||
}
|
||||
return $"Registry instance: {SharedRegistry.GetHashCode()}, Tools: [{string.Join(", ", toolNames)}]";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually notify tool changes to MCP clients
|
||||
/// Public API for users to trigger notifications when needed
|
||||
/// </summary>
|
||||
public static void NotifyToolChanges()
|
||||
{
|
||||
UnityToolRegistry.TriggerToolsChangedNotification();
|
||||
OnToolsChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dd9b6252fa10a4edc9a5f2ee0140365d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Constants for parameter schema property names
|
||||
/// Ensures consistency between Unity and TypeScript sides
|
||||
/// </summary>
|
||||
public static class ParameterSchemaConstants
|
||||
{
|
||||
public const string TYPE_PROPERTY = "Type";
|
||||
public const string DESCRIPTION_PROPERTY = "Description";
|
||||
public const string DEFAULT_VALUE_PROPERTY = "DefaultValue";
|
||||
public const string ENUM_PROPERTY = "Enum";
|
||||
public const string PROPERTIES_PROPERTY = "Properties";
|
||||
public const string REQUIRED_PROPERTY = "Required";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base interface for Unity MCP tool handlers
|
||||
/// Following the Open-Closed principle, when adding new tools,
|
||||
/// create a new class that implements this interface
|
||||
/// </summary>
|
||||
public interface IUnityTool
|
||||
{
|
||||
/// <summary>
|
||||
/// Get tool name
|
||||
/// </summary>
|
||||
string ToolName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get parameter schema information for TypeScript side
|
||||
/// </summary>
|
||||
ToolParameterSchema ParameterSchema { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Execute tool
|
||||
/// </summary>
|
||||
/// <param name="paramsToken">JSON token for parameters</param>
|
||||
/// <returns>Execution result</returns>
|
||||
Task<BaseToolResponse> ExecuteAsync(JToken paramsToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameter schema definition for tools (immutable)
|
||||
/// </summary>
|
||||
public class ToolParameterSchema
|
||||
{
|
||||
public readonly Dictionary<string, ParameterInfo> Properties;
|
||||
public readonly string[] Required;
|
||||
|
||||
public ToolParameterSchema(Dictionary<string, ParameterInfo> properties = null, string[] required = null)
|
||||
{
|
||||
Properties = properties ?? new Dictionary<string, ParameterInfo>();
|
||||
Required = required ?? new string[0];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Individual parameter information (immutable)
|
||||
/// </summary>
|
||||
public class ParameterInfo
|
||||
{
|
||||
public readonly string Type;
|
||||
public readonly string Description;
|
||||
public readonly object DefaultValue;
|
||||
public readonly string[] Enum;
|
||||
|
||||
public ParameterInfo(string type, string description, object defaultValue = null, string[] enumValues = null)
|
||||
{
|
||||
Type = type;
|
||||
Description = description;
|
||||
DefaultValue = defaultValue;
|
||||
Enum = enumValues;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f9cf1a64741648cb8533b76ec6f3a87
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Attribute to mark classes for automatic MCP tool registration
|
||||
/// Classes marked with this attribute will be automatically registered as MCP commands
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
public class McpToolAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether this tool should only be displayed in development mode
|
||||
/// </summary>
|
||||
public bool DisplayDevelopmentOnly { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the specific security setting required to execute this command
|
||||
/// </summary>
|
||||
public SecuritySettings RequiredSecuritySetting { get; set; } = SecuritySettings.None;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description of this tool
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize McpTool attribute
|
||||
/// </summary>
|
||||
public McpToolAttribute()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f9f72309eee6a49528d19b182ff51ad6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,64 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
// Instantiates the InputVisualizationCanvas prefab and manages its lifecycle.
|
||||
internal static class OverlayCanvasFactory
|
||||
{
|
||||
private const string CANVAS_PREFAB_PATH = "Packages/io.github.hatayama.uloopmcp/Runtime/Common/InputVisualizationCanvas.prefab";
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void ResetStaticFields()
|
||||
{
|
||||
_instance = null;
|
||||
}
|
||||
|
||||
private static InputVisualizationCanvas _instance;
|
||||
|
||||
public static InputVisualizationCanvas VisualizationCanvas
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureExists();
|
||||
Debug.Assert(_instance != null, "InputVisualizationCanvas instance must exist after EnsureExists");
|
||||
return _instance!;
|
||||
}
|
||||
}
|
||||
|
||||
public static void EnsureExists()
|
||||
{
|
||||
if (_instance != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Domain Reload resets _instance but DontDestroyOnLoad objects survive; reclaim one and destroy duplicates
|
||||
InputVisualizationCanvas[] existing =
|
||||
Object.FindObjectsByType<InputVisualizationCanvas>(FindObjectsSortMode.None);
|
||||
for (int i = 0; i < existing.Length; i++)
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
_instance = existing[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
Object.DestroyImmediate(existing[i].gameObject);
|
||||
}
|
||||
}
|
||||
if (_instance != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(CANVAS_PREFAB_PATH);
|
||||
Debug.Assert(prefab != null, $"InputVisualizationCanvas prefab not found at {CANVAS_PREFAB_PATH}");
|
||||
|
||||
GameObject go = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
|
||||
Object.DontDestroyOnLoad(go);
|
||||
_instance = go.GetComponent<InputVisualizationCanvas>();
|
||||
Debug.Assert(_instance != null, "InputVisualizationCanvas component not found on prefab");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 76e5da6abcd484cbcbdbbf462a6fdd4b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,52 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic data model representing service execution result
|
||||
/// Used in Application Service Layer
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of result data</typeparam>
|
||||
public class ServiceResult<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether execution was successful
|
||||
/// </summary>
|
||||
public bool Success { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Data of execution result
|
||||
/// </summary>
|
||||
public T Data { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message when execution fails
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create ServiceResult
|
||||
/// </summary>
|
||||
/// <param name="success">Execution result</param>
|
||||
/// <param name="data">Result data</param>
|
||||
/// <param name="errorMessage">Error message (null on success)</param>
|
||||
public ServiceResult(bool success, T data = default, string errorMessage = null)
|
||||
{
|
||||
Success = success;
|
||||
Data = data;
|
||||
ErrorMessage = errorMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create success result
|
||||
/// </summary>
|
||||
/// <param name="data">Result data</param>
|
||||
/// <returns>ServiceResult representing success</returns>
|
||||
public static ServiceResult<T> SuccessResult(T data) => new(true, data);
|
||||
|
||||
/// <summary>
|
||||
/// Create failure result
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">Error message</param>
|
||||
/// <returns>ServiceResult representing failure</returns>
|
||||
public static ServiceResult<T> FailureResult(string errorMessage) => new(false, default, errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de2101885e97d4499889ddbb6bb6e358
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,54 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal static class ToolExecutionAvailability
|
||||
{
|
||||
internal static bool ShouldReportDependencyUnavailableBeforeDisabled(string toolName)
|
||||
{
|
||||
return ShouldReportDependencyUnavailableBeforeDisabled(
|
||||
toolName,
|
||||
isTestFrameworkAvailable: IsTestFrameworkAvailable);
|
||||
}
|
||||
|
||||
internal static bool ShouldReportDependencyUnavailableBeforeDisabled(
|
||||
string toolName,
|
||||
bool isTestFrameworkAvailable)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(toolName), "toolName must not be null or empty");
|
||||
|
||||
return toolName == McpConstants.TOOL_NAME_RUN_TESTS && !isTestFrameworkAvailable;
|
||||
}
|
||||
|
||||
internal static bool ShouldExposeInRegisteredTools(string toolName)
|
||||
{
|
||||
return ShouldExposeInRegisteredTools(
|
||||
toolName,
|
||||
isToolEnabled: ToolSettings.IsToolEnabled(toolName),
|
||||
isTestFrameworkAvailable: IsTestFrameworkAvailable);
|
||||
}
|
||||
|
||||
internal static bool ShouldExposeInRegisteredTools(
|
||||
string toolName,
|
||||
bool isToolEnabled,
|
||||
bool isTestFrameworkAvailable)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(toolName), "toolName must not be null or empty");
|
||||
|
||||
return isToolEnabled ||
|
||||
ShouldReportDependencyUnavailableBeforeDisabled(toolName, isTestFrameworkAvailable);
|
||||
}
|
||||
|
||||
internal static bool IsTestFrameworkAvailable
|
||||
{
|
||||
get
|
||||
{
|
||||
#if ULOOPMCP_HAS_TEST_FRAMEWORK
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12a3cd3bb7213f14ab18fa845fe17f13
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,169 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility class to generate ToolParameterSchema from DTO classes
|
||||
/// Eliminates duplication between DTO default values and schema definitions
|
||||
/// </summary>
|
||||
public static class ToolParameterSchemaGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generate ToolParameterSchema from DTO type using reflection
|
||||
/// </summary>
|
||||
/// <typeparam name="TDto">DTO type</typeparam>
|
||||
/// <returns>Generated parameter schema</returns>
|
||||
public static ToolParameterSchema FromDto<TDto>() where TDto : class, new()
|
||||
{
|
||||
Type dtoType = typeof(TDto);
|
||||
Dictionary<string, ParameterInfo> properties = new Dictionary<string, ParameterInfo>();
|
||||
List<string> required = new List<string>();
|
||||
|
||||
// Create instance to get default values
|
||||
TDto defaultInstance = new TDto();
|
||||
|
||||
foreach (PropertyInfo property in dtoType.GetProperties())
|
||||
{
|
||||
// Skip properties without public setters
|
||||
if (!property.CanWrite || property.SetMethod?.IsPublic != true)
|
||||
continue;
|
||||
|
||||
BrowsableAttribute browsableAttribute = property.GetCustomAttribute<BrowsableAttribute>();
|
||||
if (browsableAttribute != null && !browsableAttribute.Browsable)
|
||||
continue;
|
||||
|
||||
// Get JSON property name (fallback to property name)
|
||||
string parameterName = GetJsonPropertyName(property);
|
||||
|
||||
// Get property type information
|
||||
string parameterType = GetParameterType(property.PropertyType);
|
||||
|
||||
// Get description from attributes
|
||||
string description = GetDescription(property);
|
||||
|
||||
// Get default value from instance
|
||||
object defaultValue = property.GetValue(defaultInstance);
|
||||
|
||||
// Get enum values if applicable
|
||||
string[] enumValues = GetEnumValues(property.PropertyType);
|
||||
|
||||
// Check if property is required
|
||||
bool isRequired = IsRequired(property);
|
||||
if (isRequired)
|
||||
{
|
||||
required.Add(parameterName);
|
||||
}
|
||||
|
||||
// Create parameter info
|
||||
ParameterInfo paramInfo = new ParameterInfo(
|
||||
parameterType,
|
||||
description,
|
||||
defaultValue,
|
||||
enumValues
|
||||
);
|
||||
|
||||
properties[parameterName] = paramInfo;
|
||||
}
|
||||
|
||||
return new ToolParameterSchema(properties, required.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get JSON property name from JsonProperty attribute or property name
|
||||
/// </summary>
|
||||
private static string GetJsonPropertyName(PropertyInfo property)
|
||||
{
|
||||
JsonPropertyAttribute jsonAttr = property.GetCustomAttribute<JsonPropertyAttribute>();
|
||||
return jsonAttr?.PropertyName ?? property.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert .NET type to parameter type string
|
||||
/// </summary>
|
||||
private static string GetParameterType(Type type)
|
||||
{
|
||||
// Handle nullable types
|
||||
Type underlyingType = Nullable.GetUnderlyingType(type) ?? type;
|
||||
|
||||
if (underlyingType == typeof(string))
|
||||
return "string";
|
||||
if (underlyingType == typeof(bool))
|
||||
return "boolean";
|
||||
if (underlyingType == typeof(int) || underlyingType == typeof(long) ||
|
||||
underlyingType == typeof(float) || underlyingType == typeof(double) ||
|
||||
underlyingType == typeof(decimal))
|
||||
return "number";
|
||||
if (underlyingType.IsArray || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)))
|
||||
return "array";
|
||||
if (underlyingType.IsEnum)
|
||||
return "string"; // Enums are treated as strings with enum constraints
|
||||
if (IsDictionaryType(underlyingType))
|
||||
return "object";
|
||||
|
||||
return "string"; // Default fallback
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if type is a dictionary type (Dictionary, IDictionary, IReadOnlyDictionary)
|
||||
/// </summary>
|
||||
private static bool IsDictionaryType(Type type)
|
||||
{
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
Type genericDef = type.GetGenericTypeDefinition();
|
||||
if (genericDef == typeof(Dictionary<,>) ||
|
||||
genericDef == typeof(IDictionary<,>) ||
|
||||
genericDef == typeof(IReadOnlyDictionary<,>))
|
||||
return true;
|
||||
}
|
||||
return type.GetInterfaces().Any(i =>
|
||||
i.IsGenericType &&
|
||||
(i.GetGenericTypeDefinition() == typeof(IDictionary<,>) ||
|
||||
i.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get description from Description attribute or generate default
|
||||
/// </summary>
|
||||
private static string GetDescription(PropertyInfo property)
|
||||
{
|
||||
DescriptionAttribute descAttr = property.GetCustomAttribute<DescriptionAttribute>();
|
||||
if (descAttr != null)
|
||||
return descAttr.Description;
|
||||
|
||||
// Generate default description
|
||||
return $"Parameter: {property.Name}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get enum values if property type is enum
|
||||
/// </summary>
|
||||
private static string[] GetEnumValues(Type type)
|
||||
{
|
||||
Type underlyingType = Nullable.GetUnderlyingType(type) ?? type;
|
||||
|
||||
if (underlyingType.IsEnum)
|
||||
{
|
||||
return Enum.GetNames(underlyingType);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if property is required using custom Required attribute
|
||||
/// For now, returns false as Unity doesn't support DataAnnotations
|
||||
/// Can be extended with custom attributes if needed
|
||||
/// </summary>
|
||||
private static bool IsRequired(PropertyInfo property)
|
||||
{
|
||||
// TODO: Implement custom Required attribute if needed
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b9d06a624dd5d4129b95d39ea144a412
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,424 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Stopwatch = System.Diagnostics.Stopwatch;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
// Related classes:
|
||||
// - UnityToolExecutor: Uses this registry to execute _tools.
|
||||
// - IUnityTool: The interface for all _tools stored in this registry.
|
||||
// - AbstractUnityTool: The base class for most tool implementations.
|
||||
// - McpToolAttribute: Attribute used to discover and register _tools automatically.
|
||||
/// <summary>
|
||||
/// Unity MCP tool registry class
|
||||
/// Supports dynamic tool registration, allowing users to add their own _tools
|
||||
/// </summary>
|
||||
public class UnityToolRegistry
|
||||
{
|
||||
private readonly Dictionary<string, IUnityTool> _tools = new();
|
||||
|
||||
/// <summary>
|
||||
/// Singleton instance for global access
|
||||
/// </summary>
|
||||
public static UnityToolRegistry Instance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor
|
||||
/// Auto-registers standard _tools
|
||||
/// </summary>
|
||||
public UnityToolRegistry()
|
||||
{
|
||||
Instance = this;
|
||||
RegisterDefaultTools();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register standard _tools
|
||||
/// </summary>
|
||||
private void RegisterDefaultTools()
|
||||
{
|
||||
// Register _tools with attribute-based discovery
|
||||
RegisterToolsWithAttributes();
|
||||
|
||||
// Manual registration for _tools without attributes (for backward compatibility)
|
||||
RegisterManualTools();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register _tools using attribute-based discovery
|
||||
/// </summary>
|
||||
private void RegisterToolsWithAttributes()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get all assemblies in the current domain
|
||||
Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies();
|
||||
|
||||
List<Type> toolTypes = new List<Type>();
|
||||
|
||||
foreach (Assembly assembly in assemblies)
|
||||
{
|
||||
// Find all types with McpTool attribute that implement IUnityTool
|
||||
Type[] types = assembly.GetTypes()
|
||||
.Where(type => type.GetCustomAttribute<McpToolAttribute>() != null)
|
||||
.Where(type => typeof(IUnityTool).IsAssignableFrom(type))
|
||||
.Where(type => !type.IsAbstract && !type.IsInterface)
|
||||
.ToArray();
|
||||
|
||||
toolTypes.AddRange(types);
|
||||
}
|
||||
|
||||
// Register all _tools - filtering will be handled by TypeScript side
|
||||
foreach (Type type in toolTypes)
|
||||
{
|
||||
// Security: Validate type before creating instance
|
||||
if (!IsValidToolType(type))
|
||||
{
|
||||
UnityEngine.Debug.LogWarning($"{McpConstants.SECURITY_LOG_PREFIX} Skipping invalid tool type: {type.FullName}");
|
||||
continue;
|
||||
}
|
||||
|
||||
IUnityTool tool = (IUnityTool)Activator.CreateInstance(type);
|
||||
RegisterTool(tool);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register _tools manually (for backward compatibility)
|
||||
/// </summary>
|
||||
private void RegisterManualTools()
|
||||
{
|
||||
// Only register _tools that don't have the McpTool attribute
|
||||
// This prevents double registration
|
||||
|
||||
if (!IsToolTypeRegistered<PingTool>())
|
||||
{
|
||||
RegisterTool(new PingTool());
|
||||
}
|
||||
|
||||
if (!IsToolTypeRegistered<CompileTool>())
|
||||
{
|
||||
RegisterTool(new CompileTool());
|
||||
}
|
||||
|
||||
if (!IsToolTypeRegistered<GetLogsTool>())
|
||||
{
|
||||
RegisterTool(new GetLogsTool());
|
||||
}
|
||||
|
||||
if (!IsToolTypeRegistered<RunTestsTool>())
|
||||
{
|
||||
RegisterTool(new RunTestsTool());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a tool type is already registered
|
||||
/// </summary>
|
||||
private bool IsToolTypeRegistered<T>() where T : IUnityTool
|
||||
{
|
||||
return _tools.Values.Any(tool => tool.GetType() == typeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Security: Validate if the type is safe to instantiate
|
||||
/// </summary>
|
||||
private bool IsValidToolType(Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Must implement IUnityTool
|
||||
if (!typeof(IUnityTool).IsAssignableFrom(type))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must not be abstract or interface
|
||||
if (type.IsAbstract || type.IsInterface)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have McpTool attribute
|
||||
if (type.GetCustomAttribute<McpToolAttribute>() == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have parameterless constructor
|
||||
if (type.GetConstructor(Type.EmptyTypes) == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UnityEngine.Debug.LogError($"{McpConstants.SECURITY_LOG_PREFIX} Error validating tool type {type?.FullName}: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register tool
|
||||
/// </summary>
|
||||
/// <param name="tool">Tool to register</param>
|
||||
public void RegisterTool(IUnityTool tool)
|
||||
{
|
||||
if (tool == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tool));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tool.ToolName))
|
||||
{
|
||||
throw new ArgumentException("Tool name cannot be null or empty", nameof(tool));
|
||||
}
|
||||
|
||||
_tools[tool.ToolName] = tool;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregister tool
|
||||
/// </summary>
|
||||
/// <param name="toolName">Name of tool to unregister</param>
|
||||
public void UnregisterTool(string toolName)
|
||||
{
|
||||
_tools.Remove(toolName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute tool
|
||||
/// </summary>
|
||||
/// <param name="toolName">Tool name</param>
|
||||
/// <param name="paramsToken">Parameters</param>
|
||||
/// <returns>Execution result</returns>
|
||||
/// <exception cref="ArgumentException">When tool is unknown</exception>
|
||||
/// <exception cref="McpSecurityException">When tool is blocked by security settings</exception>
|
||||
public async Task<BaseToolResponse> ExecuteToolAsync(string toolName, JToken paramsToken)
|
||||
{
|
||||
if (!_tools.TryGetValue(toolName, out IUnityTool tool))
|
||||
{
|
||||
throw new ArgumentException($"Unknown tool: {toolName}");
|
||||
}
|
||||
|
||||
if (!ToolSettings.IsToolEnabled(toolName)
|
||||
&& !ToolExecutionAvailability.ShouldReportDependencyUnavailableBeforeDisabled(toolName))
|
||||
{
|
||||
throw new ToolDisabledException(toolName);
|
||||
}
|
||||
|
||||
// Security check - validate tool before execution
|
||||
if (!McpSecurityChecker.IsToolAllowed(toolName))
|
||||
{
|
||||
throw new McpSecurityException(toolName, "Tool is blocked by security settings");
|
||||
}
|
||||
|
||||
Stopwatch mainThreadHopStopwatch = Stopwatch.StartNew();
|
||||
await MainThreadSwitcher.SwitchToMainThread();
|
||||
mainThreadHopStopwatch.Stop();
|
||||
|
||||
Stopwatch toolBodyStopwatch = Stopwatch.StartNew();
|
||||
BaseToolResponse response = await tool.ExecuteAsync(paramsToken);
|
||||
toolBodyStopwatch.Stop();
|
||||
|
||||
AppendExecuteDynamicCodeTimingsIfSupported(
|
||||
response,
|
||||
$"[Perf] RegistryMainThreadHop: {mainThreadHopStopwatch.Elapsed.TotalMilliseconds:F1}ms",
|
||||
$"[Perf] ToolBody: {toolBodyStopwatch.Elapsed.TotalMilliseconds:F1}ms");
|
||||
return response;
|
||||
}
|
||||
|
||||
private static void AppendExecuteDynamicCodeTimingsIfSupported(
|
||||
BaseToolResponse response,
|
||||
params string[] timingEntries)
|
||||
{
|
||||
if (response is not ExecuteDynamicCodeResponse executeDynamicCodeResponse)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (executeDynamicCodeResponse.Timings == null)
|
||||
{
|
||||
executeDynamicCodeResponse.Timings = new List<string>();
|
||||
}
|
||||
|
||||
executeDynamicCodeResponse.Timings.AddRange(timingEntries);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get detailed information of registered _tools
|
||||
/// </summary>
|
||||
/// <returns>Array of tool information</returns>
|
||||
public ToolInfo[] GetRegisteredTools()
|
||||
{
|
||||
return _tools.Values
|
||||
.Where(tool => ToolExecutionAvailability.ShouldExposeInRegisteredTools(tool.ToolName))
|
||||
.Select(tool =>
|
||||
{
|
||||
// Check if tool has McpTool attribute with DisplayDevelopmentOnly
|
||||
bool displayDevelopmentOnly = false;
|
||||
McpToolAttribute attribute = tool.GetType().GetCustomAttribute<McpToolAttribute>();
|
||||
if (attribute != null)
|
||||
{
|
||||
displayDevelopmentOnly = attribute.DisplayDevelopmentOnly;
|
||||
}
|
||||
|
||||
// Check security settings
|
||||
bool isAllowed = McpSecurityChecker.IsToolAllowed(tool.ToolName);
|
||||
|
||||
// Get description from attribute
|
||||
string description = attribute?.Description ?? "";
|
||||
|
||||
// Modify description for blocked _tools
|
||||
if (!isAllowed)
|
||||
{
|
||||
description = $"[BLOCKED] {description} - Blocked by security settings";
|
||||
}
|
||||
|
||||
return new ToolInfo(tool.ToolName, description, tool.ParameterSchema, displayDevelopmentOnly);
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all tools including disabled ones. Used by Tool Settings UI.
|
||||
/// </summary>
|
||||
public ToolInfo[] GetAllRegisteredToolInfos()
|
||||
{
|
||||
return _tools.Values.Select(tool =>
|
||||
{
|
||||
McpToolAttribute attribute = tool.GetType().GetCustomAttribute<McpToolAttribute>();
|
||||
string description = attribute?.Description ?? "";
|
||||
bool displayDevelopmentOnly = attribute?.DisplayDevelopmentOnly ?? false;
|
||||
return new ToolInfo(tool.ToolName, description, tool.ParameterSchema, displayDevelopmentOnly);
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public ToolSettingsCatalogItem[] GetToolSettingsCatalog()
|
||||
{
|
||||
return _tools.Values.Select(tool =>
|
||||
{
|
||||
Type toolType = tool.GetType();
|
||||
McpToolAttribute attribute = toolType.GetCustomAttribute<McpToolAttribute>();
|
||||
string description = attribute?.Description ?? "";
|
||||
bool displayDevelopmentOnly = attribute?.DisplayDevelopmentOnly ?? false;
|
||||
bool isThirdParty = IsThirdPartyAssembly(toolType.Assembly.GetName().Name);
|
||||
|
||||
return new ToolSettingsCatalogItem(
|
||||
tool.ToolName,
|
||||
description,
|
||||
displayDevelopmentOnly,
|
||||
isThirdParty);
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a tool belongs to a third-party assembly.
|
||||
/// </summary>
|
||||
public bool IsThirdPartyTool(string toolName)
|
||||
{
|
||||
if (!_tools.TryGetValue(toolName, out IUnityTool tool))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
string assemblyName = tool.GetType().Assembly.GetName().Name;
|
||||
return IsThirdPartyAssembly(assemblyName);
|
||||
}
|
||||
|
||||
private static bool IsThirdPartyAssembly(string assemblyName)
|
||||
{
|
||||
return assemblyName != "uLoopMCP.Editor";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get tool type by name for security checking
|
||||
/// </summary>
|
||||
/// <param name="toolName">Tool name</param>
|
||||
/// <returns>Tool type or null if not found</returns>
|
||||
public Type GetToolType(string toolName)
|
||||
{
|
||||
if (_tools.TryGetValue(toolName, out IUnityTool tool))
|
||||
{
|
||||
return tool.GetType();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if specified tool is registered
|
||||
/// </summary>
|
||||
/// <param name="toolName">Tool name</param>
|
||||
/// <returns>True if registered</returns>
|
||||
public bool IsToolRegistered(string toolName)
|
||||
{
|
||||
return _tools.ContainsKey(toolName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually trigger _tools changed notification
|
||||
/// Used for manual notifications and post-compilation notifications
|
||||
/// </summary>
|
||||
public static void TriggerToolsChangedNotification()
|
||||
{
|
||||
// Call the public method in McpServerController
|
||||
McpServerController.TriggerToolChangeNotification();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class representing tool information
|
||||
/// </summary>
|
||||
public class ToolInfo
|
||||
{
|
||||
[JsonProperty("name")] public string Name { get; }
|
||||
|
||||
[JsonProperty("description")] public string Description { get; }
|
||||
|
||||
[JsonProperty("parameterSchema")] public ToolParameterSchema ParameterSchema { get; }
|
||||
|
||||
[JsonProperty("displayDevelopmentOnly")] public bool DisplayDevelopmentOnly { get; }
|
||||
|
||||
public ToolInfo(string name, string description, ToolParameterSchema parameterSchema, bool displayDevelopmentOnly = false)
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
ParameterSchema = parameterSchema;
|
||||
DisplayDevelopmentOnly = displayDevelopmentOnly;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight registry metadata used by Tool Settings UI without generating parameter schemas.
|
||||
/// </summary>
|
||||
public class ToolSettingsCatalogItem
|
||||
{
|
||||
public readonly string Name;
|
||||
public readonly string Description;
|
||||
public readonly bool DisplayDevelopmentOnly;
|
||||
public readonly bool IsThirdParty;
|
||||
|
||||
public ToolSettingsCatalogItem(
|
||||
string name,
|
||||
string description,
|
||||
bool displayDevelopmentOnly,
|
||||
bool isThirdParty)
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
DisplayDevelopmentOnly = displayDevelopmentOnly;
|
||||
IsThirdParty = isThirdParty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b13155ca2a4cd42b2b0c2997a7925511
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,43 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Data model representing validation result
|
||||
/// Used in Application Service Layer
|
||||
/// </summary>
|
||||
public class ValidationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether validation was successful
|
||||
/// </summary>
|
||||
public bool IsValid { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message when validation fails
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create ValidationResult
|
||||
/// </summary>
|
||||
/// <param name="isValid">Validation result</param>
|
||||
/// <param name="errorMessage">Error message (null on success)</param>
|
||||
public ValidationResult(bool isValid, string errorMessage = null)
|
||||
{
|
||||
IsValid = isValid;
|
||||
ErrorMessage = errorMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create success result
|
||||
/// </summary>
|
||||
/// <returns>ValidationResult representing success</returns>
|
||||
public static ValidationResult Success() => new(true);
|
||||
|
||||
/// <summary>
|
||||
/// Create failure result
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">Error message</param>
|
||||
/// <returns>ValidationResult representing failure</returns>
|
||||
public static ValidationResult Failure(string errorMessage) => new(false, errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de99bb9f98ad74025bd8be4e41a953af
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dbe57138c4a024b33bfe815d180e9068
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Response for debug sleep tool execution
|
||||
/// Related classes:
|
||||
/// - SleepTool: Implementation that generates this response
|
||||
/// - SleepSchema: Input parameters for the sleep tool
|
||||
/// - BaseToolResponse: Base class providing timing information
|
||||
/// </summary>
|
||||
public class SleepResponse : BaseToolResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Message indicating the result of the sleep operation
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Actual number of seconds the tool slept before completion or timeout
|
||||
/// </summary>
|
||||
public int ActualSleepSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the operation completed successfully or was cancelled
|
||||
/// </summary>
|
||||
public bool WasCancelled { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 28eaf066ee0a94a999dd67f9f4baabcd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Schema for debug sleep tool parameters
|
||||
/// Related classes:
|
||||
/// - SleepTool: Implementation of the debug sleep tool
|
||||
/// - SleepResponse: Response structure for the sleep tool
|
||||
/// </summary>
|
||||
public class SleepSchema : BaseToolSchema
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of seconds to sleep for testing purposes (default: 15)
|
||||
/// </summary>
|
||||
[Description("Number of seconds to sleep for testing purposes (default: 15)")]
|
||||
public int SleepSeconds { get; set; } = 15;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 65adc65e8b27d41e889d4ccb67be267b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Debug tool that sleeps for specified duration to test timeout functionality
|
||||
/// Related classes:
|
||||
/// - SleepSchema: Input parameters including sleep duration and timeout
|
||||
/// - SleepResponse: Output containing results and timing information
|
||||
/// - AbstractUnityTool: Base class providing timeout infrastructure
|
||||
/// </summary>
|
||||
[McpTool(
|
||||
DisplayDevelopmentOnly = true,
|
||||
Description = "Debug tool that sleeps for specified duration to test timeout functionality"
|
||||
)]
|
||||
public class SleepTool : AbstractUnityTool<SleepSchema, SleepResponse>
|
||||
{
|
||||
public override string ToolName => "debug-sleep";
|
||||
|
||||
protected override async Task<SleepResponse> ExecuteAsync(SleepSchema parameters, CancellationToken cancellationToken)
|
||||
{
|
||||
int actualSleepSeconds = 0;
|
||||
|
||||
// Sleep 1 second at a time, checking for cancellation between each sleep
|
||||
for (int i = 0; i < parameters.SleepSeconds; i++)
|
||||
{
|
||||
// Check for cancellation before each sleep iteration
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Sleep for 1 second with cancellation support
|
||||
await Task.Delay(1000, cancellationToken);
|
||||
|
||||
actualSleepSeconds = i + 1;
|
||||
}
|
||||
|
||||
return new SleepResponse
|
||||
{
|
||||
Message = $"Successfully slept for {actualSleepSeconds} seconds",
|
||||
ActualSleepSeconds = actualSleepSeconds,
|
||||
WasCancelled = false
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3819fd5fb05cb4309a2229a93f79f74a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ae802852e2dfd47dc979543d41bb00d2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,31 @@
|
||||
# execute-dynamic-code design notes
|
||||
|
||||
This directory owns the user-facing `execute-dynamic-code` tool contract.
|
||||
The public contract must stay stable even when the internal compiler strategy changes.
|
||||
|
||||
## Fast path intent
|
||||
|
||||
- The preferred compilation path is Unity-bundled Roslyn.
|
||||
- `SharedRoslynCompilerWorkerHost` keeps a warm compiler process so structural cache misses stay fast.
|
||||
- The shared worker is an optimization only. It must not own response shaping, security validation, or tool-specific policy.
|
||||
|
||||
## Prewarm intent
|
||||
|
||||
- Auto prewarm exists to hide first-use compiler startup cost after server startup or recovery.
|
||||
- Prewarm must go through the same runtime facade used by normal requests.
|
||||
- Prewarm must be single-flight. It should never create a parallel execution lane or bypass runtime idleness checks.
|
||||
- Prewarm is allowed to improve latency only. It must not change observable tool behavior.
|
||||
|
||||
## Fallback intent
|
||||
|
||||
- Preferred order is: shared Roslyn worker -> one-shot Roslyn -> AssemblyBuilder fallback.
|
||||
- Fallback changes performance, not semantics.
|
||||
- Any fallback from the ideal fast path is a maintainer-visible error condition and must be logged through `DynamicCompilationHealthMonitor`.
|
||||
|
||||
## Invariants
|
||||
|
||||
- Security validation always runs in Unity after compilation and before execution.
|
||||
- In restricted mode, metadata validation and IL validation must both complete before `Assembly.Load`.
|
||||
- Tool entry points and use cases depend on runtime-facing facades, not compiler backend details.
|
||||
- Worker state is disposable. Domain reload or worker protocol failure must be handled by rebuilding state, not by preserving it.
|
||||
- The compile backend may change, but `ExecuteDynamicCodeResponse` shape and user-facing behavior must remain stable.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a32019de73d24a92b23228d40898acf
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,98 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Response for dynamic code execution tool
|
||||
|
||||
/// Related classes: ExecuteDynamicCodeTool, ExecuteDynamicCodeSchema
|
||||
/// </summary>
|
||||
public class ExecuteDynamicCodeResponse : BaseToolResponse
|
||||
{
|
||||
private const bool EmitTimingsInJsonResponses = false;
|
||||
|
||||
/// <summary>Execution success flag</summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>Execution result</summary>
|
||||
public string Result { get; set; }
|
||||
|
||||
/// <summary>Log messages</summary>
|
||||
public List<string> Logs { get; set; } = new();
|
||||
|
||||
/// <summary>Compilation errors</summary>
|
||||
public List<CompilationErrorDto> CompilationErrors { get; set; } = new();
|
||||
|
||||
/// <summary>Error message (on failure)</summary>
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>Current security level</summary>
|
||||
public string SecurityLevel { get; set; }
|
||||
|
||||
/// <summary>Error message (alias for ErrorMessage)</summary>
|
||||
public string Error
|
||||
{
|
||||
get => ErrorMessage;
|
||||
set => ErrorMessage = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Code formatted for compilation
|
||||
/// (After extracting/moving using statements and applying class/method wrapping)
|
||||
/// Allows checking the actual compiled code during debugging
|
||||
/// </summary>
|
||||
public string UpdatedCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Summary of diagnostics (unique count, total count, first error brief)
|
||||
/// </summary>
|
||||
public string DiagnosticsSummary { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Structured diagnostics for rich clients (line/column/code/message/hint/suggestions)
|
||||
/// </summary>
|
||||
public List<CompilationErrorDto> Diagnostics { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight internal timings for benchmark comparison.
|
||||
/// </summary>
|
||||
public List<string> Timings { get; set; } = new();
|
||||
|
||||
// Keep timings available in memory for diagnostics and readiness decisions
|
||||
// while avoiding noisy default payloads for normal execute-dynamic-code users.
|
||||
public bool ShouldSerializeTimings()
|
||||
{
|
||||
return EmitTimingsInJsonResponses;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO for compilation error information
|
||||
/// </summary>
|
||||
public class CompilationErrorDto
|
||||
{
|
||||
/// <summary>Error message</summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>Line number</summary>
|
||||
public int Line { get; set; }
|
||||
|
||||
/// <summary>Column number</summary>
|
||||
public int Column { get; set; }
|
||||
|
||||
/// <summary>Compiler error code (e.g., CS0103)</summary>
|
||||
public string ErrorCode { get; set; }
|
||||
|
||||
/// <summary>Optional hint for resolving the error</summary>
|
||||
public string Hint { get; set; }
|
||||
|
||||
/// <summary>Suggested fixes (e.g., add using or qualify)</summary>
|
||||
public List<string> Suggestions { get; set; } = new();
|
||||
|
||||
/// <summary>Context lines around the error with a caret pointer</summary>
|
||||
public string Context { get; set; }
|
||||
|
||||
/// <summary>Pointer column for caret rendering (1-based)</summary>
|
||||
public int PointerColumn { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 88dbc381725914ce488e21b9b27a2cb8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal static class ExecuteDynamicCodeResponseTimingAugmenter
|
||||
{
|
||||
public static void AppendTimingEntries(
|
||||
ExecuteDynamicCodeResponse response,
|
||||
double mainThreadWaitMilliseconds,
|
||||
double toolTotalMilliseconds,
|
||||
double requestTotalMilliseconds)
|
||||
{
|
||||
if (response == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.Timings == null)
|
||||
{
|
||||
response.Timings = new List<string>();
|
||||
}
|
||||
|
||||
response.Timings.Add($"[Perf] MainThreadWait: {mainThreadWaitMilliseconds:F1}ms");
|
||||
response.Timings.Add($"[Perf] ToolTotal: {toolTotalMilliseconds:F1}ms");
|
||||
response.Timings.Add($"[Perf] RequestTotal: {requestTotalMilliseconds:F1}ms");
|
||||
response.Timings.AddRange(DynamicCodeStartupTelemetry.CreateTimingEntries());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8683636c860b448cf93c98208c36946b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameter schema for dynamic code execution tool
|
||||
/// Related classes: ExecuteDynamicCodeTool, ExecuteDynamicCodeResponse
|
||||
/// </summary>
|
||||
public class ExecuteDynamicCodeSchema : BaseToolSchema
|
||||
{
|
||||
/// <summary>C# code to execute</summary>
|
||||
[Description(@"Editor automation only — no file I/O, no script authoring.
|
||||
|
||||
Direct statements only (no classes/namespaces/methods); return is optional (auto 'return null;' if omitted).
|
||||
|
||||
You may include using directives at the top; they are hoisted above the wrapper.
|
||||
Example:
|
||||
using UnityEngine;
|
||||
var x = Mathf.PI;
|
||||
return x;
|
||||
|
||||
Do:
|
||||
- Prefab/material wiring (PrefabUtility)
|
||||
- AddComponent + reference wiring (SerializedObject)
|
||||
- Scene/hierarchy edits
|
||||
|
||||
Don’t:
|
||||
- System.IO.* (File/Directory/Path)
|
||||
- AssetDatabase.CreateFolder / file writes
|
||||
- Create/edit .cs/.asmdef (use Terminal/IDE instead)
|
||||
|
||||
Need files/dirs? Run terminal commands.")]
|
||||
public string Code { get; set; } = "";
|
||||
|
||||
/// <summary>Runtime parameters (advanced; usually unnecessary)</summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Advanced)]
|
||||
[Description(@"Advanced, optional parameters passed to the snippet.
|
||||
|
||||
- For throwaway snippets, prefer defining locals directly in code
|
||||
- Keys are not preserved; values map to parameters[""param0""], parameters[""param1""], ...
|
||||
- Use when injecting UnityEngine.Object or sensitive values, or reusing a snippet with varying data
|
||||
- Use object values supported by the Editor; prefer fully-qualified type names for ambiguous refs (e.g., UnityEngine.Object)
|
||||
|
||||
- Input type must be an OBJECT. Strings like ""{}"" are invalid.
|
||||
- Do: omit 'Parameters' entirely, or use {}
|
||||
- Don't: ""{}"" (string), ""{""a"":1}"" (string)
|
||||
|
||||
Examples:
|
||||
OK: ""Parameters"": {}
|
||||
OK: (omit the 'Parameters' property)
|
||||
NG: ""Parameters"": ""{}""")]
|
||||
public Dictionary<string, object> Parameters { get; set; } = new();
|
||||
|
||||
/// <summary>Compile only (do not execute)</summary>
|
||||
[Description(@"Compile only (no execution).
|
||||
|
||||
- Uses Roslyn validation to surface diagnostics
|
||||
- For new MonoBehaviours: create .cs → compile (mcp__uLoopMCP__compile with ForceRecompile=false) → ensure ErrorCount=0 → AddComponent")]
|
||||
public bool CompileOnly { get; set; } = false;
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Advanced)]
|
||||
public bool YieldToForegroundRequests { get; set; } = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96f1941ea53e64fbf9dca3d1b273ae72
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// MCP Dynamic C# Code Execution Tool.
|
||||
/// Delegates workflow orchestration to the dedicated execute-dynamic-code use case.
|
||||
/// </summary>
|
||||
[McpTool(Description = @"Execute C# code dynamically in Unity Editor for editor automation.
|
||||
|
||||
Use find-game-objects first for basic selected GameObject discovery or property inspection. Use this after built-in inspection tools are not enough or when you need to modify Unity state.
|
||||
|
||||
Direct statements only (no classes/namespaces/methods); return is optional (auto 'return null;' if omitted).
|
||||
|
||||
You may include using directives at the top; they are hoisted above the wrapper.
|
||||
Example:
|
||||
using UnityEngine;
|
||||
var x = Mathf.PI;
|
||||
return x;
|
||||
|
||||
Do:
|
||||
- Prefab/material wiring (PrefabUtility)
|
||||
- AddComponent + reference wiring (SerializedObject)
|
||||
- Scene/hierarchy edits
|
||||
|
||||
Don't:
|
||||
- System.IO.* (File/Directory/Path)
|
||||
- AssetDatabase.CreateFolder / file writes
|
||||
- Create/edit .cs/.asmdef (use Terminal/IDE instead)
|
||||
|
||||
Need files/dirs? Run terminal commands.
|
||||
|
||||
See examples at {project_root}/.claude/skills/unity-cli-loop/uloop-execute-dynamic-code/examples/")]
|
||||
public class ExecuteDynamicCodeTool : AbstractUnityTool<ExecuteDynamicCodeSchema, ExecuteDynamicCodeResponse>
|
||||
{
|
||||
public override string ToolName => "execute-dynamic-code";
|
||||
|
||||
protected override async Task<ExecuteDynamicCodeResponse> ExecuteAsync(
|
||||
ExecuteDynamicCodeSchema parameters,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
IExecuteDynamicCodeUseCase useCase = await DynamicCodeServices.GetExecuteDynamicCodeUseCaseAsync();
|
||||
return await useCase.ExecuteAsync(parameters, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c0a0005c984014fe9ae1227c1e458432
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e4545560316c242f08f6034dc0c57177
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
name: uloop-execute-dynamic-code
|
||||
description: "Execute C# with Unity APIs when existing uloop tools cannot inspect or edit enough. Use for scene, prefab, SerializedObject, AssetDatabase refresh/.meta generation, menu, or PlayMode automation."
|
||||
context: fork
|
||||
---
|
||||
|
||||
# Task
|
||||
|
||||
Execute the following request using `uloop execute-dynamic-code`: $ARGUMENTS
|
||||
|
||||
For basic selected GameObject discovery or property inspection, use `find-game-objects --search-mode Selected` before this tool. Use this tool after the built-in inspection tools are not enough or when you need to modify Unity state.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Read the relevant reference file(s) from the Code Examples section below
|
||||
2. Construct C# code based on the reference examples
|
||||
3. For multiline snippets, write the C# statements to a temporary `.csx` file and execute `uloop execute-dynamic-code --code-file <path>`
|
||||
4. Use `uloop execute-dynamic-code --code '<code>'` only for short one-line snippets
|
||||
5. If execution fails, adjust code and retry
|
||||
6. Report the execution result
|
||||
|
||||
## Parameters
|
||||
|
||||
- `--code '<code>'`: Inline C# statements to execute. Use this for one-line snippets only.
|
||||
- `--code-file <path>`: Read C# statements from a UTF-8 file. Prefer this for multiline snippets, especially on PowerShell, because Windows `.cmd` shims can lose lines from multiline inline arguments before `uloop` receives them.
|
||||
- **Shell quoting**: bash/zsh uses single quotes, for example `uloop execute-dynamic-code --code 'using UnityEngine; return Mathf.PI;'`. PowerShell single-quoted strings can contain normal double quotes, for example `uloop execute-dynamic-code --code 'Debug.Log("Hello!");'`.
|
||||
- `--parameters {}` (advanced, optional): Pass an object when reusing a snippet with varying data or when keeping values outside the code. Values are exposed as `parameters["param0"]`, `parameters["param1"]`, and so on. Omit this flag for most snippets, and pass an object instead of a JSON string.
|
||||
- `--compile-only true` (optional): Compile the snippet without executing it. Use this when you want Roslyn diagnostics before running new code.
|
||||
|
||||
## Code Rules
|
||||
|
||||
Write direct statements only — no class/namespace/method wrappers. Return is optional.
|
||||
|
||||
```csharp
|
||||
using UnityEngine;
|
||||
float x = Mathf.PI;
|
||||
return x;
|
||||
```
|
||||
|
||||
**Forbidden** — these will be rejected at compile time: `System.IO.*`, `AssetDatabase.CreateFolder`, creating/editing `.cs`/`.asmdef` files. Use terminal commands for file operations instead.
|
||||
|
||||
## Output
|
||||
|
||||
Returns JSON:
|
||||
- `Success`: boolean — overall execution success
|
||||
- `Result`: string — value of the snippet's `return` statement (empty when omitted)
|
||||
- `Logs`: string[] — execution diagnostics from the dynamic-code runner, not Unity Console entries
|
||||
- `CompilationErrors`: object[] — Roslyn diagnostics with `Message`, `Line`, `Column`, `ErrorCode`, optional `Hint` and `Suggestions`
|
||||
- `ErrorMessage`: string — top-level failure summary (empty on success)
|
||||
- `Error`: string — alias of `ErrorMessage`
|
||||
- `SecurityLevel`: string — dynamic-code security level active for the request
|
||||
- `UpdatedCode`: string|null — the wrapped form actually compiled (handy when debugging using-statement reordering)
|
||||
- `DiagnosticsSummary`: string|null — compact summary when diagnostics are available
|
||||
- `Diagnostics`: object[] — structured diagnostics; same shape as `CompilationErrors`, usually populated together with it
|
||||
|
||||
Use `uloop get-logs` to retrieve `Debug.Log`, `Debug.LogWarning`, and `Debug.LogError` messages emitted by the snippet. On `Success: false`, inspect `CompilationErrors` first. If empty, read `ErrorMessage` (and `Logs` for extra context) — the failure may be a runtime exception, security violation, cancellation, or an "execution in progress" rejection, all of which return empty `CompilationErrors`. Both EditMode and PlayMode are supported targets — the snippet runs in whichever mode the Editor is currently in.
|
||||
|
||||
## Code Examples by Category
|
||||
|
||||
For detailed code examples, refer to these files:
|
||||
|
||||
- **Prefab operations**: See [references/prefab-operations.md](references/prefab-operations.md)
|
||||
- Create prefabs, instantiate, add components, modify properties
|
||||
- **Material operations**: See [references/material-operations.md](references/material-operations.md)
|
||||
- Create materials, set shaders/textures, modify properties
|
||||
- **Asset operations**: See [references/asset-operations.md](references/asset-operations.md)
|
||||
- Find/search assets, duplicate, move, rename, load
|
||||
- **ScriptableObject**: See [references/scriptableobject.md](references/scriptableobject.md)
|
||||
- Create ScriptableObjects, modify with SerializedObject
|
||||
- **Scene operations**: See [references/scene-operations.md](references/scene-operations.md)
|
||||
- Create/modify GameObjects, set parents, wire references, load scenes
|
||||
- **Batch operations**: See [references/batch-operations.md](references/batch-operations.md)
|
||||
- Bulk modify objects, batch add/remove components, rename, layer/tag/material replacement
|
||||
- **Cleanup operations**: See [references/cleanup-operations.md](references/cleanup-operations.md)
|
||||
- Detect broken scripts, missing references, unused materials, empty GameObjects
|
||||
- **Undo operations**: See [references/undo-operations.md](references/undo-operations.md)
|
||||
- Undo-aware operations: RecordObject, AddComponent, SetParent, grouping
|
||||
- **Selection operations**: See [references/selection-operations.md](references/selection-operations.md)
|
||||
- Get/set selection, multi-select, filter by type/editability
|
||||
- **PlayMode automation (zsh)**: See [references/playmode-automation-zsh.md](references/playmode-automation-zsh.md)
|
||||
- Click UI buttons, invoke methods, set fields, tool combination workflows for zsh users
|
||||
- **PlayMode automation (PowerShell)**: See [references/playmode-automation-powershell.md](references/playmode-automation-powershell.md)
|
||||
- Click UI buttons, invoke methods, set fields, tool combination workflows for PowerShell users
|
||||
- **PlayMode UI controls**: See [references/playmode-ui-controls.md](references/playmode-ui-controls.md)
|
||||
- InputField, Slider, Toggle, Dropdown, drag & drop simulation, list all UI controls
|
||||
- **PlayMode inspection**: See [references/playmode-inspection.md](references/playmode-inspection.md)
|
||||
- Scene info, game state via reflection, physics state, raycast checks, GameObject search, position/rotation
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2445f075f1af349fc8bf73259f1df493
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5155f736569e440ea4679a66a5ee36b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user