package com.wok.supportbot.controller; import com.wok.supportbot.app.AssistantApp; import com.wok.supportbot.config.McpClientManager; import com.wok.supportbot.entity.McpServerConfig; import com.wok.supportbot.mcp.McpToolCallbackAdapter; import com.wok.supportbot.service.McpServerConfigService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * MCP Server 配置管理控制器 * 提供 MCP Server 配置的增删改查、启用/禁用、连接测试、缓存刷新等 API */ @RestController @Slf4j public class McpServerConfigController { @Autowired private McpServerConfigService mcpServerConfigService; @Autowired private McpClientManager mcpClientManager; @Autowired private AssistantApp assistantApp; // ==================== 分页列表 ==================== /** * 获取 MCP Server 配置列表(分页) * * @param page 页码(默认1) * @param size 每页大小(默认10) * @return 分页配置列表 */ @GetMapping("/mcp-server/list") @PreAuthorize("hasRole('admin')") public ResponseEntity> listConfigs( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(required = false) String sortField, @RequestParam(required = false) String sortOrder) { try { Map result = mcpServerConfigService.listConfigs(page, size, sortField, sortOrder); Map data = new LinkedHashMap<>(); data.put("success", true); data.put("data", result.get("records")); data.put("total", result.get("total")); data.put("page", result.get("page")); data.put("size", result.get("size")); data.put("pages", result.get("pages")); return ResponseEntity.ok(data); } catch (Exception e) { return ResponseEntity.status(500).body(Map.of( "success", false, "message", "查询失败:" + e.getMessage() )); } } // ==================== 单条详情 ==================== /** * 获取单条 MCP Server 配置详情 * * @param id 配置ID * @return 配置详情 */ @GetMapping("/mcp-server/{id}") @PreAuthorize("hasRole('admin')") public ResponseEntity> getConfigDetail(@PathVariable("id") Long id) { try { Map config = mcpServerConfigService.getConfigDetail(id); if (config == null) { return ResponseEntity.status(404).body(Map.of( "success", false, "message", "配置不存在" )); } return ResponseEntity.ok(Map.of( "success", true, "data", config )); } catch (Exception e) { return ResponseEntity.status(500).body(Map.of( "success", false, "message", "查询失败:" + e.getMessage() )); } } // ==================== 可用工具列表 ==================== /** * 获取所有已启用 MCP Server 暴露的工具清单(按 Server 分组)。 * 工具名为带 mcp_ 前缀的注册名,与角色授权(allowed_mcp_tools)存储值一致,供「客服角色管理」授权 UI 使用。 * * @return [{ configId, serverName, transportType, tools: [{ name, rawName, description }] }] */ @GetMapping("/mcp-server/tools") @PreAuthorize("hasRole('admin')") public ResponseEntity> listAvailableTools() { try { List> servers = mcpClientManager.listAvailableTools(); List> result = new ArrayList<>(); for (Map server : servers) { Map out = new LinkedHashMap<>(); out.put("configId", server.get("config_id")); out.put("serverName", server.get("name")); out.put("transportType", server.get("transport_type")); List> tools = new ArrayList<>(); @SuppressWarnings("unchecked") List> rawTools = (List>) server.get("tools"); if (rawTools != null) { String configId = (String) server.get("config_id"); for (Map tool : rawTools) { String rawName = (String) tool.get("name"); Map toolOut = new LinkedHashMap<>(); toolOut.put("name", McpToolCallbackAdapter.buildToolName(configId, rawName)); toolOut.put("rawName", rawName); toolOut.put("description", tool.get("description")); tools.add(toolOut); } } out.put("tools", tools); result.add(out); } return ResponseEntity.ok(Map.of("success", true, "data", result)); } catch (Exception e) { return ResponseEntity.status(500).body(Map.of( "success", false, "message", "获取工具列表失败:" + e.getMessage() )); } } // ==================== 新建配置 ==================== /** * 新建 MCP Server 配置 * * @param config 配置对象 * @return 创建结果 */ @PostMapping("/mcp-server") @PreAuthorize("hasRole('admin')") public ResponseEntity> createConfig(@RequestBody McpServerConfig config) { try { Map created = mcpServerConfigService.createConfig(config); // 增量操作:仅为新建配置创建客户端,不影响其他已有连接 Object idObj = created.get("id"); if (idObj != null) { try { mcpClientManager.addClient(Long.parseLong(idObj.toString())); } catch (Exception e) { log.warn("新建配置后创建客户端失败(不影响配置保存): {}", e.getMessage()); } // MCP 工具清单可能变化,清空 AssistantApp 的 ChatClient 缓存,避免继续使用旧工具集 assistantApp.clearCache(); } return ResponseEntity.ok(Map.of( "success", true, "data", created, "message", "配置创建成功" )); } catch (IllegalArgumentException e) { return ResponseEntity.badRequest().body(Map.of( "success", false, "message", e.getMessage() )); } catch (Exception e) { return ResponseEntity.status(500).body(Map.of( "success", false, "message", "创建失败:" + e.getMessage() )); } } // ==================== 更新配置 ==================== /** * 更新 MCP Server 配置 * * @param id 配置ID * @param config 更新内容 * @return 更新结果 */ @PutMapping("/mcp-server/{id}") @PreAuthorize("hasRole('admin')") public ResponseEntity> updateConfig( @PathVariable("id") Long id, @RequestBody McpServerConfig config) { try { Map updated = mcpServerConfigService.updateConfig(id, config); // 增量操作:仅重建更新的配置客户端,不影响其他已有连接 mcpClientManager.rebuildClient(id); // MCP 工具清单可能变化,清空 AssistantApp 的 ChatClient 缓存,避免继续使用旧工具集 assistantApp.clearCache(); return ResponseEntity.ok(Map.of( "success", true, "data", updated, "message", "配置更新成功" )); } catch (RuntimeException e) { return ResponseEntity.badRequest().body(Map.of( "success", false, "message", e.getMessage() )); } catch (Exception e) { return ResponseEntity.status(500).body(Map.of( "success", false, "message", "更新失败:" + e.getMessage() )); } } // ==================== 删除配置 ==================== /** * 删除 MCP Server 配置(逻辑删除) * * @param id 配置ID * @return 删除结果 */ @DeleteMapping("/mcp-server/{id}") @PreAuthorize("hasRole('admin')") public ResponseEntity> deleteConfig(@PathVariable("id") Long id) { try { mcpServerConfigService.deleteConfig(id); // 增量操作:仅移除被删除的配置客户端,不影响其他已有连接 mcpClientManager.removeClient(id); // MCP 工具清单可能变化,清空 AssistantApp 的 ChatClient 缓存,避免继续使用旧工具集 assistantApp.clearCache(); return ResponseEntity.ok(Map.of( "success", true, "message", "配置删除成功" )); } catch (RuntimeException e) { return ResponseEntity.badRequest().body(Map.of( "success", false, "message", e.getMessage() )); } catch (Exception e) { return ResponseEntity.status(500).body(Map.of( "success", false, "message", "删除失败:" + e.getMessage() )); } } // ==================== 启用/禁用 ==================== /** * 切换配置启用/禁用状态 * * @param id 配置ID * @param body 请求体(可选):{active: true/false} * @param active query 兜底参数(可选):兼容不带 body 的旧调用方(active 放 query string) * @return 操作结果 */ @PutMapping("/mcp-server/{id}/toggle") @PreAuthorize("hasRole('admin')") public ResponseEntity> toggleActive( @PathVariable("id") Long id, @RequestBody(required = false) Map body, @RequestParam(value = "active", required = false) Boolean activeParam) { try { Boolean active = (body != null) ? body.get("active") : null; if (active == null) { active = activeParam; } if (active == null) { return ResponseEntity.badRequest().body(Map.of( "success", false, "message", "active 参数不能为空" )); } mcpServerConfigService.toggleActive(id, active); // 增量操作:启用时重建客户端,禁用时移除客户端 if (active) { mcpClientManager.rebuildClient(id); } else { mcpClientManager.disableClient(id); } // MCP 工具清单可能变化,清空 AssistantApp 的 ChatClient 缓存,避免继续使用旧工具集 assistantApp.clearCache(); return ResponseEntity.ok(Map.of( "success", true, "message", active ? "配置已启用" : "配置已禁用" )); } catch (RuntimeException e) { return ResponseEntity.badRequest().body(Map.of( "success", false, "message", e.getMessage() )); } catch (Exception e) { return ResponseEntity.status(500).body(Map.of( "success", false, "message", "操作失败:" + e.getMessage() )); } } // ==================== 测试连接 ==================== /** * 测试 MCP Server 连接 * 创建临时客户端进行握手 + 工具列表查询,不影响正常缓存 * * @param id 配置ID * @return 测试结果(success / latencyMs / toolsCount / errorMessage) */ @PostMapping("/mcp-server/{id}/test") @PreAuthorize("hasRole('admin')") public ResponseEntity> testConnection(@PathVariable("id") Long id) { try { Map result = mcpClientManager.testConnection(id); return ResponseEntity.ok(result); } catch (Exception e) { return ResponseEntity.status(500).body(Map.of( "success", false, "message", "测试失败:" + e.getMessage() )); } } // ==================== 刷新连接 ==================== /** * 刷新所有 MCP 客户端连接 * 清空现有缓存并重新建立所有已启用配置的客户端连接 * * @return 刷新结果 */ @PostMapping("/mcp-server/refresh") @PreAuthorize("hasRole('admin')") public ResponseEntity> refreshAll() { try { mcpClientManager.refreshAll(); // MCP 工具清单可能变化,清空 AssistantApp 的 ChatClient 缓存,避免继续使用旧工具集 assistantApp.clearCache(); return ResponseEntity.ok(Map.of( "success", true, "message", "MCP 客户端连接已刷新" )); } catch (Exception e) { return ResponseEntity.status(500).body(Map.of( "success", false, "message", "刷新失败:" + e.getMessage() )); } } // ==================== 健康检查 ==================== /** * 获取所有 MCP Server 的健康状态 * 返回各配置的在线/离线状态、延迟、最后检查时间等信息 * * @return 所有 MCP Server 的健康状态 */ @GetMapping("/mcp-server/health") @PreAuthorize("hasRole('admin')") public ResponseEntity> getAllHealthStatus() { try { Map healthMap = mcpClientManager.getAllHealthStatus(); Map data = new LinkedHashMap<>(); data.put("success", true); data.put("data", healthMap); return ResponseEntity.ok(data); } catch (Exception e) { return ResponseEntity.status(500).body(Map.of( "success", false, "message", "获取健康状态失败:" + e.getMessage() )); } } /** * 手动触发单个 MCP Server 的健康检查 * 对指定配置的客户端执行 listTools 探测,返回最新健康状态 * * @param id 配置ID * @return 该配置的健康状态 */ @PostMapping("/mcp-server/{id}/health-check") @PreAuthorize("hasRole('admin')") public ResponseEntity> checkSingleHealth(@PathVariable("id") Long id) { try { McpClientManager.HealthStatus status = mcpClientManager.checkHealthForConfig(id); return ResponseEntity.ok(Map.of( "success", true, "data", Map.of( "configId", id.toString(), "status", status.status(), "latencyMs", status.latencyMs(), "lastCheckTime", status.lastCheckTime().toString(), "errorMessage", status.errorMessage() != null ? status.errorMessage() : "" ) )); } catch (Exception e) { return ResponseEntity.status(500).body(Map.of( "success", false, "message", "健康检查失败:" + e.getMessage() )); } } }