diff --git a/pom.xml b/pom.xml
index 3e6f72b..bf117f8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -93,9 +93,23 @@
+
org.springframework.ai
spring-ai-starter-mcp-client
+
+
+ io.modelcontextprotocol.sdk
+ mcp
+
+
+
+
+
+
+ io.modelcontextprotocol.sdk
+ mcp
+ 0.18.3
diff --git a/src/main/java/com/wok/supportbot/SupportBotApplication.java b/src/main/java/com/wok/supportbot/SupportBotApplication.java
index aab707c..78caa3b 100644
--- a/src/main/java/com/wok/supportbot/SupportBotApplication.java
+++ b/src/main/java/com/wok/supportbot/SupportBotApplication.java
@@ -4,6 +4,7 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* 主启动类
@@ -13,6 +14,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
@EnableAsync
+@EnableTransactionManagement
public class SupportBotApplication {
public static void main(String[] args) {
diff --git a/src/main/java/com/wok/supportbot/config/McpClientManager.java b/src/main/java/com/wok/supportbot/config/McpClientManager.java
index 7041d8a..2ad255b 100644
--- a/src/main/java/com/wok/supportbot/config/McpClientManager.java
+++ b/src/main/java/com/wok/supportbot/config/McpClientManager.java
@@ -6,6 +6,8 @@ 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.json.McpJsonMapper;
+import io.modelcontextprotocol.json.jackson2.JacksonMcpJsonMapper;
import io.modelcontextprotocol.spec.McpSchema;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
@@ -91,6 +93,12 @@ public class McpClientManager {
@Value("${mcp.client.init-timeout:15s}")
private String initTimeoutStr;
+ /**
+ * 共享的 JSON 映射器实例(Jackson 实现),供 StdioClientTransport 等组件使用
+ * 新版 SDK(0.12+)要求显式传入 McpJsonMapper
+ */
+ private final McpJsonMapper mcpJsonMapper = new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper());
+
/**
* 应用启动时自动加载所有已启用的 MCP Server 配置并建立连接
* 确保第一次对话请求时 clientCache 已就绪,MCP 工具可以被注册到 ChatClient
@@ -422,7 +430,7 @@ public class McpClientManager {
paramsBuilder.env(envStr);
}
- StdioClientTransport transport = new StdioClientTransport(paramsBuilder.build());
+ StdioClientTransport transport = new StdioClientTransport(paramsBuilder.build(), mcpJsonMapper);
client = buildSyncClient(transport);
log.info("正在初始化 stdio MCP 客户端: id={}, command={}", config.getId(), command);
@@ -577,6 +585,37 @@ public class McpClientManager {
}
}
+ /**
+ * 尝试重新建立指定配置的 MCP 客户端连接(用于调用失败时的自动恢复)。
+ * 先关闭并移除旧客户端,再通过懒加载重新创建。
+ *
+ * @param configId 配置ID
+ * @return 重建后的 MCP Client 实例,失败返回 null
+ */
+ public McpSyncClient reconnect(Long configId) {
+ String key = configId.toString();
+ // 先移除旧客户端(同时关闭底层连接)
+ McpSyncClient old = clientCache.remove(key);
+ if (old != null) {
+ try {
+ old.close();
+ } catch (Exception e) {
+ log.debug("关闭旧 MCP 客户端时出错: configId={}, error={}", configId, e.getMessage());
+ }
+ log.info("MCP 客户端已移除,准备重建: configId={}", configId);
+ }
+ // 从不可用集合中移除,允许重新尝试
+ unavailableConfigs.remove(key);
+ // 通过懒加载重建客户端
+ McpSyncClient newClient = getClient(configId);
+ if (newClient != null) {
+ log.info("MCP 客户端重建成功: configId={}", configId);
+ } else {
+ log.warn("MCP 客户端重建失败: configId={}", configId);
+ }
+ return newClient;
+ }
+
/**
* 测试指定配置的 MCP Server 连接
* 创建临时客户端 -> initialize -> listTools -> close,不影响正常缓存
diff --git a/src/main/java/com/wok/supportbot/config/ModelHealthService.java b/src/main/java/com/wok/supportbot/config/ModelHealthService.java
index 42c65b3..e6e7f10 100644
--- a/src/main/java/com/wok/supportbot/config/ModelHealthService.java
+++ b/src/main/java/com/wok/supportbot/config/ModelHealthService.java
@@ -50,6 +50,12 @@ public class ModelHealthService {
/** 单配置探活超时时间(秒) */
private static final int PROBE_TIMEOUT_SECONDS = 5;
+ /** 空配置跳过阈值:连续 N 次返回 0 行后跳过该 appType */
+ private static final int EMPTY_SKIP_THRESHOLD = 3;
+
+ /** 记录各 appType 连续空查询次数,达到阈值后跳过直到下次配置变更 */
+ private final ConcurrentHashMap emptyCountMap = new ConcurrentHashMap<>();
+
/**
* 应用销毁时关闭线程池,防止资源泄漏
*/
@@ -87,7 +93,23 @@ public class ModelHealthService {
for (String appType : appTypes) {
try {
+ // 跳过连续多次空查询的 appType(如未配置的 RERANK),减少无效 DB 查询
+ int emptyCount = emptyCountMap.getOrDefault(appType, 0);
+ if (emptyCount >= EMPTY_SKIP_THRESHOLD) {
+ log.debug("跳过空配置 appType [{}] 的探活(已连续 {} 次返回空)", appType, emptyCount);
+ continue;
+ }
+
List configs = configService.getAllActiveConfigs(appType);
+ if (configs.isEmpty()) {
+ // 本次查询为空,递增计数
+ int newCount = emptyCountMap.merge(appType, 1, Integer::sum);
+ log.debug("appType [{}] 无活跃配置(连续 {} 次)", appType, newCount);
+ continue;
+ }
+ // 有配置则重置计数
+ emptyCountMap.remove(appType);
+
for (AiModelConfig config : configs) {
Future