Skip to main content
Glama
McpApiLogger.cs19.5 kB
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"); } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dsgarage/UniMCP4CC'

If you have feedback or need assistance with the MCP directory API, please join our Discord server