✅SSE MCP Server如何实现重连?

在 MCP 的早期版本中,还没有出现Streamable http这种方式,主要依赖的就是本地 Stdio,远程 SSE 建立长连接。SSE 是一种典型的单向流式机制,通过一个端点持续监听服务器指令,再通过另一个端点发送消息。在稳定网络环境下,这种模式可以正常工作,但它对长连接的依赖非常强,一旦网络出现抖动、服务端重启、代理回收空闲连接,SSE 管道就会立即断开。

基于上面的问题,Spring AI MCP Client 也没有提供自动重连的能力,一旦 SSE 被中断,客户端实际上就失去了与 MCP Server 的指令通道,工具虽然注册着,但不再响应任何调用,整个智能体就会陷入无工具状态。

客户端无法自行恢复工具能力,只能重新初始化 MCP Client 才能重新建立会话。对于需要长时间稳定运行的企业级智能体系统,这明显无法满足可靠性要求。网络波动在生产环境中不可避免,而服务端升级重启也属于常态,如果 MCP Client 缺乏自愈能力,那么系统的工具链随时可能失效,导致业务流程意外中断。因此,在 SSE 模式下如何实现可靠的重连能力,成为保障智能体系统可用性的关键。

问题复现

接下来,我给大家演示一下这个问题现象,让大家能够直观的感受到,做这个事情的必要性。

我们先启动MCP Server SSE的项目,端口8003:



然后启动MCP Client,端口8001,代码和前面章节中的一样,通过手动方式,将mcp server 注入到了chatclient之中。



1
2
3
4
5
6
HttpClientSseClientTransport transport = HttpClientSseClientTransport.builder("http://127.0.0.1:8003").sseEndpo
int("/sse").build();McpSyncClient sseClient = McpClient.sync(transport) .clientInfo(new io.modelcontextprotocol.spec.McpSchema.Implementation("sse-client", "1.0")) .requestTimeout(Duration.ofSeconds(10)) .build();sseClient.initialize();
List<McpSyncClient> clients = List.of(sseClient);
SyncMcpToolCallbackProvider provider = SyncMcpToolCallbackProvider.builder() .mcpClients(clients) .build();
ToolCallback[] callbacks = provider.getToolCallbacks();
this.chatClient = ChatClient.builder(chatModel) .defaultToolCallbacks(callbacks) .defaultTools() .build();

连接成功后,我们尝试访问一下,看能不能调用工具:



接下来,问题来了,我现在MCP Server挂了,我们直接断开8003这个项目,然后就可以看到8001这边立马就会产生了报错,这个报错就表明,我们已经连不上MCP Server了。



尝试再访问下接口,我们可以看到工具已经没法用了:



然后我们再恢复MCP Server,重新启动8003端口,但是我们的MCP Client,依然无法感知重连上MCP Server,工具调用依然失败!这在生产环境肯定是没法接受的效果,我不可能为了这种情况,再去手动重启项目,重新初始化,这样根本就没法用。所以,MCP Server的重连机制是不可获取的!





重连机制

要解决这个问题,核心是为 SSE 模式增加一层弹性的连接管理机制,使客户端能够自动检测到 SSE 中断,并主动重新建立连接,重新初始化会话与工具注册流程。这样,即使网络链路被关闭,客户端也能自动完成恢复,不需要人工干预,也不会影响智能体对 MCP 工具的调用。

Spring AI 虽然没有提供现成的重连脚手架,但是他提供了一些有用的方法,可以让我们自行实现重连效果。

核心方法就是 McpSyncClient 的 ping 方法,可以作为我们的心跳检测手段。项目启动的时候先初始化一次,如果初始化失败,就会启动一个后台重试线程,不停地尝试重新初始化。接着再利用一个定时任务,做心跳检测,比如可以每隔 5 秒 ping 一次 MCP Server。并且使用了原子标记,只会启动一个重试线程,不会出现重复创建多个任务的情况。重试线程会一直循环重连,连成功了就自动停止。这样一来,无论是网络抖一下还是服务器重启,客户端都能自动恢复。

工具代码我们还复用之前的一个查天气的Demo:

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class WeatherService {

@Tool(description = "根据城市名称查询天气信息") public String getWeather(String city) {
if (city == null) {
return "请提供城市名称"; }
return switch (city) {
case "北京" -> "北京: 晴, 25°C";
case "上海" -> "上海: 多云, 22°C";
case "深圳" -> "深圳: 小雨, 28°C";
default -> city + ": 下雪, -20°C"; }; }
}

