using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using UnityEditor;
using Newtonsoft.Json;
namespace io.github.hatayama.uLoopMCP
{
///
/// An entry for the MCP communication log.
/// Log information that includes a request/response pair.
///
public record McpCommunicationLogEntry
{
public readonly string CommandName;
public readonly DateTime Timestamp;
public readonly string Request;
public readonly string Response;
public readonly bool IsError;
public bool IsExpanded { get; set; }
public string HeaderText => $"[{CommandName}: {Timestamp:HH:mm:ss}]";
public McpCommunicationLogEntry(string commandName, DateTime timestamp, string request, string response, bool isError, bool isExpanded = false)
{
CommandName = commandName;
Timestamp = timestamp;
Request = request;
Response = response;
IsError = isError;
IsExpanded = isExpanded;
}
}
///
/// A class for managing MCP communication logs.
///
public static class McpCommunicationLogger
{
// Note: Communication logs are now managed via McpSessionManager
private static List _logs;
private static Dictionary _pendingRequests;
///
/// Event for when the log is updated (for UI updates).
///
public static event Action OnLogUpdated;
///
/// Static constructor (automatically executed after a domain reload).
///
static McpCommunicationLogger()
{
LoadFromSessionState();
}
///
/// Information about pending requests.
///
private record PendingRequest
{
public readonly string CommandName;
public readonly DateTime Timestamp;
public readonly string RequestJson;
public PendingRequest(string commandName, DateTime timestamp, string requestJson)
{
CommandName = commandName;
Timestamp = timestamp;
RequestJson = requestJson;
}
}
///
/// Gets all logs.
///
public static IReadOnlyList GetAllLogs()
{
lock (_logs)
{
return new List(_logs).AsReadOnly();
}
}
///
/// Records a request (awaiting response).
///
public static async Task LogRequest(string jsonRequest)
{
// Skip if communication logs are disabled
if (!McpEditorSettings.GetSettings().enableCommunicationLogs)
{
return;
}
JObject request = JObject.Parse(jsonRequest);
string method = request["method"]?.ToString() ?? "unknown";
string id = NormalizeId(request["id"]);
PendingRequest pendingRequest = new(method, DateTime.Now, jsonRequest);
// Store in memory immediately (on any thread)
lock (_pendingRequests)
{
_pendingRequests[id] = pendingRequest;
}
// Switch to main thread for SessionState operations
await MainThreadSwitcher.SwitchToMainThread();
SaveToSessionState();
OnLogUpdated?.Invoke();
}
///
/// Records a response (adds it to the log paired with its request).
///
public static async Task RecordLogResponse(string jsonResponse)
{
// Skip if communication logs are disabled
if (!McpEditorSettings.GetSettings().enableCommunicationLogs)
{
return;
}
JObject response = JObject.Parse(jsonResponse);
string id = NormalizeId(response["id"]);
PendingRequest pendingRequest;
bool foundPendingRequest;
// Process response in memory immediately (on any thread)
lock (_pendingRequests)
{
foundPendingRequest = _pendingRequests.TryGetValue(id, out pendingRequest);
if (foundPendingRequest)
{
_pendingRequests.Remove(id);
}
}
if (foundPendingRequest)
{
bool isError = response["error"] != null;
// Create new logs in a closed state.
McpCommunicationLogEntry logEntry = new(
pendingRequest.CommandName,
pendingRequest.Timestamp,
pendingRequest.RequestJson,
jsonResponse,
isError,
false // Start with the toggle closed.
);
lock (_logs)
{
// If there are existing logs, close them all (before adding a new log).
foreach (McpCommunicationLogEntry existingLog in _logs)
{
existingLog.IsExpanded = false;
}
_logs.Add(logEntry);
// Remove old entries if exceeding maximum
while (_logs.Count > McpUIConstants.MAX_COMMUNICATION_LOG_ENTRIES)
{
_logs.RemoveAt(0); // Remove oldest entry
}
}
// Switch to main thread for SessionState operations
await MainThreadSwitcher.SwitchToMainThread();
SaveToSessionState();
OnLogUpdated?.Invoke();
}
else
{
}
}
///
/// Clears all logs.
///
public static void ClearLogs()
{
lock (_logs)
{
_logs.Clear();
}
lock (_pendingRequests)
{
_pendingRequests.Clear();
}
// Also completely delete from SessionState and execute UI update on the main thread.
EditorApplication.delayCall += () =>
{
ClearLogSessionState();
OnLogUpdated?.Invoke();
};
}
///
/// Restores data from SessionState.
///
private static void LoadFromSessionState()
{
// Restore logs.
string logsJson = McpEditorSettings.GetCommunicationLogsJson();
_logs = JsonConvert.DeserializeObject>(logsJson) ?? new List();
// Restore pending requests.
string pendingJson = McpEditorSettings.GetPendingRequestsJson();
_pendingRequests = JsonConvert.DeserializeObject>(pendingJson) ?? new Dictionary();
}
///
/// Saves data to SessionState.
///
public static void SaveToSessionState()
{
// Create snapshots to avoid collection modification during serialization
List logsSnapshot;
Dictionary pendingSnapshot;
lock (_logs)
{
logsSnapshot = new List(_logs);
}
lock (_pendingRequests)
{
pendingSnapshot = new Dictionary(_pendingRequests);
}
string logsJson = JsonConvert.SerializeObject(logsSnapshot);
string pendingJson = JsonConvert.SerializeObject(pendingSnapshot);
McpEditorSettings.SetCommunicationLogsJson(logsJson);
McpEditorSettings.SetPendingRequestsJson(pendingJson);
}
///
/// Clears SessionState related to communication logs (for fixing log issues).
///
public static void ClearLogSessionState()
{
McpEditorSettings.ClearCommunicationLogs();
lock (_logs)
{
_logs?.Clear();
}
lock (_pendingRequests)
{
_pendingRequests?.Clear();
}
// SessionState clear complete (no log output).
// Notify UI of update.
EditorApplication.delayCall += () => OnLogUpdated?.Invoke();
}
///
/// Normalize ID to consistent string representation for proper matching
///
private static string NormalizeId(JToken idToken)
{
if (idToken == null)
{
return "unknown";
}
// Convert both string and numeric IDs to string for consistent matching
return idToken.ToString();
}
}
}