286 lines
9.2 KiB
C#
286 lines
9.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading.Tasks;
|
|
using Newtonsoft.Json.Linq;
|
|
using UnityEditor;
|
|
using Newtonsoft.Json;
|
|
|
|
namespace io.github.hatayama.uLoopMCP
|
|
{
|
|
/// <summary>
|
|
/// An entry for the MCP communication log.
|
|
/// Log information that includes a request/response pair.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A class for managing MCP communication logs.
|
|
/// </summary>
|
|
public static class McpCommunicationLogger
|
|
{
|
|
// Note: Communication logs are now managed via McpSessionManager
|
|
|
|
private static List<McpCommunicationLogEntry> _logs;
|
|
private static Dictionary<string, PendingRequest> _pendingRequests;
|
|
|
|
/// <summary>
|
|
/// Event for when the log is updated (for UI updates).
|
|
/// </summary>
|
|
public static event Action OnLogUpdated;
|
|
|
|
/// <summary>
|
|
/// Static constructor (automatically executed after a domain reload).
|
|
/// </summary>
|
|
static McpCommunicationLogger()
|
|
{
|
|
LoadFromSessionState();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Information about pending requests.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all logs.
|
|
/// </summary>
|
|
public static IReadOnlyList<McpCommunicationLogEntry> GetAllLogs()
|
|
{
|
|
lock (_logs)
|
|
{
|
|
return new List<McpCommunicationLogEntry>(_logs).AsReadOnly();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Records a request (awaiting response).
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Records a response (adds it to the log paired with its request).
|
|
/// </summary>
|
|
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
|
|
{
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all logs.
|
|
/// </summary>
|
|
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();
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Restores data from SessionState.
|
|
/// </summary>
|
|
private static void LoadFromSessionState()
|
|
{
|
|
// Restore logs.
|
|
string logsJson = McpEditorSettings.GetCommunicationLogsJson();
|
|
_logs = JsonConvert.DeserializeObject<List<McpCommunicationLogEntry>>(logsJson) ?? new List<McpCommunicationLogEntry>();
|
|
|
|
// Restore pending requests.
|
|
string pendingJson = McpEditorSettings.GetPendingRequestsJson();
|
|
_pendingRequests = JsonConvert.DeserializeObject<Dictionary<string, PendingRequest>>(pendingJson) ?? new Dictionary<string, PendingRequest>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves data to SessionState.
|
|
/// </summary>
|
|
public static void SaveToSessionState()
|
|
{
|
|
// Create snapshots to avoid collection modification during serialization
|
|
List<McpCommunicationLogEntry> logsSnapshot;
|
|
Dictionary<string, PendingRequest> pendingSnapshot;
|
|
|
|
lock (_logs)
|
|
{
|
|
logsSnapshot = new List<McpCommunicationLogEntry>(_logs);
|
|
}
|
|
|
|
lock (_pendingRequests)
|
|
{
|
|
pendingSnapshot = new Dictionary<string, PendingRequest>(_pendingRequests);
|
|
}
|
|
|
|
string logsJson = JsonConvert.SerializeObject(logsSnapshot);
|
|
string pendingJson = JsonConvert.SerializeObject(pendingSnapshot);
|
|
|
|
McpEditorSettings.SetCommunicationLogsJson(logsJson);
|
|
McpEditorSettings.SetPendingRequestsJson(pendingJson);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears SessionState related to communication logs (for fixing log issues).
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Normalize ID to consistent string representation for proper matching
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
} |