When integrating Spring AI's MPC client in a Spring Boot application, if the MPC server is unavailable during client application startup, the entire application fails to start.
This creates an undesired hard dependency between client and server components, which is problematic in distributed systems. The client application should be able to start successfully even when the MPC server is temporarily unavailable, and should attempt to connect when the client is actually used and the server becomes available.
Error Details
The following exception occurs during application startup when the MPC server is unavailable:
ava.util.concurrent.CompletionException: java.net.ConnectException at java.base/java.util.concurrent.CompletableFuture.encodeRelay(CompletableFuture.java:368) ~[na:na] at java.base/java.util.concurrent.CompletableFuture.completeRelay(CompletableFuture.java:377) ~[na:na] at java.base/java.util.concurrent.CompletableFuture$UniCompose.tryFire(CompletableFuture.java:1152) ~[na:na] at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510) ~[na:na] at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run$$$capture(CompletableFuture.java:1773) ~[na:na] at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java) ~[na:na] at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) ~[na:na] at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) ~[na:na] at java.base/java.lang.Thread.run(Thread.java:842) ~[na:na] Caused by: java.net.ConnectException: null at java.net.http/jdk.internal.net.http.common.Utils.toConnectException(Utils.java:1055) ~[java.net.http:na] at java.net.http/jdk.internal.net.http.PlainHttpConnection.connectAsync(PlainHttpConnection.java:198) ~[java.net.http:na] at java.net.http/jdk.internal.net.http.PlainHttpConnection.checkRetryConnect(PlainHttpConnection.java:230) ~[java.net.http:na] at java.net.http/jdk.internal.net.http.PlainHttpConnection.lambda$connectAsync$1(PlainHttpConnection.java:206) ~[java.net.http:na] at java.base/java.util.concurrent.CompletableFuture.uniHandle(CompletableFuture.java:934) ~[na:na] at java.base/java.util.concurrent.CompletableFuture$UniHandle.tryFire(CompletableFuture.java:911) ~[na:na] ... 6 common frames omitted Caused by: java.nio.channels.ClosedChannelException: null at java.base/sun.nio.ch.SocketChannelImpl.ensureOpen(SocketChannelImpl.java:195) ~[na:na] at java.base/sun.nio.ch.SocketChannelImpl.beginConnect(SocketChannelImpl.java:760) ~[na:na] at java.base/sun.nio.ch.SocketChannelImpl.connect(SocketChannelImpl.java:848) ~[na:na] at java.net.http/jdk.internal.net.http.PlainHttpConnection.lambda$connectAsync$0(PlainHttpConnection.java:183) ~[java.net.http:na] at java.base/java.security.AccessController.doPrivileged(AccessController.java:569) ~[na:na] at java.net.http/jdk.internal.net.http.PlainHttpConnection.connectAsync(PlainHttpConnection.java:185) ~[java.net.http:na] ... 10 common frames omitted
2025-05-18T16:51:05.442+08:00 ERROR 30076 --- [insight-ai] [onPool-worker-2] reactor.core.publisher.Operators : Operator called default onErrorDropped
reactor.core.Exceptions$ErrorCallbackNotImplemented: java.net.ConnectException Caused by: java.net.ConnectException: null at java.net.http/jdk.internal.net.http.common.Utils.toConnectException(Utils.java:1055) ~[java.net.http:na] Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Assembly trace from producer [reactor.core.publisher.MonoCompletionStage] : reactor.core.publisher.Mono.fromFuture(Mono.java:629) io.modelcontextprotocol.client.transport.HttpClientSseClientTransport.connect(HttpClientSseClientTransport.java:177) Error has been observed at the following site(s): *__Mono.fromFuture ⇢ at io.modelcontextprotocol.client.transport.HttpClientSseClientTransport.connect(HttpClientSseClientTransport.java:177) Original Stack Trace: at java.net.http/jdk.internal.net.http.common.Utils.toConnectException(Utils.java:1055) ~[java.net.http:na] at java.net.http/jdk.internal.net.http.PlainHttpConnection.connectAsync(PlainHttpConnection.java:198) ~[java.net.http:na] at java.net.http/jdk.internal.net.http.PlainHttpConnection.checkRetryConnect(PlainHttpConnection.java:230) ~[java.net.http:na] at java.net.http/jdk.internal.net.http.PlainHttpConnection.lambda$connectAsync$1(PlainHttpConnection.java:206) ~[java.net.http:na] at java.base/java.util.concurrent.CompletableFuture.uniHandle(CompletableFuture.java:934) ~[na:na] at java.base/java.util.concurrent.CompletableFuture$UniHandle.tryFire(CompletableFuture.java:911) ~[na:na] at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510) ~[na:na] at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run$$$capture(CompletableFuture.java:1773) ~[na:na] at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java) ~[na:na] at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) ~[na:na] at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) ~[na:na] at java.base/java.lang.Thread.run(Thread.java:842) ~[na:na] Caused by: java.nio.channels.ClosedChannelException: null at java.base/sun.nio.ch.SocketChannelImpl.ensureOpen(SocketChannelImpl.java:195) ~[na:na] at java.base/sun.nio.ch.SocketChannelImpl.beginConnect(SocketChannelImpl.java:760) ~[na:na] at java.base/sun.nio.ch.SocketChannelImpl.connect(SocketChannelImpl.java:848) ~[na:na] at java.net.http/jdk.internal.net.http.PlainHttpConnection.lambda$connectAsync$0(PlainHttpConnection.java:183) ~[java.net.http:na] at java.base/java.security.AccessController.doPrivileged(AccessController.java:569) ~[na:na] at java.net.http/jdk.internal.net.http.PlainHttpConnection.connectAsync(PlainHttpConnection.java:185) ~[java.net.http:na] at java.net.http/jdk.internal.net.http.PlainHttpConnection.checkRetryConnect(PlainHttpConnection.java:230) ~[java.net.http:na] at java.net.http/jdk.internal.net.http.PlainHttpConnection.lambda$connectAsync$1(PlainHttpConnection.java:206) ~[java.net.http:na] at java.base/java.util.concurrent.CompletableFuture.uniHandle(CompletableFuture.java:934) ~[na:na] at java.base/java.util.concurrent.CompletableFuture$UniHandle.tryFire(CompletableFuture.java:911) ~[na:na] at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510) ~[na:na] at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run$$$capture(CompletableFuture.java:1773) ~[na:na] at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java) ~[na:na] at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) ~[na:na] at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) ~[na:na] at java.base/java.lang.Thread.run(Thread.java:842) ~[na:na]
2025-05-18T16:51:05.444+08:00 ERROR 30076 --- [insight-ai] [onPool-worker-1] i.m.c.t.WebFluxSseClientTransport : Fatal SSE error, not retrying: null 2025-05-18T16:51:05.445+08:00 ERROR 30076 --- [insight-ai] [onPool-worker-1] reactor.core.publisher.Operators : Operator called default onErrorDropped
reactor.core.Exceptions$ErrorCallbackNotImplemented: org.springframework.web.reactive.function.client.WebClientRequestException Caused by: org.springframework.web.reactive.function.client.WebClientRequestException: null at org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.lambda$wrapException$9(ExchangeFunctions.java:137) ~[spring-webflux-6.2.5.jar:6.2.5] Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Assembly trace from producer [reactor.core.publisher.MonoErrorSupplied] : reactor.core.publisher.Mono.error(Mono.java:315) org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.wrapException(ExchangeFunctions.java:137) Error has been observed at the following site(s): ______Mono.error ⇢ at org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.wrapException(ExchangeFunctions.java:137) _Mono.onErrorResume ⇢ at org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.exchange(ExchangeFunctions.java:106) | Mono.map ⇢ at org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.exchange(ExchangeFunctions.java:107) |_ Mono.doOnNext ⇢ at org.springframework.web.reactive.function.client.DefaultWebClient$ObservationFilterFunction.filter(DefaultWebClient.java:745) ______Mono.defer ⇢ at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.lambda$exchange$12(DefaultWebClient.java:467) | checkpoint ⇢ Request to GET http://localhost:8081/sse [DefaultWebClient] | Mono.switchIfEmpty ⇢ at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.lambda$exchange$12(DefaultWebClient.java:472) | Mono.doOnNext ⇢ at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.lambda$exchange$12(DefaultWebClient.java:478) | Mono.doOnError ⇢ at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.lambda$exchange$12(DefaultWebClient.java:479) | Mono.doFinally ⇢ at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.lambda$exchange$12(DefaultWebClient.java:480) | Mono.contextWrite ⇢ at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.lambda$exchange$12(DefaultWebClient.java:486) Mono.deferContextual ⇢ at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.exchange(DefaultWebClient.java:453) | Mono.flatMapMany ⇢ at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultResponseSpec.bodyToFlux(DefaultWebClient.java:601) | Flux.retryWhen ⇢ at io.modelcontextprotocol.client.transport.WebFluxSseClientTransport.eventStream(WebFluxSseClientTransport.java:261) | Flux.concatMap ⇢ at io.modelcontextprotocol.client.transport.WebFluxSseClientTransport.connect(WebFluxSseClientTransport.java:172) _____Flux.handle ⇢ at io.modelcontextprotocol.client.transport.WebFluxSseClientTransport.lambda$eventStream$5(WebFluxSseClientTransport.java:261) Original Stack Trace: at org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.lambda$wrapException$9(ExchangeFunctions.java:137) ~[spring-webflux-6.2.5.jar:6.2.5] at reactor.core.publisher.MonoErrorSupplied.subscribe(MonoErrorSupplied.java:55) ~[reactor-core-3.7.4.jar:3.7.4] at reactor.core.publisher.Mono.subscribe(Mono.java:4576) ~[reactor-core-3.7.4.jar:3.7.4] at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onError(FluxOnErrorResume.java:103) ~[reactor-core-3.7.4.jar:3.7.4] at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onError(FluxPeekFuseable.java:234) ~[reactor-core-3.7.4.jar:3.7.4] at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onError(FluxPeekFuseable.java:234) ~[reactor-core-3.7.4.jar:3.7.4] at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.onError(MonoIgnoreThen.java:280) ~[reactor-core-3.7.4.jar:3.7.4] at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onError(FluxMapFuseable.java:142) ~[reactor-core-3.7.4.jar:3.7.4] at reactor.core.publisher.MonoCompletionStage$MonoCompletionStageSubscription.apply(MonoCompletionStage.java:115) ~[reactor-core-3.7.4.jar:3.7.4] at reactor.core.publisher.MonoCompletionStage$MonoCompletionStageSubscription.apply(MonoCompletionStage.java:67) ~[reactor-core-3.7.4.jar:3.7.4] at java.base/java.util.concurrent.CompletableFuture.uniHandle(CompletableFuture.java:934) ~[na:na] at java.base/java.util.concurrent.CompletableFuture$UniHandle.tryFire(CompletableFuture.java:911) ~[na:na] at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510) ~[na:na] at java.base/java.util.concurrent.CompletableFuture.postFire(CompletableFuture.java:614) ~[na:na] at java.base/java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:844) ~[na:na] at java.base/java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483) ~[na:na] at java.base/java.util.concurrent.ForkJoinTask.doExec$$$capture(ForkJoinTask.java:373) ~[na:na] at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java) ~[na:na] at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) ~[na:na] at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) ~[na:na] at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) ~[na:na] at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ~[na:na] Caused by: java.net.ConnectException: null at java.net.http/jdk.internal.net.http.common.Utils.toConnectException(Utils.java:1055) ~[java.net.http:na] Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Assembly trace from producer [reactor.core.publisher.MonoCompletionStage] : reactor.core.publisher.Mono.fromCompletionStage(Mono.java:543) org.springframework.http.client.reactive.JdkClientHttpConnector.lambda$connect$1(JdkClientHttpConnector.java:123) Error has been observed at the following site(s): __Mono.fromCompletionStage ⇢ at org.springframework.http.client.reactive.JdkClientHttpConnector.lambda$connect$1(JdkClientHttpConnector.java:123) | Mono.map ⇢ at org.springframework.http.client.reactive.JdkClientHttpConnector.lambda$connect$1(JdkClientHttpConnector.java:124) ____Mono.defer ⇢ at org.springframework.http.client.reactive.JdkClientHttpConnector.connect(JdkClientHttpConnector.java:117) _____Mono.then ⇢ at org.springframework.http.client.reactive.JdkClientHttpConnector.connect(JdkClientHttpConnector.java:117) | Mono.doOnRequest ⇢ at org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.exchange(ExchangeFunctions.java:104) | Mono.doOnCancel ⇢ at org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.exchange(ExchangeFunctions.java:105) Original Stack Trace: at java.net.http/jdk.internal.net.http.common.Utils.toConnectException(Utils.java:1055) ~[java.net.http:na] at java.net.http/jdk.internal.net.http.PlainHttpConnection.connectAsync(PlainHttpConnection.java:198) ~[java.net.http:na] at java.net.http/jdk.internal.net.http.PlainHttpConnection.checkRetryConnect(PlainHttpConnection.java:230) ~[java.net.http:na] at java.net.http/jdk.internal.net.http.PlainHttpConnection.lambda$connectAsync$1(PlainHttpConnection.java:206) ~[java.net.http:na] at java.base/java.util.concurrent.CompletableFuture.uniHandle(CompletableFuture.java:934) ~[na:na] at java.base/java.util.concurrent.CompletableFuture$UniHandle.tryFire(CompletableFuture.java:911) ~[na:na] at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510) ~[na:na] at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run$$$capture(CompletableFuture.java:1773) ~[na:na] at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java) ~[na:na] at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) ~[na:na] at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) ~[na:na] at java.base/java.lang.Thread.run(Thread.java:842) ~[na:na] Caused by: java.nio.channels.ClosedChannelException: null at java.base/sun.nio.ch.SocketChannelImpl.ensureOpen(SocketChannelImpl.java:195) ~[na:na] at java.base/sun.nio.ch.SocketChannelImpl.beginConnect(SocketChannelImpl.java:760) ~[na:na] at java.base/sun.nio.ch.SocketChannelImpl.connect(SocketChannelImpl.java:848) ~[na:na] at java.net.http/jdk.internal.net.http.PlainHttpConnection.lambda$connectAsync$0(PlainHttpConnection.java:183) ~[java.net.http:na] at java.base/java.security.AccessController.doPrivileged(AccessController.java:569) ~[na:na] at java.net.http/jdk.internal.net.http.PlainHttpConnection.connectAsync(PlainHttpConnection.java:185) ~[java.net.http:na] at java.net.http/jdk.internal.net.http.PlainHttpConnection.checkRetryConnect(PlainHttpConnection.java:230) ~[java.net.http:na] at java.net.http/jdk.internal.net.http.PlainHttpConnection.lambda$connectAsync$1(PlainHttpConnection.java:206) ~[java.net.http:na]
The error occurs in HttpClientSseClientTransport.connect()
when attempting to establish an SSE connection to the MPC server at application startup.
Expected Behavior
The client application should: 1. Start successfully regardless of MPC server availability 2. Automatically connect to the MPC server when it becomes available 3. Provide appropriate error handling or fallback responses when the server is unavailable during actual usage
Current Workarounds
Currently, I have to implement complex workarounds such as: - Custom ApplicationContextInitializer to check server availability before startup - BeanFactoryPostProcessor to modify bean definitions - Bean post-processors to replace or disable problematic beans - Manual exception handling around every client usage
These workarounds add complexity and diverge from Spring's "convention over configuration" philosophy.
Feature Request
Please consider adding configuration options to enable graceful degradation when the MPC server is unavailable:
```properties
Proposed configuration options
spring.ai.mcp.fail-fast=false # Don't fail application startup when server is unavailable spring.ai.mcp.connect-on-startup=false # Don't try to connect during initialization spring.ai.mcp.lazy-connection=true # Only connect when the client is actually used spring.ai.mcp.reconnect-interval=5000 # Try reconnecting every 5 seconds spring.ai.mcp.connection-failure-mode=silent # Options: fail, warn, silent, retry
Comment From: lianneli
LGTM! I'm also in great need of this function.
Comment From: ibrahimsahin
@tzolov @markpollack Is there anyway to manage sse connection manually? Reconnect or renew session etc. And update session info on client side. In production mcp is not sufficient.In local everything is ok but in cloud(gcp) there are timeouts and if one time connection lost program has crash.
Comment From: yuyu1025
I'm considering integrating Resilience4j into WebClient. Is this a good choice? Give me some opinions.
Comment From: kakawait
Yes coupling client with servers (because we can have multiple MCP servers subscribed by 1 client) isn't a good idea.
In addition, if you're MCP server is auth and isn't hardcoded auth like shared key like access_token, but you'd like to forward MCP client app auth to MCP server isn't possible because MCP client is performing connection at app startup...
Comment From: waltertan1988
Dear @tzolov @markpollack, I think this issue not only happens in mcp tools, but also in any tools that depended on the network (eg. database). Let's see this method: org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration#toolCallbackResolver, it will make application startup fail when getToolCallbacks() fails. Apart from mcp client, maybe we can also provide a extension to customize the fail action but not just throw errors to JVM?Also see PR#4382.
Comment From: a-simeshin
+1 to PR from @waltertan1988
Tested with 1.1.0-M1 on client and server side with both spring.ai.mcp.client.initialized
true and false, both leads to startup fail if the server is unavailable.