using System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace io.github.hatayama.uLoopMCP { /// /// Current client context for JSON-RPC processing /// public class ClientExecutionContext { public string Endpoint { get; } public ClientExecutionContext(string endpoint) { Endpoint = endpoint; } } /// /// 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 /// public static class JsonRpcProcessor { /// /// Current client context for async operations /// private static readonly AsyncLocal _currentClientContext = new(); /// /// Get current client context (ProcessID and Endpoint) /// public static ClientExecutionContext CurrentClientContext => _currentClientContext.Value; /// /// Process JSON-RPC request and generate response with client context /// public static async Task ProcessRequest(string jsonRequest, string clientEndpoint) { var context = new ClientExecutionContext(clientEndpoint); _currentClientContext.Value = context; try { return await ProcessRequest(jsonRequest); } finally { _currentClientContext.Value = null; } } /// /// Process JSON-RPC request and generate response /// public static async Task 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); } } /// /// Parse JSON-RPC request string into structured object /// 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(), 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() }; } /// /// 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) /// 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; } } /// /// 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. /// private static void HandleFocusWindowNotification() { // Intentionally empty - focus-window is handled by TypeScript MCP server at OS level } /// /// Process RPC request and return response JSON /// private static async Task 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); } /// /// Create JSON-RPC success response /// /// Request ID - must be same type as received (string/number/null per JSON-RPC spec) /// Command execution result 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); } } /// /// Create JSON-RPC error response /// /// Request ID - must be same type as received (string/number/null per JSON-RPC spec) /// Exception to convert to error response 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); } /// /// Execute appropriate handler according to method name /// Use new command-based structure /// private static async Task ExecuteMethod(string method, JToken paramsToken) { return await UnityApiHandler.ExecuteCommandAsync(method, paramsToken); } } /// /// Constants for JSON-RPC error types /// public static class JsonRpcErrorTypes { public const string SecurityBlocked = "security_blocked"; public const string InternalError = "internal_error"; } /// /// Base class for JSON-RPC error data /// public abstract class JsonRpcErrorData { public abstract string type { get; } public string message { get; protected set; } protected JsonRpcErrorData(string message) { this.message = message; } } /// /// Error data for security blocked commands /// 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; } } /// /// Error data for internal errors /// public class InternalErrorData : JsonRpcErrorData { public override string type => JsonRpcErrorTypes.InternalError; public InternalErrorData(string message) : base(message) { } } /// /// JSON-RPC error object /// 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; } } /// /// JSON-RPC success response /// 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; } } /// /// JSON-RPC error response /// 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; } } }