Skip to main content
Glama
McpClient.cs61.3 kB
using Godot; using System; using System.Collections.Generic; using System.Net; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; using System.Linq; using FileAccess = Godot.FileAccess; /// <summary> /// MCP HTTP API 服务器 v4.1 - 线程安全版本,所有场景树操作在主线程执行 /// </summary> public partial class McpClient : Node { private HttpListener? _httpListener; private bool _isRunning = false; private const string ApiUrl = "http://127.0.0.1:7777/"; // 增强日志系统 - 环形缓冲区 private readonly LinkedList<LogEntry> _logBuffer = new(); private const int MaxLogBufferSize = 1000; private const string LogFilePath = "user://mcp_logs.txt"; private readonly List<LogEntry> _logs = new(); // 保留向后兼容 private bool _autoLogGDPrint = true; // 自动记录 GD.Print // 信号监听系统 - 全局监听 private readonly List<SignalEvent> _signalEventsBuffer = new(); private const int MaxSignalEventsBufferSize = 5000; private const string SignalEventsFilePath = "user://mcp_signal_events.txt"; private bool _isGlobalSignalMonitoring = false; private readonly HashSet<string> _monitoredSignals = new(); // 监听的信号名称过滤 // 线程安全的请求队列 private readonly Queue<PendingRequest> _requestQueue = new(); private readonly object _queueLock = new(); private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; public override void _Ready() { // 重置日志和信号事件文件 InitializeLogFile(); InitializeSignalEventsFile(); // 开启全局信号监听 StartGlobalSignalMonitoring(); GD.Print("=".PadRight(60, '=')); GD.Print("[MCP] Godot MCP v5.0 - 完整调试系统"); GD.Print("=".PadRight(60, '=')); GD.Print("[MCP] ✓ 全局信号监听已启动"); GD.Print("[MCP] ✓ 日志自动记录已启动"); StartServer(); } public override void _Process(double delta) { // 在主线程处理所有待处理的请求 ProcessPendingRequests(); // 自动捕获控制台输出 (通过重定向 GD.Print 实现需要引擎支持) // 这里我们使用定期轮询的方式 } private async void StartServer() { try { _httpListener = new HttpListener(); _httpListener.Prefixes.Add(ApiUrl); _httpListener.Start(); _isRunning = true; GD.Print($"[MCP] ✓ 监听: {ApiUrl}"); GD.Print("[MCP] ✓ 48 个独立HTTP端点已就绪 (场景树+信号+日志)"); _ = Task.Run(HandleRequestsAsync); } catch (Exception ex) { GD.PrintErr($"[MCP] ✗ 启动失败: {ex.Message}"); } } private async Task HandleRequestsAsync() { while (_isRunning && _httpListener != null) { try { var context = await _httpListener.GetContextAsync(); _ = Task.Run(() => ProcessRequestAsync(context)); } catch { } } } private async Task ProcessRequestAsync(HttpListenerContext context) { try { if (context.Request.HttpMethod != "POST") { context.Response.StatusCode = 405; context.Response.Close(); return; } var path = context.Request.Url?.AbsolutePath ?? "/"; GD.Print($"[MCP] {path}"); using var reader = new System.IO.StreamReader(context.Request.InputStream); var body = await reader.ReadToEndAsync(); // 创建待处理请求并等待主线程处理 var pendingRequest = new PendingRequest { Path = path, Body = body, CompletionSource = new TaskCompletionSource<ApiResponse>() }; lock (_queueLock) { _requestQueue.Enqueue(pendingRequest); } // 等待主线程处理完成 var response = await pendingRequest.CompletionSource.Task; var json = JsonSerializer.Serialize(response, JsonOptions); var bytes = Encoding.UTF8.GetBytes(json); context.Response.ContentType = "application/json; charset=utf-8"; context.Response.ContentLength64 = bytes.Length; await context.Response.OutputStream.WriteAsync(bytes, 0, bytes.Length); context.Response.Close(); } catch (Exception ex) { GD.PrintErr($"[MCP] Error: {ex.Message}"); context.Response.StatusCode = 500; context.Response.Close(); } } /// <summary> /// 在主线程处理所有待处理的请求 /// </summary> private void ProcessPendingRequests() { while (true) { PendingRequest? request = null; lock (_queueLock) { if (_requestQueue.Count > 0) { request = _requestQueue.Dequeue(); } } if (request == null) break; try { var response = RouteRequest(request.Path, request.Body); request.CompletionSource.SetResult(response); } catch (Exception ex) { request.CompletionSource.SetResult(ErrorResponse(ex.Message)); } } } /// <summary> /// 根据 HTTP 路径路由到对应方法 /// </summary> private ApiResponse RouteRequest(string path, string body) { try { return path switch { "/get_scene_tree" => GetSceneTree(Deserialize<SceneTreeRequest>(body)), "/get_node_info" => GetNodeInfo(Deserialize<NodePathRequest>(body)), "/create_node" => CreateNode(Deserialize<CreateNodeRequest>(body)), "/delete_node" => DeleteNode(Deserialize<NodePathRequest>(body)), "/load_scene" => LoadScene(Deserialize<ScenePathRequest>(body)), "/get_property" => GetProperty(Deserialize<PropertyRequest>(body)), "/set_property" => SetProperty(Deserialize<SetPropertyRequest>(body)), "/list_properties" => ListProperties(Deserialize<NodePathRequest>(body)), "/call_method" => CallMethod(Deserialize<CallMethodRequest>(body)), "/list_methods" => ListMethods(Deserialize<NodePathRequest>(body)), "/execute_csharp" => ExecuteCSharp(Deserialize<CodeRequest>(body)), "/get_global_variables" => GetGlobalVariables(), "/get_resource_info" => GetResourceInfo(Deserialize<ResourcePathRequest>(body)), "/list_resources" => ListResources(Deserialize<ListResourcesRequest>(body)), "/load_resource" => LoadResource(Deserialize<ResourcePathRequest>(body)), "/get_performance_stats" => GetPerformanceStats(), "/get_logs" => GetLogs(Deserialize<LogsRequest>(body)), "/take_screenshot" => TakeScreenshot(Deserialize<ScreenshotRequest>(body)), "/get_time" => GetTime(), "/get_node_children" => GetNodeChildren(Deserialize<NodePathRequest>(body)), "/get_node_parent" => GetNodeParent(Deserialize<NodePathRequest>(body)), "/find_nodes_by_type" => FindNodesByType(Deserialize<FindNodesRequest>(body)), "/find_nodes_by_name" => FindNodesByName(Deserialize<FindNodesRequest>(body)), "/find_nodes_by_group" => FindNodesByGroup(Deserialize<FindNodesRequest>(body)), "/get_node_ancestors" => GetNodeAncestors(Deserialize<AncestorsRequest>(body)), "/get_scene_tree_stats" => GetSceneTreeStats(Deserialize<NodePathRequest>(body)), "/node_exists" => NodeExists(Deserialize<NodePathRequest>(body)), "/get_node_subtree" => GetNodeSubtree(Deserialize<SubtreeRequest>(body)), "/search_nodes" => SearchNodes(Deserialize<FindNodesRequest>(body)), "/get_node_context" => GetNodeContext(Deserialize<NodeContextRequest>(body)), // 简化场景树 "/get_scene_tree_simple" => GetSceneTreeSimple(Deserialize<SimpleTreeRequest>(body)), // 信号系统 "/get_node_signals" => GetNodeSignals(Deserialize<NodePathRequest>(body)), "/get_signal_connections" => GetSignalConnections(Deserialize<SignalConnectionRequest>(body)), "/connect_signal" => ConnectSignal(Deserialize<SignalConnectionRequest>(body)), "/disconnect_signal" => DisconnectSignal(Deserialize<SignalConnectionRequest>(body)), "/emit_signal" => EmitSignal(Deserialize<SignalEmitRequest>(body)), "/start_signal_monitoring" => StartSignalMonitoring(Deserialize<SignalMonitorRequest>(body)), "/stop_signal_monitoring" => StopSignalMonitoring(), "/get_signal_events" => GetSignalEvents(Deserialize<SignalEventQueryRequest>(body)), "/clear_signal_events" => ClearSignalEvents(), // 增强日志系统 "/get_logs_filtered" => GetLogsFiltered(Deserialize<LogFilterRequest>(body)), "/get_log_stats" => GetLogStats(), "/export_logs" => ExportLogs(Deserialize<LogExportRequest>(body)), "/clear_logs" => ClearLogs(), "/add_custom_log" => AddCustomLog(Deserialize<CustomLogRequest>(body)), _ => ErrorResponse($"未知端点: {path}") }; } catch (Exception ex) { GD.PrintErr($"[MCP] 路由错误: {ex.Message}"); return ErrorResponse(ex.Message); } } private T Deserialize<T>(string json) where T : new() { try { return JsonSerializer.Deserialize<T>(json, JsonOptions) ?? new T(); } catch { return new T(); } } // ========== 场景树方法 (获取游戏运行时信息) ========== private ApiResponse GetSceneTree(SceneTreeRequest req) { var root = GetTree().Root; // 现在安全了,在主线程执行 var tree = BuildTree(root, req.IncludeProperties); return Ok(tree); } private ApiResponse GetNodeInfo(NodePathRequest req) { var node = GetNodeOrNull(req.NodePath); if (node == null) return Err($"找不到节点: {req.NodePath}"); return Ok(new NodeInfo { Name = node.Name, Type = node.GetType().Name, Path = node.GetPath().ToString(), Children = node.GetChildren().Select(c => c.Name.ToString()).ToList(), Properties = GetProps(node), Methods = GetMethods(node), Signals = GetSignals(node) }); } private ApiResponse CreateNode(CreateNodeRequest req) { var parent = GetNodeOrNull(req.ParentPath); if (parent == null) return Err($"父节点不存在: {req.ParentPath}"); var node = CreateByType(req.NodeType); if (node == null) return Err($"无法创建类型: {req.NodeType}"); node.Name = req.NodeName; parent.AddChild(node); return Ok(new { path = node.GetPath().ToString() }); } private ApiResponse DeleteNode(NodePathRequest req) { var node = GetNodeOrNull(req.NodePath); if (node == null) return Err($"节点不存在: {req.NodePath}"); node.QueueFree(); return Ok("已删除"); } private ApiResponse LoadScene(ScenePathRequest req) { GetTree().CallDeferred("change_scene_to_file", req.ScenePath); return Ok($"加载中: {req.ScenePath}"); } // ========== 属性方法 (游戏运行时属性) ========== private ApiResponse GetProperty(PropertyRequest req) { var node = GetNodeOrNull(req.NodePath); if (node == null) return Err($"节点不存在: {req.NodePath}"); var value = node.Get(req.PropertyName); // 获取运行时属性值 return Ok(new { value = Conv(value) }); } private ApiResponse SetProperty(SetPropertyRequest req) { var node = GetNodeOrNull(req.NodePath); if (node == null) return Err($"节点不存在: {req.NodePath}"); node.Set(req.PropertyName, ToVar(req.Value)); // 设置运行时属性 return Ok($"已设置: {req.PropertyName}"); } private ApiResponse ListProperties(NodePathRequest req) { var node = GetNodeOrNull(req.NodePath); if (node == null) return Err($"节点不存在: {req.NodePath}"); return Ok(GetProps(node)); } // ========== 方法调用 (运行时方法) ========== private ApiResponse CallMethod(CallMethodRequest req) { var node = GetNodeOrNull(req.NodePath); if (node == null) return Err($"节点不存在: {req.NodePath}"); var args = new Godot.Collections.Array(); req.Args?.ForEach(a => args.Add(ToVar(a))); var result = node.Call(req.MethodName, args); // 运行时调用 return Ok(Conv(result)); } private ApiResponse ListMethods(NodePathRequest req) { var node = GetNodeOrNull(req.NodePath); if (node == null) return Err($"节点不存在: {req.NodePath}"); return Ok(GetMethods(node)); } // ========== 脚本和资源 ========== private ApiResponse ExecuteCSharp(CodeRequest req) => Ok("需要 Roslyn"); private ApiResponse GetGlobalVariables() { return Ok(GetTree().Root.GetChildren().ToDictionary( c => c.Name.ToString(), c => (object)new { type = c.GetType().Name, path = c.GetPath().ToString() } )); } private ApiResponse GetResourceInfo(ResourcePathRequest req) { var r = GD.Load(req.ResourcePath); return r == null ? Err("资源不存在") : Ok(new { type = r.GetType().Name, path = r.ResourcePath }); } private ApiResponse ListResources(ListResourcesRequest req) { var list = new List<string>(); var dir = DirAccess.Open(req.Path); if (dir != null) { dir.ListDirBegin(); for (var f = dir.GetNext(); f != ""; f = dir.GetNext()) if (!dir.CurrentIsDir()) list.Add(f); dir.ListDirEnd(); } return Ok(list); } private ApiResponse LoadResource(ResourcePathRequest req) { var r = GD.Load(req.ResourcePath); return r == null ? Err("加载失败") : Ok(new { type = r.GetType().Name }); } // ========== 调试方法 (运行时统计) ========== private ApiResponse GetPerformanceStats() { return Ok(new { fps = Engine.GetFramesPerSecond(), // 实时 FPS processTime = Performance.GetMonitor(Performance.Monitor.TimeProcess), physicsTime = Performance.GetMonitor(Performance.Monitor.TimePhysicsProcess), memoryUsage = Performance.GetMonitor(Performance.Monitor.MemoryStatic), // 实时内存 nodeCount = Performance.GetMonitor(Performance.Monitor.ObjectNodeCount), // 实时节点数 drawCalls = Performance.GetMonitor(Performance.Monitor.RenderTotalDrawCallsInFrame) }); } private ApiResponse GetLogs(LogsRequest req) => Ok(_logs.TakeLast(req.Count)); private ApiResponse TakeScreenshot(ScreenshotRequest req) { var path = req.SavePath ?? "user://screenshot.png"; var img = GetViewport().GetTexture().GetImage(); // 当前画面截图 img.SavePng(path); return Ok(new { path }); } private ApiResponse GetTime() => Ok(new { unix = Time.GetUnixTimeFromSystem(), datetime = DateTime.Now.ToString("s"), ticks = Time.GetTicksMsec() }); // ========== 扩展场景树查询方法 ========== private ApiResponse GetNodeChildren(NodePathRequest req) { var node = GetNodeOrNull(req.NodePath); if (node == null) return Err($"节点不存在: {req.NodePath}"); var children = node.GetChildren().Select(c => new { name = c.Name.ToString(), type = c.GetType().Name, path = c.GetPath().ToString() }).ToList(); return Ok(children); } private ApiResponse GetNodeParent(NodePathRequest req) { var node = GetNodeOrNull(req.NodePath); if (node == null) return Err($"节点不存在: {req.NodePath}"); var parent = node.GetParent(); if (parent == null) return Ok(null); return Ok(new { name = parent.Name.ToString(), type = parent.GetType().Name, path = parent.GetPath().ToString() }); } private ApiResponse FindNodesByType(FindNodesRequest req) { var root = GetNodeOrNull(req.RootPath); if (root == null) return Err($"根节点不存在: {req.RootPath}"); var results = new List<object>(); FindNodesByTypeRecursive(root, req.NodeType ?? "", results); return Ok(new { count = results.Count, nodes = results }); } private void FindNodesByTypeRecursive(Node node, string targetType, List<object> results) { if (node.GetType().Name == targetType) { results.Add(new { name = node.Name.ToString(), type = node.GetType().Name, path = node.GetPath().ToString() }); } foreach (Node child in node.GetChildren()) { FindNodesByTypeRecursive(child, targetType, results); } } private ApiResponse FindNodesByName(FindNodesRequest req) { var root = GetNodeOrNull(req.RootPath); if (root == null) return Err($"根节点不存在: {req.RootPath}"); var pattern = req.NamePattern ?? ""; var results = new List<object>(); var comparison = req.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; FindNodesByNameRecursive(root, pattern, results, req.ExactMatch, comparison, req.MaxResults); return Ok(new { count = results.Count, nodes = results, truncated = results.Count >= req.MaxResults }); } private void FindNodesByNameRecursive(Node node, string pattern, List<object> results, bool exactMatch, StringComparison comparison, int maxResults) { if (results.Count >= maxResults) return; var nodeName = node.Name.ToString(); bool matches = exactMatch ? nodeName.Equals(pattern, comparison) : nodeName.Contains(pattern, comparison); if (matches) { results.Add(new { name = nodeName, type = node.GetType().Name, path = node.GetPath().ToString() }); } foreach (Node child in node.GetChildren()) { if (results.Count >= maxResults) break; FindNodesByNameRecursive(child, pattern, results, exactMatch, comparison, maxResults); } } private ApiResponse FindNodesByGroup(FindNodesRequest req) { var root = GetNodeOrNull(req.RootPath); if (root == null) return Err($"根节点不存在: {req.RootPath}"); var groupName = req.GroupName ?? ""; var nodes = GetTree().GetNodesInGroup(groupName); var results = nodes.Select(n => new { name = n.Name.ToString(), type = n.GetType().Name, path = n.GetPath().ToString(), groups = n.GetGroups().Select(g => g.ToString()).ToList() }).Take(req.MaxResults).ToList(); return Ok(new { count = results.Count, nodes = results, groupName }); } private ApiResponse GetNodeAncestors(AncestorsRequest req) { var node = GetNodeOrNull(req.NodePath); if (node == null) return Err($"节点不存在: {req.NodePath}"); var ancestors = new List<object>(); var current = node.GetParent(); var level = 0; while (current != null && (req.Levels < 0 || level < req.Levels)) { var ancestorInfo = new Dictionary<string, object> { ["level"] = level + 1, ["name"] = current.Name.ToString(), ["type"] = current.GetType().Name, ["path"] = current.GetPath().ToString() }; if (req.IncludeSiblings) { var parent = current.GetParent(); if (parent != null) { var siblings = parent.GetChildren() .Where(c => c.GetPath() != current.GetPath()) .Select(c => new { name = c.Name.ToString(), type = c.GetType().Name, path = c.GetPath().ToString() }).ToList(); ancestorInfo["siblings"] = siblings; ancestorInfo["siblingCount"] = siblings.Count; } } ancestors.Add(ancestorInfo); current = current.GetParent(); level++; } return Ok(new { nodePath = req.NodePath, ancestorCount = ancestors.Count, ancestors }); } private ApiResponse GetSceneTreeStats(NodePathRequest req) { var root = GetNodeOrNull(req.NodePath); if (root == null) return Err($"节点不存在: {req.NodePath}"); var stats = new Dictionary<string, int>(); var groups = new Dictionary<string, int>(); var totalNodes = 0; var maxDepth = 0; CollectStatsRecursive(root, stats, groups, ref totalNodes, ref maxDepth, 0); return Ok(new { totalNodes, maxDepth, nodesByType = stats.OrderByDescending(x => x.Value).ToDictionary(x => x.Key, x => x.Value), nodesByGroup = groups.OrderByDescending(x => x.Value).ToDictionary(x => x.Key, x => x.Value), rootPath = req.NodePath }); } private void CollectStatsRecursive(Node node, Dictionary<string, int> stats, Dictionary<string, int> groups, ref int total, ref int maxDepth, int currentDepth) { total++; if (currentDepth > maxDepth) maxDepth = currentDepth; var type = node.GetType().Name; stats[type] = stats.GetValueOrDefault(type, 0) + 1; foreach (var group in node.GetGroups()) { var groupName = group.ToString(); groups[groupName] = groups.GetValueOrDefault(groupName, 0) + 1; } foreach (Node child in node.GetChildren()) { CollectStatsRecursive(child, stats, groups, ref total, ref maxDepth, currentDepth + 1); } } private ApiResponse NodeExists(NodePathRequest req) { var node = GetNodeOrNull(req.NodePath); return Ok(new { exists = node != null, path = req.NodePath }); } private ApiResponse GetNodeSubtree(SubtreeRequest req) { var root = GetNodeOrNull(req.NodePath); if (root == null) return Err($"节点不存在: {req.NodePath}"); var tree = BuildTreeWithDepth(root, req.MaxDepth, 0, req.IncludeProperties); return Ok(tree); } private Dictionary<string, object> BuildTreeWithDepth(Node n, int maxDepth, int currentDepth, bool includeProperties) { var d = new Dictionary<string, object> { ["name"] = n.Name, ["type"] = n.GetType().Name, ["path"] = n.GetPath().ToString(), ["depth"] = currentDepth }; if (includeProperties) { d["properties"] = GetProps(n); } if (maxDepth < 0 || currentDepth < maxDepth) { d["children"] = n.GetChildren() .Select(c => BuildTreeWithDepth(c, maxDepth, currentDepth + 1, includeProperties)) .ToList(); } else { d["children"] = new List<object>(); d["hasChildren"] = n.GetChildCount() > 0; d["childCount"] = n.GetChildCount(); } return d; } private ApiResponse SearchNodes(FindNodesRequest req) { var root = GetNodeOrNull(req.RootPath); if (root == null) return Err($"根节点不存在: {req.RootPath}"); var results = new List<object>(); SearchNodesRecursive(root, req, results); return Ok(new { count = results.Count, nodes = results, truncated = results.Count >= req.MaxResults, criteria = new { namePattern = req.NamePattern, nodeType = req.NodeType, groupName = req.GroupName } }); } private void SearchNodesRecursive(Node node, FindNodesRequest req, List<object> results) { if (results.Count >= req.MaxResults) return; bool matches = true; // 检查名称匹配 if (!string.IsNullOrEmpty(req.NamePattern)) { var comparison = req.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; var nodeName = node.Name.ToString(); matches = req.ExactMatch ? nodeName.Equals(req.NamePattern, comparison) : nodeName.Contains(req.NamePattern, comparison); } // 检查类型匹配 if (matches && !string.IsNullOrEmpty(req.NodeType)) { matches = node.GetType().Name == req.NodeType; } // 检查组匹配 if (matches && !string.IsNullOrEmpty(req.GroupName)) { matches = node.IsInGroup(req.GroupName); } if (matches) { results.Add(new { name = node.Name.ToString(), type = node.GetType().Name, path = node.GetPath().ToString(), groups = node.GetGroups().Select(g => g.ToString()).ToList() }); } foreach (Node child in node.GetChildren()) { if (results.Count >= req.MaxResults) break; SearchNodesRecursive(child, req, results); } } private ApiResponse GetNodeContext(NodeContextRequest req) { var node = GetNodeOrNull(req.NodePath); if (node == null) return Err($"节点不存在: {req.NodePath}"); var context = new Dictionary<string, object> { ["node"] = new { name = node.Name.ToString(), type = node.GetType().Name, path = node.GetPath().ToString(), groups = node.GetGroups().Select(g => g.ToString()).ToList() } }; if (req.IncludeParent) { var parent = node.GetParent(); context["parent"] = parent != null ? new { name = parent.Name.ToString(), type = parent.GetType().Name, path = parent.GetPath().ToString() } : null; } if (req.IncludeSiblings) { var parent = node.GetParent(); if (parent != null) { var siblings = parent.GetChildren() .Where(c => c.GetPath() != node.GetPath()) .Select(c => new { name = c.Name.ToString(), type = c.GetType().Name, path = c.GetPath().ToString() }).ToList(); context["siblings"] = siblings; context["siblingCount"] = siblings.Count; } } if (req.IncludeChildren) { var children = node.GetChildren().Select(c => new { name = c.Name.ToString(), type = c.GetType().Name, path = c.GetPath().ToString(), childCount = c.GetChildCount() }).ToList(); context["children"] = children; context["childCount"] = children.Count; } return Ok(context); } // ========== 简化场景树 ========== private ApiResponse GetSceneTreeSimple(SimpleTreeRequest req) { var root = GetNodeOrNull(req.RootPath); if (root == null) return Err($"节点不存在: {req.RootPath}"); var tree = BuildSimpleTree(root, req.MaxDepth, 0); return Ok(tree); } private Dictionary<string, object> BuildSimpleTree(Node node, int maxDepth, int currentDepth) { var result = new Dictionary<string, object> { ["name"] = node.Name.ToString(), ["type"] = node.GetType().Name }; if (currentDepth < maxDepth) { result["children"] = node.GetChildren() .Select(c => BuildSimpleTree(c, maxDepth, currentDepth + 1)) .ToList(); } return result; } // ========== 信号系统 ========== private ApiResponse GetNodeSignals(NodePathRequest req) { var node = GetNodeOrNull(req.NodePath); if (node == null) return Err($"节点不存在: {req.NodePath}"); var signals = node.GetSignalList() .Select(s => (Godot.Collections.Dictionary)s) .Select(s => new { name = s["name"].AsString(), args = ((Godot.Collections.Array)s["args"]) .Select(a => ((Godot.Collections.Dictionary)a)["name"].AsString()) .ToList() }) .ToList(); return Ok(new { nodePath = req.NodePath, signals }); } private ApiResponse GetSignalConnections(SignalConnectionRequest req) { var node = GetNodeOrNull(req.SourceNodePath); if (node == null) return Err($"节点不存在: {req.SourceNodePath}"); var connections = node.GetSignalConnectionList(req.SignalName) .Select(c => (Godot.Collections.Dictionary)c) .Select(c => new { signal = c["signal"].AsString(), callable = c["callable"].ToString(), target = c.ContainsKey("target") ? c["target"].ToString() : null }) .ToList(); return Ok(new { signal = req.SignalName, connections }); } private ApiResponse ConnectSignal(SignalConnectionRequest req) { var source = GetNodeOrNull(req.SourceNodePath); if (source == null) return Err($"源节点不存在: {req.SourceNodePath}"); if (string.IsNullOrEmpty(req.TargetNodePath) || string.IsNullOrEmpty(req.TargetMethod)) return Err("需要目标节点路径和方法名"); var target = GetNodeOrNull(req.TargetNodePath); if (target == null) return Err($"目标节点不存在: {req.TargetNodePath}"); try { var callable = new Callable(target, req.TargetMethod); source.Connect(req.SignalName, callable); LogInfo($"信号已连接: {req.SourceNodePath}.{req.SignalName} → {req.TargetNodePath}.{req.TargetMethod}"); return Ok(new { connected = true, source = req.SourceNodePath, signal = req.SignalName, target = req.TargetNodePath, method = req.TargetMethod }); } catch (Exception ex) { return Err($"连接信号失败: {ex.Message}"); } } private ApiResponse DisconnectSignal(SignalConnectionRequest req) { var source = GetNodeOrNull(req.SourceNodePath); if (source == null) return Err($"源节点不存在: {req.SourceNodePath}"); if (string.IsNullOrEmpty(req.TargetNodePath) || string.IsNullOrEmpty(req.TargetMethod)) return Err("需要目标节点路径和方法名"); var target = GetNodeOrNull(req.TargetNodePath); if (target == null) return Err($"目标节点不存在: {req.TargetNodePath}"); try { var callable = new Callable(target, req.TargetMethod); source.Disconnect(req.SignalName, callable); LogInfo($"信号已断开: {req.SourceNodePath}.{req.SignalName} ✗ {req.TargetNodePath}.{req.TargetMethod}"); return Ok(new { disconnected = true, source = req.SourceNodePath, signal = req.SignalName }); } catch (Exception ex) { return Err($"断开信号失败: {ex.Message}"); } } private ApiResponse EmitSignal(SignalEmitRequest req) { var node = GetNodeOrNull(req.NodePath); if (node == null) return Err($"节点不存在: {req.NodePath}"); try { if (req.Args == null || req.Args.Count == 0) { node.EmitSignal(req.SignalName); } else { var variants = req.Args.Select(a => ToVar(a)).ToArray(); node.EmitSignal(req.SignalName, variants); } LogInfo($"信号已发射: {req.NodePath}.{req.SignalName}"); return Ok(new { emitted = true, nodePath = req.NodePath, signal = req.SignalName, argCount = req.Args?.Count ?? 0 }); } catch (Exception ex) { return Err($"发射信号失败: {ex.Message}"); } } private ApiResponse StartSignalMonitoring(SignalMonitorRequest req) { // 设置信号过滤 (为空则监听所有) if (!string.IsNullOrEmpty(req.SignalName)) { _monitoredSignals.Add(req.SignalName); } else { _monitoredSignals.Clear(); // 清空过滤器 = 监听所有 } LogInfo($"信号监听过滤器已更新 - 监听信号: {(req.SignalName ?? "所有")}"); return Ok(new { monitoring = _isGlobalSignalMonitoring, monitoredSignals = _monitoredSignals.Count > 0 ? _monitoredSignals.ToList() : new List<string> { "所有信号" }, currentEventsCount = _signalEventsBuffer.Count }); } private ApiResponse StopSignalMonitoring() { var eventCount = _signalEventsBuffer.Count; LogInfo($"信号监听统计 - 共记录 {eventCount} 个事件 (缓冲区)"); return Ok(new { monitoring = _isGlobalSignalMonitoring, note = "全局监听持续运行,可通过设置过滤器控制记录", totalEvents = eventCount }); } private ApiResponse GetSignalEvents(SignalEventQueryRequest req) { // 从缓冲区和文件读取信号事件 var allEvents = ReadAllSignalEvents(); var query = allEvents.AsEnumerable(); // 按节点路径过滤 if (!string.IsNullOrEmpty(req.NodePath)) query = query.Where(e => e.NodePath.Contains(req.NodePath, StringComparison.OrdinalIgnoreCase)); // 按信号名称过滤 if (!string.IsNullOrEmpty(req.SignalName)) query = query.Where(e => e.SignalName.Equals(req.SignalName, StringComparison.OrdinalIgnoreCase)); // 按时间范围过滤 if (req.StartTime.HasValue) { var startTime = DateTimeOffset.FromUnixTimeSeconds(req.StartTime.Value).DateTime; query = query.Where(e => e.Timestamp >= startTime); } if (req.EndTime.HasValue) { var endTime = DateTimeOffset.FromUnixTimeSeconds(req.EndTime.Value).DateTime; query = query.Where(e => e.Timestamp <= endTime); } var events = query.TakeLast(req.Count).ToList(); return Ok(new { totalEvents = allEvents.Count, matchedEvents = query.Count(), returnedEvents = events.Count, events = events.Select(e => new { timestamp = e.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff"), unixTimestamp = new DateTimeOffset(e.Timestamp).ToUnixTimeSeconds(), nodePath = e.NodePath, nodeType = e.NodeType, signalName = e.SignalName, args = e.Args }).ToList() }); } private List<SignalEvent> ReadAllSignalEvents() { var events = new List<SignalEvent>(_signalEventsBuffer); // 从文件读取历史事件 try { using var file = FileAccess.Open(SignalEventsFilePath, FileAccess.ModeFlags.Read); if (file != null) { while (!file.EofReached()) { var line = file.GetLine(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("===")) continue; // 解析格式: [2024-01-01 12:34:56.789] /root/Main (Node2D) :: ready [arg1, arg2] var match = System.Text.RegularExpressions.Regex.Match( line, @"\[([\d\-: .]+)\] (.+?) \((\w+)\) :: (\w+)(?: \[(.+?)\])?" ); if (match.Success) { var argsStr = match.Groups[5].Value; var args = string.IsNullOrEmpty(argsStr) ? new List<string>() : argsStr.Split(", ").ToList(); events.Add(new SignalEvent { Timestamp = DateTime.Parse(match.Groups[1].Value), NodePath = match.Groups[2].Value, NodeType = match.Groups[3].Value, SignalName = match.Groups[4].Value, Args = args }); } } } } catch { } return events.OrderBy(e => e.Timestamp).ToList(); } private ApiResponse ClearSignalEvents() { var bufferCount = _signalEventsBuffer.Count; var fileEvents = ReadAllSignalEvents().Count - bufferCount; _signalEventsBuffer.Clear(); InitializeSignalEventsFile(); LogInfo($"信号事件已清空 - 缓冲区: {bufferCount}, 文件: {fileEvents}"); return Ok(new { cleared = true, bufferCleared = bufferCount, fileReset = true }); } // ========== 增强日志系统 ========== private void InitializeLogFile() { try { using var file = FileAccess.Open(LogFilePath, FileAccess.ModeFlags.Write); if (file != null) { file.StoreString($"=== MCP 日志 - 启动时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss} ===\n"); } } catch (Exception ex) { GD.PrintErr($"[MCP] 初始化日志文件失败: {ex.Message}"); } } private void InitializeSignalEventsFile() { try { using var file = FileAccess.Open(SignalEventsFilePath, FileAccess.ModeFlags.Write); if (file != null) { file.StoreString($"=== MCP 信号事件 - 启动时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss} ===\n"); } } catch (Exception ex) { GD.PrintErr($"[MCP] 初始化信号事件文件失败: {ex.Message}"); } } /// <summary> /// 启动全局信号监听 - 自动记录所有节点的信号触发 /// </summary> private void StartGlobalSignalMonitoring() { _isGlobalSignalMonitoring = true; // 连接到场景树的 node_added 信号,自动监听新添加的节点 GetTree().NodeAdded += OnNodeAddedToTree; // 监听已存在的节点 MonitorExistingNodes(GetTree().Root); } private void OnNodeAddedToTree(Node node) { if (!_isGlobalSignalMonitoring) return; // 为新节点的所有信号添加监听 var signals = node.GetSignalList(); foreach (var signalDict in signals) { var dict = (Godot.Collections.Dictionary)signalDict; var signalName = dict["name"].AsString(); // 检查是否需要监听此信号 if (_monitoredSignals.Count == 0 || _monitoredSignals.Contains(signalName)) { try { node.Connect(signalName, Callable.From(() => RecordSignalEvent(node, signalName, null))); } catch { // 某些信号可能有参数,跳过 } } } } private void MonitorExistingNodes(Node root) { OnNodeAddedToTree(root); foreach (Node child in root.GetChildren()) { MonitorExistingNodes(child); } } /// <summary> /// 记录信号事件 /// </summary> private void RecordSignalEvent(Node node, string signalName, object[]? args) { var evt = new SignalEvent { Timestamp = DateTime.Now, NodePath = node.GetPath().ToString(), NodeType = node.GetType().Name, SignalName = signalName, Args = args?.Select(a => a?.ToString() ?? "null").ToList() ?? new List<string>() }; // 添加到缓冲区 _signalEventsBuffer.Add(evt); // 如果缓冲区满了,写入最旧的事件到文件 if (_signalEventsBuffer.Count > MaxSignalEventsBufferSize) { var oldEvent = _signalEventsBuffer[0]; _signalEventsBuffer.RemoveAt(0); WriteSignalEventToFile(oldEvent); } } private void WriteSignalEventToFile(SignalEvent evt) { try { using var file = FileAccess.Open(SignalEventsFilePath, FileAccess.ModeFlags.ReadWrite); if (file != null) { file.SeekEnd(); var argsStr = evt.Args.Count > 0 ? $" [{string.Join(", ", evt.Args)}]" : ""; file.StoreString($"[{evt.Timestamp:yyyy-MM-dd HH:mm:ss.fff}] {evt.NodePath} ({evt.NodeType}) :: {evt.SignalName}{argsStr}\n"); } } catch { } } private void LogInfo(string message) { AddLog("info", message); GD.Print($"[MCP] {message}"); } private void LogWarning(string message) { AddLog("warning", message); GD.PushWarning($"[MCP] {message}"); } private void LogError(string message) { AddLog("error", message); GD.PrintErr($"[MCP] {message}"); } private void AddLog(string level, string message) { var entry = new LogEntry { Timestamp = DateTime.Now, Level = level, Message = message }; // 添加到环形缓冲区 _logBuffer.AddLast(entry); // 如果超过最大大小,移除最旧的并写入文件 if (_logBuffer.Count > MaxLogBufferSize) { var oldEntry = _logBuffer.First!.Value; _logBuffer.RemoveFirst(); WriteLogToFile(oldEntry); } // 同时添加到旧的 _logs 列表以保持向后兼容 _logs.Add(entry); } private void WriteLogToFile(LogEntry entry) { try { using var file = FileAccess.Open(LogFilePath, FileAccess.ModeFlags.ReadWrite); if (file != null) { file.SeekEnd(); file.StoreString($"[{entry.Timestamp:yyyy-MM-dd HH:mm:ss}] [{entry.Level.ToUpper()}] {entry.Message}\n"); } } catch { } } private ApiResponse GetLogsFiltered(LogFilterRequest req) { var allLogs = _logBuffer.Concat(ReadLogsFromFile()).ToList(); var query = allLogs.AsEnumerable(); if (!string.IsNullOrEmpty(req.Level)) query = query.Where(log => log.Level.Equals(req.Level, StringComparison.OrdinalIgnoreCase)); if (!string.IsNullOrEmpty(req.MessagePattern)) query = query.Where(log => log.Message.Contains(req.MessagePattern, StringComparison.OrdinalIgnoreCase)); if (req.StartTime.HasValue) { var startTime = DateTimeOffset.FromUnixTimeSeconds(req.StartTime.Value).DateTime; query = query.Where(log => log.Timestamp >= startTime); } if (req.EndTime.HasValue) { var endTime = DateTimeOffset.FromUnixTimeSeconds(req.EndTime.Value).DateTime; query = query.Where(log => log.Timestamp <= endTime); } var logs = query.TakeLast(req.MaxCount).ToList(); return Ok(new { totalMatched = logs.Count, logs }); } private List<LogEntry> ReadLogsFromFile() { var logs = new List<LogEntry>(); try { using var file = FileAccess.Open(LogFilePath, FileAccess.ModeFlags.Read); if (file != null) { while (!file.EofReached()) { var line = file.GetLine(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("===")) continue; var match = System.Text.RegularExpressions.Regex.Match( line, @"\[([\d\-: ]+)\] \[(\w+)\] (.+)" ); if (match.Success) { logs.Add(new LogEntry { Timestamp = DateTime.Parse(match.Groups[1].Value), Level = match.Groups[2].Value.ToLower(), Message = match.Groups[3].Value }); } } } } catch { } return logs; } private ApiResponse GetLogStats() { var allLogs = _logBuffer.Concat(ReadLogsFromFile()).ToList(); var stats = new { totalLogs = allLogs.Count, inBuffer = _logBuffer.Count, inFile = allLogs.Count - _logBuffer.Count, byLevel = allLogs.GroupBy(l => l.Level) .ToDictionary(g => g.Key, g => g.Count()), oldestLog = allLogs.Count > 0 ? allLogs.First().Timestamp : (DateTime?)null, newestLog = allLogs.Count > 0 ? allLogs.Last().Timestamp : (DateTime?)null }; return Ok(stats); } private ApiResponse ExportLogs(LogExportRequest req) { var filePath = req.FilePath ?? "user://logs_export.txt"; try { var allLogs = _logBuffer.Concat(ReadLogsFromFile()).ToList(); using var file = FileAccess.Open(filePath, FileAccess.ModeFlags.Write); if (file != null) { file.StoreString($"=== MCP 日志导出 - {DateTime.Now:yyyy-MM-dd HH:mm:ss} ===\n"); file.StoreString($"总计: {allLogs.Count} 条日志\n\n"); foreach (var log in allLogs) { file.StoreString($"[{log.Timestamp:yyyy-MM-dd HH:mm:ss}] [{log.Level.ToUpper()}] {log.Message}\n"); } LogInfo($"日志已导出到: {filePath}"); return Ok(new { exported = true, filePath, logCount = allLogs.Count }); } return Err("无法打开导出文件"); } catch (Exception ex) { return Err($"导出日志失败: {ex.Message}"); } } private ApiResponse ClearLogs() { var bufferCount = _logBuffer.Count; var fileCount = ReadLogsFromFile().Count; _logBuffer.Clear(); _logs.Clear(); InitializeLogFile(); LogInfo($"日志已清空 - 缓冲区: {bufferCount}, 文件: {fileCount}"); return Ok(new { cleared = true, bufferCleared = bufferCount, fileReset = true }); } private ApiResponse AddCustomLog(CustomLogRequest req) { AddLog(req.Level, req.Message); return Ok(new { logged = true, level = req.Level, message = req.Message }); } // ========== 辅助方法 ========== private Dictionary<string, object> BuildTree(Node n, bool props) { var d = new Dictionary<string, object> { ["name"] = n.Name, ["type"] = n.GetType().Name, ["path"] = n.GetPath().ToString(), ["children"] = n.GetChildren().Select(c => BuildTree(c, props)).ToList() }; if (props) d["properties"] = GetProps(n); return d; } private Dictionary<string, object?> GetProps(Node n) => n.GetPropertyList() .Select(p => (Godot.Collections.Dictionary)p) .Where(p => !p["name"].AsString().StartsWith("_")) .ToDictionary(p => p["name"].AsString(), p => { try { return Conv(n.Get(p["name"].AsString())); } catch { return null; } }); private List<string> GetMethods(Node n) => n.GetMethodList() .Select(m => ((Godot.Collections.Dictionary)m)["name"].AsString()) .Where(m => !m.StartsWith("_")) .ToList(); private List<string> GetSignals(Node n) => n.GetSignalList() .Select(s => ((Godot.Collections.Dictionary)s)["name"].AsString()) .ToList(); private Node? CreateByType(string t) { var type = Type.GetType($"Godot.{t}") ?? Type.GetType(t); return type != null && typeof(Node).IsAssignableFrom(type) ? (Node?)Activator.CreateInstance(type) : null; } private object? Conv(Variant v) => v.VariantType switch { Variant.Type.Nil => null, Variant.Type.Bool => v.AsBool(), Variant.Type.Int => v.AsInt64(), Variant.Type.Float => v.AsDouble(), Variant.Type.String => v.AsString(), Variant.Type.Vector2 => new { x = v.AsVector2().X, y = v.AsVector2().Y }, Variant.Type.Vector3 => new { x = v.AsVector3().X, y = v.AsVector3().Y, z = v.AsVector3().Z }, _ => v.ToString() }; private Variant ToVar(object? o) => o switch { null => default, bool b => b, int i => i, long l => l, float f => f, double d => d, string s => s, JsonElement je => je.ValueKind switch { JsonValueKind.Null => default, JsonValueKind.True => true, JsonValueKind.False => false, JsonValueKind.Number => je.GetDouble(), JsonValueKind.String => je.GetString() ?? "", _ => je.ToString() }, _ => o.ToString() ?? "" }; private ApiResponse Ok(object? d) => new() { Success = true, Data = d }; private ApiResponse Err(string msg) => new() { Success = false, Error = msg }; private ApiResponse ErrorResponse(string msg) => new() { Success = false, Error = msg }; public override void _ExitTree() { _isRunning = false; _httpListener?.Stop(); _httpListener?.Close(); GD.Print("[MCP] 已停止"); } } // ========== 强类型请求模型 ========== public class SceneTreeRequest { [JsonPropertyName("includeProperties")] public bool IncludeProperties { get; set; } } public class NodePathRequest { [JsonPropertyName("nodePath")] public string NodePath { get; set; } = ""; } public class CreateNodeRequest { [JsonPropertyName("parentPath")] public string ParentPath { get; set; } = ""; [JsonPropertyName("nodeType")] public string NodeType { get; set; } = ""; [JsonPropertyName("nodeName")] public string NodeName { get; set; } = ""; } public class ScenePathRequest { [JsonPropertyName("scenePath")] public string ScenePath { get; set; } = ""; } public class PropertyRequest { [JsonPropertyName("nodePath")] public string NodePath { get; set; } = ""; [JsonPropertyName("propertyName")] public string PropertyName { get; set; } = ""; } public class SetPropertyRequest { [JsonPropertyName("nodePath")] public string NodePath { get; set; } = ""; [JsonPropertyName("propertyName")] public string PropertyName { get; set; } = ""; [JsonPropertyName("value")] public object? Value { get; set; } } public class CallMethodRequest { [JsonPropertyName("nodePath")] public string NodePath { get; set; } = ""; [JsonPropertyName("methodName")] public string MethodName { get; set; } = ""; [JsonPropertyName("args")] public List<object>? Args { get; set; } } public class CodeRequest { [JsonPropertyName("code")] public string Code { get; set; } = ""; } public class ResourcePathRequest { [JsonPropertyName("resourcePath")] public string ResourcePath { get; set; } = ""; } public class ListResourcesRequest { [JsonPropertyName("path")] public string Path { get; set; } = "res://"; [JsonPropertyName("filter")] public string? Filter { get; set; } } public class LogsRequest { [JsonPropertyName("count")] public int Count { get; set; } = 50; } public class ScreenshotRequest { [JsonPropertyName("savePath")] public string? SavePath { get; set; } } public class FindNodesRequest { [JsonPropertyName("nodeType")] public string? NodeType { get; set; } [JsonPropertyName("namePattern")] public string? NamePattern { get; set; } [JsonPropertyName("rootPath")] public string RootPath { get; set; } = "/root"; [JsonPropertyName("caseSensitive")] public bool CaseSensitive { get; set; } = false; [JsonPropertyName("exactMatch")] public bool ExactMatch { get; set; } = false; [JsonPropertyName("groupName")] public string? GroupName { get; set; } [JsonPropertyName("maxResults")] public int MaxResults { get; set; } = 50; } public class SubtreeRequest { [JsonPropertyName("nodePath")] public string NodePath { get; set; } = ""; [JsonPropertyName("maxDepth")] public int MaxDepth { get; set; } = 2; [JsonPropertyName("includeProperties")] public bool IncludeProperties { get; set; } = false; } public class AncestorsRequest { [JsonPropertyName("nodePath")] public string NodePath { get; set; } = ""; [JsonPropertyName("levels")] public int Levels { get; set; } = -1; [JsonPropertyName("includeSiblings")] public bool IncludeSiblings { get; set; } = false; } public class NodeContextRequest { [JsonPropertyName("nodePath")] public string NodePath { get; set; } = ""; [JsonPropertyName("includeParent")] public bool IncludeParent { get; set; } = true; [JsonPropertyName("includeSiblings")] public bool IncludeSiblings { get; set; } = true; [JsonPropertyName("includeChildren")] public bool IncludeChildren { get; set; } = true; } // ========== 响应模型 ========== public class ApiResponse { [JsonPropertyName("success")] public bool Success { get; set; } [JsonPropertyName("data")] public object? Data { get; set; } [JsonPropertyName("error")] public string? Error { get; set; } } public class NodeInfo { [JsonPropertyName("name")] public string Name { get; set; } = ""; [JsonPropertyName("type")] public string Type { get; set; } = ""; [JsonPropertyName("path")] public string Path { get; set; } = ""; [JsonPropertyName("children")] public List<string> Children { get; set; } = new(); [JsonPropertyName("properties")] public Dictionary<string, object?> Properties { get; set; } = new(); [JsonPropertyName("methods")] public List<string> Methods { get; set; } = new(); [JsonPropertyName("signals")] public List<string> Signals { get; set; } = new(); } // ========== 简化场景树请求 ========== public class SimpleTreeRequest { [JsonPropertyName("rootPath")] public string RootPath { get; set; } = "/root"; [JsonPropertyName("maxDepth")] public int MaxDepth { get; set; } = 3; } // ========== 信号系统请求 ========== public class SignalConnectionRequest { [JsonPropertyName("sourceNodePath")] public string SourceNodePath { get; set; } = ""; [JsonPropertyName("signalName")] public string SignalName { get; set; } = ""; [JsonPropertyName("targetNodePath")] public string? TargetNodePath { get; set; } [JsonPropertyName("targetMethod")] public string? TargetMethod { get; set; } } public class SignalEmitRequest { [JsonPropertyName("nodePath")] public string NodePath { get; set; } = ""; [JsonPropertyName("signalName")] public string SignalName { get; set; } = ""; [JsonPropertyName("args")] public List<object>? Args { get; set; } } public class SignalMonitorRequest { [JsonPropertyName("nodePath")] public string? NodePath { get; set; } [JsonPropertyName("signalName")] public string? SignalName { get; set; } [JsonPropertyName("maxEvents")] public int MaxEvents { get; set; } = 1000; } public class SignalEventQueryRequest { [JsonPropertyName("count")] public int Count { get; set; } = 50; [JsonPropertyName("nodePath")] public string? NodePath { get; set; } [JsonPropertyName("signalName")] public string? SignalName { get; set; } [JsonPropertyName("startTime")] public long? StartTime { get; set; } [JsonPropertyName("endTime")] public long? EndTime { get; set; } } public class SignalEvent { [JsonPropertyName("timestamp")] public DateTime Timestamp { get; set; } [JsonPropertyName("nodePath")] public string NodePath { get; set; } = ""; [JsonPropertyName("nodeType")] public string NodeType { get; set; } = ""; [JsonPropertyName("signalName")] public string SignalName { get; set; } = ""; [JsonPropertyName("args")] public List<string> Args { get; set; } = new(); } // ========== 增强日志系统请求 ========== public class LogFilterRequest { [JsonPropertyName("level")] public string? Level { get; set; } [JsonPropertyName("messagePattern")] public string? MessagePattern { get; set; } [JsonPropertyName("startTime")] public long? StartTime { get; set; } [JsonPropertyName("endTime")] public long? EndTime { get; set; } [JsonPropertyName("maxCount")] public int MaxCount { get; set; } = 100; } public class LogExportRequest { [JsonPropertyName("filePath")] public string? FilePath { get; set; } } public class CustomLogRequest { [JsonPropertyName("message")] public string Message { get; set; } = ""; [JsonPropertyName("level")] public string Level { get; set; } = "info"; } public class LogEntry { [JsonPropertyName("timestamp")] public DateTime Timestamp { get; set; } [JsonPropertyName("level")] public string Level { get; set; } = ""; [JsonPropertyName("message")] public string Message { get; set; } = ""; } /// <summary> /// 待处理的请求 (用于线程间通信) /// </summary> internal class PendingRequest { public string Path { get; set; } = ""; public string Body { get; set; } = ""; public TaskCompletionSource<ApiResponse> CompletionSource { get; set; } = null!; }

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/MingHuiLiu/godot4-runtime-mcp'

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