You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
660 lines
27 KiB
660 lines
27 KiB
package com.wok.supportbot.config;
|
|
|
|
import com.wok.supportbot.entity.McpServerConfig;
|
|
import com.wok.supportbot.service.McpServerConfigService;
|
|
import io.modelcontextprotocol.client.McpSyncClient;
|
|
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
|
|
import io.modelcontextprotocol.client.transport.ServerParameters;
|
|
import io.modelcontextprotocol.client.transport.StdioClientTransport;
|
|
import io.modelcontextprotocol.spec.McpSchema;
|
|
import jakarta.annotation.PostConstruct;
|
|
import jakarta.annotation.PreDestroy;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.ai.model.ModelOptionsUtils;
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.scheduling.annotation.Scheduled;
|
|
import org.springframework.stereotype.Component;
|
|
|
|
import java.time.Duration;
|
|
import java.time.Instant;
|
|
import java.util.*;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
|
|
/**
|
|
* MCP 客户端生命周期管理器
|
|
* 负责 MCP Client 的创建、缓存、刷新和销毁。
|
|
* <p>
|
|
* 支持两种传输模式:
|
|
* - SSE(Server-Sent Events):通过 HttpClientSseClientTransport 连接远程 MCP Server
|
|
* - stdio(标准输入输出):通过 StdioClientTransport 启动本地 MCP Server 进程
|
|
*/
|
|
@Component
|
|
@Slf4j
|
|
public class McpClientManager {
|
|
|
|
@Autowired
|
|
private McpServerConfigService mcpServerConfigService;
|
|
|
|
/**
|
|
* 客户端缓存:key = 配置ID 的字符串形式,value = MCP Client 实例
|
|
* volatile 保证 refreshAll() 双缓冲切换时的可见性
|
|
*/
|
|
private volatile ConcurrentHashMap<String, McpSyncClient> clientCache = new ConcurrentHashMap<>();
|
|
|
|
/**
|
|
* 不可用配置集合:记录已知不存在或未启用的配置ID,避免重复查询 DB
|
|
* 配置被启用或新建时需同步移除此集合中的对应条目
|
|
*/
|
|
private final Set<String> unavailableConfigs = ConcurrentHashMap.newKeySet();
|
|
|
|
/**
|
|
* 健康状态缓存:key = 配置ID 的字符串形式,value = 健康检查结果
|
|
*/
|
|
private final ConcurrentHashMap<String, HealthStatus> healthCache = new ConcurrentHashMap<>();
|
|
|
|
/**
|
|
* MCP Server 健康状态记录
|
|
*/
|
|
public record HealthStatus(
|
|
/** 状态:ONLINE / OFFLINE / UNKNOWN */
|
|
String status,
|
|
/** 响应延迟(毫秒) */
|
|
long latencyMs,
|
|
/** 最后检查时间 */
|
|
Instant lastCheckTime,
|
|
/** 错误信息(健康时为 null) */
|
|
String errorMessage
|
|
) {
|
|
public static HealthStatus online(long latencyMs) {
|
|
return new HealthStatus("ONLINE", latencyMs, Instant.now(), null);
|
|
}
|
|
|
|
public static HealthStatus offline(long latencyMs, String errorMessage) {
|
|
return new HealthStatus("OFFLINE", latencyMs, Instant.now(), errorMessage);
|
|
}
|
|
|
|
public static HealthStatus unknown(String errorMessage) {
|
|
return new HealthStatus("UNKNOWN", 0, Instant.now(), errorMessage);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* MCP 客户端请求超时时间,支持 Duration 格式(如 30s、PT30S、60s)
|
|
*/
|
|
@Value("${mcp.client.request-timeout:30s}")
|
|
private String requestTimeoutStr;
|
|
|
|
/**
|
|
* MCP 客户端初始化超时时间,支持 Duration 格式(如 15s、PT15S、30s)
|
|
*/
|
|
@Value("${mcp.client.init-timeout:15s}")
|
|
private String initTimeoutStr;
|
|
|
|
/**
|
|
* 应用启动时自动加载所有已启用的 MCP Server 配置并建立连接
|
|
* 确保第一次对话请求时 clientCache 已就绪,MCP 工具可以被注册到 ChatClient
|
|
*/
|
|
@PostConstruct
|
|
public void init() {
|
|
log.info("McpClientManager 初始化,开始加载已启用的 MCP Server 配置...");
|
|
refreshAll();
|
|
}
|
|
|
|
/**
|
|
* 刷新所有 MCP 客户端连接(双缓冲策略)
|
|
* 先在新 Map 中构建所有客户端,再原子切换引用,最后关闭旧客户端。
|
|
* 避免清空与重建之间的请求全部失败。
|
|
*/
|
|
public void refreshAll() {
|
|
log.info("开始刷新所有 MCP 客户端连接...");
|
|
|
|
// 获取所有启用的配置(一次查询代替两次按类型过滤)
|
|
List<McpServerConfig> allActiveConfigs = mcpServerConfigService.listAllActiveConfigs();
|
|
|
|
// 第一步:在全新 Map 中构建所有客户端(不影响现有缓存)
|
|
ConcurrentHashMap<String, McpSyncClient> newCache = new ConcurrentHashMap<>();
|
|
for (McpServerConfig config : allActiveConfigs) {
|
|
try {
|
|
McpSyncClient client = createClientDirectly(config);
|
|
if (client != null) {
|
|
newCache.put(config.getId().toString(), client);
|
|
}
|
|
} catch (Exception e) {
|
|
log.error("创建客户端失败: id={}, name={}, transportType={}, error={}",
|
|
config.getId(), config.getName(), config.getTransportType(), e.getMessage());
|
|
}
|
|
}
|
|
|
|
// 第二步:原子切换引用(volatile 保证其他线程立即可见)
|
|
ConcurrentHashMap<String, McpSyncClient> oldCache = clientCache;
|
|
clientCache = newCache;
|
|
unavailableConfigs.clear();
|
|
|
|
// 第三步:关闭旧缓存中的客户端(不影响新请求)
|
|
for (Map.Entry<String, McpSyncClient> entry : oldCache.entrySet()) {
|
|
try {
|
|
McpSyncClient client = entry.getValue();
|
|
if (client != null) {
|
|
client.close();
|
|
log.debug("已关闭旧 MCP 客户端: configId={}", entry.getKey());
|
|
}
|
|
} catch (Exception e) {
|
|
log.error("关闭旧 MCP 客户端失败: configId={}, error={}", entry.getKey(), e.getMessage());
|
|
}
|
|
}
|
|
|
|
log.info("MCP 客户端刷新完成,当前缓存数量: {}", clientCache.size());
|
|
}
|
|
|
|
/**
|
|
* 获取指定配置的客户端(懒加载)
|
|
* 使用 computeIfAbsent 保证同一配置只创建一次客户端,避免并发竞态。
|
|
* 如果缓存中不存在且不在不可用集合中,从 DB 读取配置后原子创建并缓存。
|
|
*
|
|
* @param configId 配置ID
|
|
* @return MCP Client 实例,不存在或未启用返回 null
|
|
*/
|
|
public McpSyncClient getClient(Long configId) {
|
|
String key = configId.toString();
|
|
|
|
// 快速路径:缓存命中
|
|
McpSyncClient cached = clientCache.get(key);
|
|
if (cached != null) {
|
|
return cached;
|
|
}
|
|
|
|
// 快速路径:已知不可用的配置,直接跳过
|
|
if (unavailableConfigs.contains(key)) {
|
|
return null;
|
|
}
|
|
|
|
// 缓存未命中,使用 computeIfAbsent 保证同一 key 只创建一次客户端
|
|
// ConcurrentHashMap.computeIfAbsent 对同一 key 加锁,避免并发线程重复创建
|
|
// 注意:mapping function 不能返回 null(会抛 NPE),因此不可用的情况记录到 unavailableConfigs
|
|
McpServerConfig config = mcpServerConfigService.getConfigById(configId);
|
|
if (config == null) {
|
|
log.warn("MCP 配置不存在: id={}", configId);
|
|
unavailableConfigs.add(key);
|
|
return null;
|
|
}
|
|
if (!Boolean.TRUE.equals(config.getIsActive())) {
|
|
log.warn("MCP 配置未启用,跳过创建客户端: id={}, name={}", configId, config.getName());
|
|
unavailableConfigs.add(key);
|
|
return null;
|
|
}
|
|
|
|
// 配置存在且启用,通过 computeIfAbsent 原子创建(防止并发重复创建)
|
|
return clientCache.computeIfAbsent(key, k -> createClientDirectly(config));
|
|
}
|
|
|
|
/**
|
|
* 获取所有已启用的 MCP 工具描述
|
|
* 遍历所有已缓存的客户端,调用 listTools() 获取真实的工具列表
|
|
*
|
|
* @return 工具描述列表,每个元素包含 config_id、name、transport_type、description、tools
|
|
*/
|
|
public List<Map<String, Object>> listAvailableTools() {
|
|
List<Map<String, Object>> tools = new ArrayList<>();
|
|
|
|
log.info("🔍 listAvailableTools: clientCache 大小 = {}", clientCache.size());
|
|
|
|
if (clientCache.isEmpty()) {
|
|
log.warn("⚠️ clientCache 为空!MCP Server 可能未配置或连接失败。");
|
|
log.warn(" 请检查:1) 是否添加了 MCP Server 配置 2) 配置是否已启用 3) 启动日志是否有连接错误");
|
|
return tools;
|
|
}
|
|
|
|
for (Map.Entry<String, McpSyncClient> entry : clientCache.entrySet()) {
|
|
Long configId;
|
|
try {
|
|
configId = Long.parseLong(entry.getKey());
|
|
} catch (NumberFormatException e) {
|
|
log.warn("缓存 key 格式异常: {}", entry.getKey());
|
|
continue;
|
|
}
|
|
|
|
McpServerConfig config = mcpServerConfigService.getConfigById(configId);
|
|
if (config == null || !Boolean.TRUE.equals(config.getIsActive())) {
|
|
continue;
|
|
}
|
|
|
|
Map<String, Object> toolInfo = new LinkedHashMap<>();
|
|
toolInfo.put("config_id", config.getId().toString());
|
|
toolInfo.put("name", config.getName());
|
|
toolInfo.put("transport_type", config.getTransportType());
|
|
toolInfo.put("description", config.getDescription());
|
|
|
|
// 从 MCP Client 获取真实的工具列表
|
|
List<Map<String, Object>> toolList = new ArrayList<>();
|
|
try {
|
|
McpSyncClient client = entry.getValue();
|
|
McpSchema.ListToolsResult listToolsResult = client.listTools();
|
|
if (listToolsResult != null && listToolsResult.tools() != null) {
|
|
log.info(" 📦 MCP Server [{}] 暴露了 {} 个工具", config.getName(), listToolsResult.tools().size());
|
|
for (McpSchema.Tool tool : listToolsResult.tools()) {
|
|
Map<String, Object> toolMeta = new LinkedHashMap<>();
|
|
toolMeta.put("name", tool.name());
|
|
toolMeta.put("description", tool.description());
|
|
// JsonSchema 对象序列化为 JSON 字符串,供 McpToolCallback 构建 ToolDefinition 使用
|
|
if (tool.inputSchema() != null) {
|
|
toolMeta.put("inputSchema", ModelOptionsUtils.toJsonString(tool.inputSchema()));
|
|
} else {
|
|
toolMeta.put("inputSchema", "{}");
|
|
}
|
|
toolList.add(toolMeta);
|
|
}
|
|
}
|
|
log.info(" 📦 MCP Server [{}] 获取到 {} 个工具", config.getName(), toolList.size());
|
|
if (toolList.isEmpty()) {
|
|
log.warn(" ⚠️ MCP Server [{}] 未暴露任何工具,AI 模型将无法调用", config.getName());
|
|
}
|
|
} catch (Exception e) {
|
|
log.error("获取 MCP 工具列表失败: serverId={}, name={}, error={}",
|
|
configId, config.getName(), e.getMessage());
|
|
}
|
|
|
|
toolInfo.put("tools", toolList);
|
|
tools.add(toolInfo);
|
|
}
|
|
|
|
return tools;
|
|
}
|
|
|
|
/**
|
|
* 定时健康检查:每 5 分钟执行一次
|
|
* 遍历所有缓存的客户端,尝试 listTools() 探测连通性,记录状态到 healthCache
|
|
*/
|
|
@Scheduled(fixedDelay = 300000)
|
|
public void healthCheck() {
|
|
log.info("开始 MCP Server 定时健康检查,当前缓存数量: {}", clientCache.size());
|
|
for (Map.Entry<String, McpSyncClient> entry : clientCache.entrySet()) {
|
|
String key = entry.getKey();
|
|
checkSingleClient(key, entry.getValue());
|
|
}
|
|
log.info("MCP Server 定时健康检查完成");
|
|
}
|
|
|
|
/**
|
|
* 手动触发单个配置的健康检查
|
|
*
|
|
* @param configId 配置ID
|
|
* @return 该配置的最新健康状态
|
|
*/
|
|
public HealthStatus checkHealthForConfig(Long configId) {
|
|
String key = configId.toString();
|
|
McpSyncClient client = clientCache.get(key);
|
|
if (client == null) {
|
|
HealthStatus status = HealthStatus.unknown("客户端未缓存或配置不存在");
|
|
healthCache.put(key, status);
|
|
return status;
|
|
}
|
|
return checkSingleClient(key, client);
|
|
}
|
|
|
|
/**
|
|
* 对单个客户端执行健康探测
|
|
* 通过调用 listTools() 判断服务端是否存活
|
|
*
|
|
* @param key 缓存 key(配置ID 字符串)
|
|
* @param client MCP 客户端实例
|
|
* @return 健康状态
|
|
*/
|
|
private HealthStatus checkSingleClient(String key, McpSyncClient client) {
|
|
long startTime = System.currentTimeMillis();
|
|
try {
|
|
McpSchema.ListToolsResult result = client.listTools();
|
|
long latencyMs = System.currentTimeMillis() - startTime;
|
|
int toolsCount = (result != null && result.tools() != null) ? result.tools().size() : 0;
|
|
HealthStatus status = HealthStatus.online(latencyMs);
|
|
healthCache.put(key, status);
|
|
log.debug("MCP Server 健康检查通过: configId={}, latencyMs={}, toolsCount={}", key, latencyMs, toolsCount);
|
|
return status;
|
|
} catch (Exception e) {
|
|
long latencyMs = System.currentTimeMillis() - startTime;
|
|
HealthStatus status = HealthStatus.offline(latencyMs, e.getMessage());
|
|
healthCache.put(key, status);
|
|
log.warn("MCP Server 健康检查失败: configId={}, latencyMs={}, error={}", key, latencyMs, e.getMessage());
|
|
return status;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取指定配置的健康状态
|
|
*
|
|
* @param configId 配置ID
|
|
* @return 健康状态,未检查过返回 UNKNOWN
|
|
*/
|
|
public HealthStatus getHealthStatus(Long configId) {
|
|
HealthStatus status = healthCache.get(configId.toString());
|
|
if (status == null) {
|
|
return HealthStatus.unknown("尚未执行健康检查");
|
|
}
|
|
return status;
|
|
}
|
|
|
|
/**
|
|
* 获取所有已缓存配置的健康状态
|
|
*
|
|
* @return Map:key = 配置ID 字符串,value = 健康状态
|
|
*/
|
|
public Map<String, HealthStatus> getAllHealthStatus() {
|
|
Map<String, HealthStatus> result = new LinkedHashMap<>();
|
|
// 遍历所有缓存的客户端,未在 healthCache 中的标记为 UNKNOWN
|
|
for (String key : clientCache.keySet()) {
|
|
HealthStatus status = healthCache.get(key);
|
|
if (status == null) {
|
|
status = HealthStatus.unknown("尚未执行健康检查");
|
|
}
|
|
result.put(key, status);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* 关闭所有 MCP 客户端连接
|
|
* 在应用关闭或手动刷新时调用
|
|
*/
|
|
@PreDestroy
|
|
public void shutdown() {
|
|
log.info("关闭所有 MCP 客户端连接,当前数量: {}", clientCache.size());
|
|
for (Map.Entry<String, McpSyncClient> entry : clientCache.entrySet()) {
|
|
try {
|
|
McpSyncClient client = entry.getValue();
|
|
if (client != null) {
|
|
client.close();
|
|
log.debug("已关闭 MCP 客户端: configId={}", entry.getKey());
|
|
}
|
|
} catch (Exception e) {
|
|
log.error("关闭 MCP 客户端失败: configId={}, error={}", entry.getKey(), e.getMessage());
|
|
}
|
|
}
|
|
clientCache.clear();
|
|
unavailableConfigs.clear();
|
|
healthCache.clear();
|
|
log.info("所有 MCP 客户端连接已关闭");
|
|
}
|
|
|
|
/**
|
|
* 根据配置创建 MCP 客户端(不负责缓存,由调用方决定是否缓存)
|
|
* <p>
|
|
* SSE 模式:使用 HttpClientSseClientTransport 连接远程 MCP Server
|
|
* stdio 模式:使用 StdioClientTransport 启动本地 MCP Server 进程
|
|
*
|
|
* @param config MCP Server 配置
|
|
* @return 创建并初始化后的客户端实例,失败返回 null
|
|
*/
|
|
private McpSyncClient createClientDirectly(McpServerConfig config) {
|
|
try {
|
|
McpSyncClient client;
|
|
|
|
if ("sse".equals(config.getTransportType())) {
|
|
// 创建 SSE 传输层:连接远程 MCP Server
|
|
// HttpClientSseClientTransport 会自动将 /sse 作为 SSE 端点附加到 baseUri
|
|
String serverUrl = config.getServerUrl().trim();
|
|
// 如果 URL 以 /sse 结尾,去掉后缀作为 baseUri(transport 层会自动附加)
|
|
String baseUri = serverUrl.replaceAll("/sse/?$", "");
|
|
HttpClientSseClientTransport transport = HttpClientSseClientTransport.builder(baseUri).build();
|
|
|
|
client = buildSyncClient(transport);
|
|
log.info("正在初始化 SSE MCP 客户端: id={}, url={}", config.getId(), baseUri);
|
|
|
|
} else if ("stdio".equals(config.getTransportType())) {
|
|
// 创建 stdio 传输层:启动本地 MCP Server 进程
|
|
String command = config.getCommand().trim();
|
|
|
|
// 解析参数列表(逗号分隔)
|
|
List<String> argsList = new ArrayList<>();
|
|
if (config.getArgs() != null && !config.getArgs().trim().isEmpty()) {
|
|
argsList = Arrays.asList(config.getArgs().split("\\s*,\\s*"));
|
|
}
|
|
|
|
// 构建 ServerParameters
|
|
ServerParameters.Builder paramsBuilder = ServerParameters.builder(command);
|
|
if (!argsList.isEmpty()) {
|
|
paramsBuilder.args(argsList);
|
|
}
|
|
|
|
// 环境变量(Map<String, Object> -> Map<String, String>)
|
|
if (config.getEnvVars() != null && !config.getEnvVars().isEmpty()) {
|
|
Map<String, String> envStr = new HashMap<>();
|
|
config.getEnvVars().forEach((k, v) -> envStr.put(k, v != null ? v.toString() : ""));
|
|
paramsBuilder.env(envStr);
|
|
}
|
|
|
|
StdioClientTransport transport = new StdioClientTransport(paramsBuilder.build());
|
|
client = buildSyncClient(transport);
|
|
log.info("正在初始化 stdio MCP 客户端: id={}, command={}", config.getId(), command);
|
|
|
|
} else {
|
|
log.error("不支持的传输类型: {}", config.getTransportType());
|
|
return null;
|
|
}
|
|
|
|
// 初始化连接(握手协议)
|
|
client.initialize();
|
|
log.info("MCP 客户端初始化成功: id={}, name={}, transportType={}",
|
|
config.getId(), config.getName(), config.getTransportType());
|
|
|
|
return client;
|
|
|
|
} catch (Exception e) {
|
|
log.error("创建 MCP 客户端失败: id={}, name={}, transportType={}, error={}",
|
|
config.getId(), config.getName(), config.getTransportType(), e.getMessage(), e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 构建 McpSyncClient 实例
|
|
* 统一设置客户端信息和超时参数,超时时间从配置文件读取
|
|
*
|
|
* @param transport 传输层实例(SSE 或 stdio)
|
|
* @return 构建的 McpSyncClient
|
|
*/
|
|
private McpSyncClient buildSyncClient(io.modelcontextprotocol.spec.McpClientTransport transport) {
|
|
Duration requestTimeout = parseDuration(requestTimeoutStr, Duration.ofSeconds(30));
|
|
Duration initTimeout = parseDuration(initTimeoutStr, Duration.ofSeconds(15));
|
|
|
|
return io.modelcontextprotocol.client.McpClient.sync(transport)
|
|
.clientInfo(new McpSchema.Implementation("supportbot-mcp-client", "1.0.0"))
|
|
.requestTimeout(requestTimeout)
|
|
.initializationTimeout(initTimeout)
|
|
.build();
|
|
}
|
|
|
|
/**
|
|
* 解析 Duration 字符串,兼容 ISO-8601 格式(PT30S)和简化格式(30s、60s)
|
|
* 解析失败时返回默认值
|
|
*
|
|
* @param durationStr Duration 字符串
|
|
* @param defaultValue 解析失败时的默认值
|
|
* @return 解析后的 Duration
|
|
*/
|
|
private Duration parseDuration(String durationStr, Duration defaultValue) {
|
|
if (durationStr == null || durationStr.isBlank()) {
|
|
return defaultValue;
|
|
}
|
|
try {
|
|
// 先尝试 ISO-8601 格式(如 PT30S、PT1M30S)
|
|
return Duration.parse(durationStr.trim());
|
|
} catch (Exception e) {
|
|
// 尝试简化格式:纯数字秒(如 "30")或带 "s" 后缀(如 "30s")
|
|
String trimmed = durationStr.trim().toLowerCase();
|
|
String numStr = trimmed.replaceAll("[^0-9.]+$", "");
|
|
if (!numStr.isEmpty()) {
|
|
try {
|
|
long seconds = Long.parseLong(numStr);
|
|
return Duration.ofSeconds(seconds);
|
|
} catch (NumberFormatException ignored) {
|
|
// 继续尝试小数
|
|
}
|
|
}
|
|
log.warn("无法解析 Duration 配置 '{}',使用默认值: {}", durationStr, defaultValue);
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
// ==================== 增量缓存管理(供 Controller 层 CRUD 操作使用) ====================
|
|
|
|
/**
|
|
* 新增配置时:创建客户端并加入缓存
|
|
* 仅对新建的单个配置创建连接,不影响其他已有连接。
|
|
*
|
|
* @param configId 新建配置的ID
|
|
*/
|
|
public void addClient(Long configId) {
|
|
String key = configId.toString();
|
|
// 从不可用集合中移除(新建的配置可能之前被标记为不可用)
|
|
unavailableConfigs.remove(key);
|
|
|
|
McpServerConfig config = mcpServerConfigService.getConfigById(configId);
|
|
if (config == null || !Boolean.TRUE.equals(config.getIsActive())) {
|
|
return;
|
|
}
|
|
McpSyncClient client = createClientDirectly(config);
|
|
if (client != null) {
|
|
clientCache.put(key, client);
|
|
log.info("增量添加 MCP 客户端: id={}, name={}", configId, config.getName());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 更新配置时:关闭旧客户端,重建新客户端
|
|
* 仅对更新的单个配置重建连接,不影响其他已有连接。
|
|
*
|
|
* @param configId 更新的配置ID
|
|
*/
|
|
public void rebuildClient(Long configId) {
|
|
String key = configId.toString();
|
|
// 先移除旧客户端
|
|
removeClient(configId);
|
|
|
|
McpServerConfig config = mcpServerConfigService.getConfigById(configId);
|
|
if (config == null) {
|
|
unavailableConfigs.add(key);
|
|
return;
|
|
}
|
|
if (!Boolean.TRUE.equals(config.getIsActive())) {
|
|
unavailableConfigs.add(key);
|
|
return;
|
|
}
|
|
|
|
unavailableConfigs.remove(key);
|
|
McpSyncClient client = createClientDirectly(config);
|
|
if (client != null) {
|
|
clientCache.put(key, client);
|
|
log.info("增量重建 MCP 客户端: id={}, name={}", configId, config.getName());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 禁用配置时:关闭并移除客户端,标记为不可用
|
|
*
|
|
* @param configId 禁用的配置ID
|
|
*/
|
|
public void disableClient(Long configId) {
|
|
removeClient(configId);
|
|
unavailableConfigs.add(configId.toString());
|
|
}
|
|
|
|
/**
|
|
* 移除指定配置的客户端缓存
|
|
* 配置删除或禁用时调用
|
|
*
|
|
* @param configId 配置ID
|
|
*/
|
|
public void removeClient(Long configId) {
|
|
String key = configId.toString();
|
|
McpSyncClient removed = clientCache.remove(key);
|
|
if (removed != null) {
|
|
try {
|
|
removed.close();
|
|
} catch (Exception e) {
|
|
log.error("关闭被移除的 MCP 客户端失败: configId={}, error={}", configId, e.getMessage());
|
|
}
|
|
log.info("已移除并关闭 MCP 客户端缓存: configId={}", configId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 测试指定配置的 MCP Server 连接
|
|
* 创建临时客户端 -> initialize -> listTools -> close,不影响正常缓存
|
|
*
|
|
* @param configId 配置ID
|
|
* @return 测试结果 Map:success / latencyMs / toolsCount / errorMessage
|
|
*/
|
|
public Map<String, Object> testConnection(Long configId) {
|
|
McpServerConfig config = mcpServerConfigService.getConfigById(configId);
|
|
if (config == null) {
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
result.put("success", false);
|
|
result.put("latencyMs", 0);
|
|
result.put("toolsCount", 0);
|
|
result.put("errorMessage", "配置不存在: id=" + configId);
|
|
return result;
|
|
}
|
|
|
|
McpSyncClient tempClient = null;
|
|
long startTime = System.currentTimeMillis();
|
|
try {
|
|
// 创建临时客户端(不加入缓存),复用 createClientDirectly 的传输层构建逻辑
|
|
tempClient = createClientDirectly(config);
|
|
if (tempClient == null) {
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
result.put("success", false);
|
|
result.put("latencyMs", System.currentTimeMillis() - startTime);
|
|
result.put("toolsCount", 0);
|
|
result.put("errorMessage", "客户端创建失败,请检查配置");
|
|
return result;
|
|
}
|
|
|
|
// 获取工具列表
|
|
McpSchema.ListToolsResult listToolsResult = tempClient.listTools();
|
|
int toolsCount = (listToolsResult != null && listToolsResult.tools() != null)
|
|
? listToolsResult.tools().size() : 0;
|
|
|
|
long latencyMs = System.currentTimeMillis() - startTime;
|
|
log.info("MCP 连接测试成功: id={}, name={}, latencyMs={}, toolsCount={}",
|
|
configId, config.getName(), latencyMs, toolsCount);
|
|
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
result.put("success", true);
|
|
result.put("latencyMs", latencyMs);
|
|
result.put("toolsCount", toolsCount);
|
|
result.put("errorMessage", null);
|
|
return result;
|
|
|
|
} catch (Exception e) {
|
|
long latencyMs = System.currentTimeMillis() - startTime;
|
|
log.warn("MCP 连接测试失败: id={}, name={}, latencyMs={}, error={}",
|
|
configId, config.getName(), latencyMs, e.getMessage());
|
|
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
result.put("success", false);
|
|
result.put("latencyMs", latencyMs);
|
|
result.put("toolsCount", 0);
|
|
result.put("errorMessage", e.getMessage());
|
|
return result;
|
|
|
|
} finally {
|
|
// 确保关闭临时客户端
|
|
if (tempClient != null) {
|
|
try {
|
|
tempClient.close();
|
|
} catch (Exception e) {
|
|
log.debug("关闭临时测试客户端失败: configId={}, error={}", configId, e.getMessage());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取当前缓存的客户端数量
|
|
*
|
|
* @return 缓存数量
|
|
*/
|
|
public int getCacheSize() {
|
|
return clientCache.size();
|
|
}
|
|
}
|