重连机制核心代码逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@Service
@Slf4j
public class RetrySSEMcpServer {

@Autowired private OpenAiChatModel chatModel;
private ChatClient chatClient;
private McpSyncClient sseClient;
// 是否正在重试 initialize(保证唯一性) private final AtomicBoolean retrying = new AtomicBoolean(false);
// initialize 重试线程 private final ExecutorService retryExecutor = Executors.newSingleThreadExecutor();
@PostConstruct public void init() {
log.info("Initializing SSE MCP Client...");
// 初始化 SSE Client this.sseClient = buildClient();
try {
this.sseClient.initialize(); log.info("SSE MCP client initialized."); }
catch (Exception e) {
log.error("Initial SSE initialize failed, will rely on retry thread.", e); // 启动重试线程 startRetryInitialize(); }
// 初始化 toolcallback SyncMcpToolCallbackProvider provider = SyncMcpToolCallbackProvider.builder() .mcpClients(List.of(this.sseClient)) .build();
ToolCallback[] callbacks = provider.getToolCallbacks();
this.chatClient = ChatClient.builder(chatModel) .defaultToolCallbacks(callbacks) .defaultTools() .build(); }
private McpSyncClient buildClient() {
HttpClientSseClientTransport transport = HttpClientSseClientTransport .builder("http://127.0.0.1:8003") .sseEndpo
int("/sse") .build();
return McpClient.sync(transport) .clientInfo(new io.modelcontextprotocol.spec.McpSchema.Implementation("sse-client", "1.0")) .requestTimeout(Duration.ofSeconds(10)) .build(); }
/** * 定时任务:每 5 秒 ping 一次 SSE * ping 不通则触发 initialize 重试线程 */ @Scheduled(fixedDelay = 5000) public void pingSse() {
log.info("SSE MCP ping...");
if (sseClient == null) {
log.warn("SSE client not initialized yet."); startRetryInitialize();
return; }
try {
sseClient.ping(); log.debug("SSE MCP ping OK."); }
catch (Exception e) {
log.error("SSE MCP ping failed: {}", e.getMessage()); startRetryInitialize(); } }
/** * 启动 initialize 重试线程 */ private void startRetryInitialize() {
// 保证只启动一个重试线程 if (!retrying.compareAndSet(false, true)) { return; }
retryExecutor.submit(() -> {
log.warn("Start retrying SSE MCP initialize...");
while (true) {
try {
// 重建 sseClient this.sseClient = buildClient(); this.sseClient.initialize(); log.info("SSE MCP re-initialized successfully.");
// chatclient 也同样需要重建 SyncMcpToolCallbackProvider provider = SyncMcpToolCallbackProvider.builder() .mcpClients(List.of(this.sseClient)) .build();
ToolCallback[] callbacks = provider.getToolCallbacks();
this.chatClient = ChatClient.builder(chatModel) .defaultToolCallbacks(callbacks) .defaultTools() .build();
retrying.set(false);
return; }
catch (Exception e) {
log.warn("Retry initialize failed, will retry in 10s. Reason: {}", e.getMessage()); }
try {
Thread.sleep(10000); }
catch (InterruptedException e) {
throw new RuntimeException(e); } } });
}
public String chat(String userMessage) {
return chatClient.prompt() .user(userMessage) .call() .content(); }
}

需要注意的是除了McpSyncClient需要重新初始化,我们的Chatclient也同样需要初始化,因为 ChatClient 内部的 ToolCallback 是在初始化时注入的。ToolCallback 绑定的 McpSyncClient 是旧的,会话已断开。即使你重新初始化了 sseClient,ChatClient 没有同步更新,仍然会继续调用旧的客户端。

我们再次启动MCP Client 8001端口,和MCP SSE Server 8003端口,来看下效果:



启动成功后,我们断开SSE 8003端口,MCP Client 8001就报错了:



然后我们再把MCP Server SSE 8003端口启动起来,查看MCP Client会不会自己实现重连:



我们可以看到上面的SSE MCP re-initalized已经成功了,我们再尝试下,进行chatclient提问: