using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
namespace LocalMcp.UnityServer
{
/// <summary>
/// MCP API呼び出し結果のログを記録・出力するシステム
/// 成功/失敗/エラーを含む全てのAPI呼び出しをテキストファイルに保存
/// </summary>
public class McpApiLogger
{
private static McpApiLogger _instance;
private static readonly object _lock = new object();
private List<ApiLogEntry> _logHistory = new List<ApiLogEntry>();
private const int MAX_HISTORY = 10000;
private string _logFilePath;
private string _errorLogFilePath;
private bool _isEnabled = true;
private bool _logToFile = true;
private bool _logToConsole = false;
public static McpApiLogger Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new McpApiLogger();
}
}
}
return _instance;
}
}
private McpApiLogger()
{
// ログファイルパスを設定(プロジェクトのLogsフォルダ)
string logsFolder = Path.Combine(Application.dataPath, "..", "Logs", "MCP");
if (!Directory.Exists(logsFolder))
{
Directory.CreateDirectory(logsFolder);
}
string timestamp = DateTime.Now.ToString("yyyyMMdd");
_logFilePath = Path.Combine(logsFolder, $"mcp_api_{timestamp}.log");
_errorLogFilePath = Path.Combine(logsFolder, $"mcp_api_errors_{timestamp}.log");
// セッション開始をログ
WriteToFile(_logFilePath, $"\n{'=',-80}\n[SESSION START] {DateTime.Now:yyyy-MM-dd HH:mm:ss}\n{'=',-80}\n");
}
/// <summary>
/// API呼び出しをログに記録
/// </summary>
public void Log(string method, string parameters, string result, ApiLogStatus status, string errorMessage = null, long durationMs = 0)
{
if (!_isEnabled) return;
var entry = new ApiLogEntry
{
Timestamp = DateTime.Now,
Method = method,
Parameters = parameters,
Result = result,
Status = status,
ErrorMessage = errorMessage,
DurationMs = durationMs
};
lock (_logHistory)
{
_logHistory.Add(entry);
if (_logHistory.Count > MAX_HISTORY)
{
_logHistory.RemoveAt(0);
}
}
// ファイル出力
if (_logToFile)
{
WriteLogEntry(entry);
}
// Consoleにも出力(オプション)
if (_logToConsole)
{
LogToConsole(entry);
}
}
/// <summary>
/// API呼び出し成功をログ
/// </summary>
public void LogSuccess(string method, string parameters, string result, long durationMs = 0)
{
Log(method, parameters, result, ApiLogStatus.Success, null, durationMs);
}
/// <summary>
/// API呼び出しエラーをログ
/// </summary>
public void LogError(string method, string parameters, string errorMessage, long durationMs = 0)
{
Log(method, parameters, null, ApiLogStatus.Error, errorMessage, durationMs);
}
/// <summary>
/// API呼び出し警告をログ(成功したが問題あり)
/// </summary>
public void LogWarning(string method, string parameters, string result, string warningMessage, long durationMs = 0)
{
Log(method, parameters, result, ApiLogStatus.Warning, warningMessage, durationMs);
}
private void WriteLogEntry(ApiLogEntry entry)
{
string logLine = FormatLogEntry(entry);
WriteToFile(_logFilePath, logLine);
// エラーは別ファイルにも記録
if (entry.Status == ApiLogStatus.Error)
{
WriteToFile(_errorLogFilePath, logLine);
}
}
private string FormatLogEntry(ApiLogEntry entry)
{
string statusIcon = entry.Status switch
{
ApiLogStatus.Success => "✓",
ApiLogStatus.Error => "✗",
ApiLogStatus.Warning => "⚠",
_ => "?"
};
string header = $"[{entry.Timestamp:HH:mm:ss.fff}] [{statusIcon}] {entry.Method} ({entry.DurationMs}ms)";
var sb = new System.Text.StringBuilder();
sb.AppendLine(header);
// パラメータ(長すぎる場合は省略)
if (!string.IsNullOrEmpty(entry.Parameters))
{
string paramDisplay = entry.Parameters.Length > 500
? entry.Parameters.Substring(0, 500) + "..."
: entry.Parameters;
sb.AppendLine($" Params: {paramDisplay}");
}
// エラーメッセージ
if (!string.IsNullOrEmpty(entry.ErrorMessage))
{
sb.AppendLine($" Error: {entry.ErrorMessage}");
}
// 結果(エラー時以外、長すぎる場合は省略)
if (entry.Status != ApiLogStatus.Error && !string.IsNullOrEmpty(entry.Result))
{
string resultDisplay = entry.Result.Length > 300
? entry.Result.Substring(0, 300) + "..."
: entry.Result;
sb.AppendLine($" Result: {resultDisplay}");
}
sb.AppendLine();
return sb.ToString();
}
private void WriteToFile(string path, string content)
{
try
{
lock (_lock)
{
File.AppendAllText(path, content);
}
}
catch (Exception e)
{
Debug.LogError($"[MCP ApiLogger] Failed to write log: {e.Message}");
}
}
private void LogToConsole(ApiLogEntry entry)
{
string message = $"[MCP API] {entry.Method}: {entry.Status}";
switch (entry.Status)
{
case ApiLogStatus.Success:
Debug.Log(message);
break;
case ApiLogStatus.Warning:
Debug.LogWarning($"{message} - {entry.ErrorMessage}");
break;
case ApiLogStatus.Error:
Debug.LogError($"{message} - {entry.ErrorMessage}");
break;
}
}
#region API Methods (MCP経由で呼び出し可能)
/// <summary>
/// ログ履歴を取得
/// </summary>
public static string GetLogs(string paramsJson, object id)
{
try
{
int count = ExtractIntParam(paramsJson, "count", 100);
string statusFilter = ExtractStringParam(paramsJson, "status"); // success, error, warning, all
string methodFilter = ExtractStringParam(paramsJson, "method");
var logs = Instance.GetLogHistory(count, statusFilter, methodFilter);
var logStrings = new List<string>();
foreach (var log in logs)
{
logStrings.Add(log.ToJson());
}
string result = $"{{\"logs\": [{string.Join(",", logStrings)}], \"total\": {logs.Count}, \"logFile\": \"{EscapeJson(Instance._logFilePath)}\"}}";
return JsonRpcResponseHelper.SuccessMessage(result, id);
}
catch (Exception e)
{
return JsonRpcResponseHelper.ErrorMessage($"Failed to get logs: {e.Message}", id);
}
}
/// <summary>
/// エラーログのみ取得
/// </summary>
public static string GetErrors(string paramsJson, object id)
{
try
{
int count = ExtractIntParam(paramsJson, "count", 50);
string methodFilter = ExtractStringParam(paramsJson, "method");
var errors = Instance.GetLogHistory(count, "error", methodFilter);
var logStrings = new List<string>();
foreach (var log in errors)
{
logStrings.Add(log.ToJson());
}
string result = $"{{\"errors\": [{string.Join(",", logStrings)}], \"total\": {errors.Count}, \"errorLogFile\": \"{EscapeJson(Instance._errorLogFilePath)}\"}}";
return JsonRpcResponseHelper.SuccessMessage(result, id);
}
catch (Exception e)
{
return JsonRpcResponseHelper.ErrorMessage($"Failed to get errors: {e.Message}", id);
}
}
/// <summary>
/// ログ統計を取得
/// </summary>
public static string GetStats(string paramsJson, object id)
{
try
{
var stats = Instance.CalculateStats();
return JsonRpcResponseHelper.SuccessMessage(stats, id);
}
catch (Exception e)
{
return JsonRpcResponseHelper.ErrorMessage($"Failed to get stats: {e.Message}", id);
}
}
/// <summary>
/// ログをクリア
/// </summary>
public static string ClearLogs(string paramsJson, object id)
{
try
{
lock (Instance._logHistory)
{
Instance._logHistory.Clear();
}
Instance.WriteToFile(Instance._logFilePath, $"\n[LOGS CLEARED] {DateTime.Now:yyyy-MM-dd HH:mm:ss}\n\n");
return JsonRpcResponseHelper.SuccessMessage("{\"success\": true, \"message\": \"Logs cleared\"}", id);
}
catch (Exception e)
{
return JsonRpcResponseHelper.ErrorMessage($"Failed to clear logs: {e.Message}", id);
}
}
/// <summary>
/// ログ設定を変更
/// </summary>
public static string Configure(string paramsJson, object id)
{
try
{
bool? enabled = ExtractBoolParam(paramsJson, "enabled");
bool? logToFile = ExtractBoolParam(paramsJson, "logToFile");
bool? logToConsole = ExtractBoolParam(paramsJson, "logToConsole");
if (enabled.HasValue) Instance._isEnabled = enabled.Value;
if (logToFile.HasValue) Instance._logToFile = logToFile.Value;
if (logToConsole.HasValue) Instance._logToConsole = logToConsole.Value;
string result = $"{{\"enabled\": {Instance._isEnabled.ToString().ToLower()}, \"logToFile\": {Instance._logToFile.ToString().ToLower()}, \"logToConsole\": {Instance._logToConsole.ToString().ToLower()}}}";
return JsonRpcResponseHelper.SuccessMessage(result, id);
}
catch (Exception e)
{
return JsonRpcResponseHelper.ErrorMessage($"Failed to configure: {e.Message}", id);
}
}
/// <summary>
/// ログファイルパスを取得
/// </summary>
public static string GetLogFilePath(string paramsJson, object id)
{
try
{
string result = $"{{\"logFile\": \"{EscapeJson(Instance._logFilePath)}\", \"errorLogFile\": \"{EscapeJson(Instance._errorLogFilePath)}\"}}";
return JsonRpcResponseHelper.SuccessMessage(result, id);
}
catch (Exception e)
{
return JsonRpcResponseHelper.ErrorMessage($"Failed to get log file path: {e.Message}", id);
}
}
#endregion
#region Internal Methods
private List<ApiLogEntry> GetLogHistory(int count, string statusFilter, string methodFilter)
{
var result = new List<ApiLogEntry>();
lock (_logHistory)
{
for (int i = _logHistory.Count - 1; i >= 0 && result.Count < count; i--)
{
var entry = _logHistory[i];
// ステータスフィルタ
if (!string.IsNullOrEmpty(statusFilter) && statusFilter != "all")
{
if (statusFilter == "error" && entry.Status != ApiLogStatus.Error) continue;
if (statusFilter == "warning" && entry.Status != ApiLogStatus.Warning) continue;
if (statusFilter == "success" && entry.Status != ApiLogStatus.Success) continue;
}
// メソッドフィルタ
if (!string.IsNullOrEmpty(methodFilter))
{
if (!entry.Method.Contains(methodFilter)) continue;
}
result.Add(entry);
}
}
return result;
}
private string CalculateStats()
{
int totalCalls = 0;
int successCount = 0;
int errorCount = 0;
int warningCount = 0;
long totalDuration = 0;
var methodStats = new Dictionary<string, MethodStats>();
lock (_logHistory)
{
foreach (var entry in _logHistory)
{
totalCalls++;
totalDuration += entry.DurationMs;
switch (entry.Status)
{
case ApiLogStatus.Success: successCount++; break;
case ApiLogStatus.Error: errorCount++; break;
case ApiLogStatus.Warning: warningCount++; break;
}
// メソッド別統計
if (!methodStats.ContainsKey(entry.Method))
{
methodStats[entry.Method] = new MethodStats { Method = entry.Method };
}
var ms = methodStats[entry.Method];
ms.CallCount++;
ms.TotalDuration += entry.DurationMs;
if (entry.Status == ApiLogStatus.Error) ms.ErrorCount++;
}
}
// 最もエラーが多いメソッドTop5
var topErrors = new List<string>();
var sortedByErrors = new List<MethodStats>(methodStats.Values);
sortedByErrors.Sort((a, b) => b.ErrorCount.CompareTo(a.ErrorCount));
for (int i = 0; i < Math.Min(5, sortedByErrors.Count); i++)
{
if (sortedByErrors[i].ErrorCount > 0)
{
topErrors.Add($"{{\"method\": \"{sortedByErrors[i].Method}\", \"errorCount\": {sortedByErrors[i].ErrorCount}}}");
}
}
double avgDuration = totalCalls > 0 ? (double)totalDuration / totalCalls : 0;
double successRate = totalCalls > 0 ? (double)successCount / totalCalls * 100 : 0;
return $"{{\"totalCalls\": {totalCalls}, \"successCount\": {successCount}, \"errorCount\": {errorCount}, \"warningCount\": {warningCount}, \"successRate\": {successRate:F1}, \"avgDurationMs\": {avgDuration:F1}, \"topErrorMethods\": [{string.Join(",", topErrors)}]}}";
}
#endregion
#region Helper Methods
private static string ExtractStringParam(string paramsJson, string paramName)
{
try
{
string pattern = $"\"{paramName}\"\\s*:\\s*\"([^\"]+)\"";
var match = System.Text.RegularExpressions.Regex.Match(paramsJson, pattern);
if (match.Success) return match.Groups[1].Value;
}
catch { }
return null;
}
private static int ExtractIntParam(string paramsJson, string paramName, int defaultValue)
{
try
{
string pattern = $"\"{paramName}\"\\s*:\\s*(\\d+)";
var match = System.Text.RegularExpressions.Regex.Match(paramsJson, pattern);
if (match.Success) return int.Parse(match.Groups[1].Value);
}
catch { }
return defaultValue;
}
private static bool? ExtractBoolParam(string paramsJson, string paramName)
{
try
{
string pattern = $"\"{paramName}\"\\s*:\\s*(true|false)";
var match = System.Text.RegularExpressions.Regex.Match(paramsJson, pattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
if (match.Success) return match.Groups[1].Value.ToLower() == "true";
}
catch { }
return null;
}
private static string EscapeJson(string str)
{
if (str == null) return "";
return str.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r");
}
#endregion
private class MethodStats
{
public string Method;
public int CallCount;
public int ErrorCount;
public long TotalDuration;
}
}
/// <summary>
/// APIログのステータス
/// </summary>
public enum ApiLogStatus
{
Success,
Error,
Warning
}
/// <summary>
/// APIログエントリ
/// </summary>
public class ApiLogEntry
{
public DateTime Timestamp;
public string Method;
public string Parameters;
public string Result;
public ApiLogStatus Status;
public string ErrorMessage;
public long DurationMs;
public string ToJson()
{
var parts = new List<string>
{
$"\"timestamp\": \"{Timestamp:yyyy-MM-dd HH:mm:ss.fff}\"",
$"\"method\": \"{EscapeJson(Method)}\"",
$"\"status\": \"{Status.ToString().ToLower()}\"",
$"\"durationMs\": {DurationMs}"
};
if (!string.IsNullOrEmpty(ErrorMessage))
{
parts.Add($"\"errorMessage\": \"{EscapeJson(ErrorMessage)}\"");
}
if (!string.IsNullOrEmpty(Parameters))
{
// パラメータは長すぎる場合省略
string paramDisplay = Parameters.Length > 200 ? Parameters.Substring(0, 200) + "..." : Parameters;
parts.Add($"\"parameters\": \"{EscapeJson(paramDisplay)}\"");
}
return "{" + string.Join(", ", parts) + "}";
}
private static string EscapeJson(string str)
{
if (str == null) return "";
return str.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r");
}
}
}