spring-projects / spring-projects/spring-ai
MCP client eagerly connects during context refresh: one unavailable MCP server prevents application startup
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 9.5k
- Forks
- 2.9k
- Avg merge
- 1d 7h
- Merged PRs (30d)
- 6
Description
Bug description
When an MCP server configured through spring.ai.mcp.client.* is unavailable, the entire Spring Boot application fails to start.
McpClientAutoConfiguration#mcpSyncClients() is a plain, non-lazy singleton @Bean that performs a blocking network call (McpSyncClient#initialize()) inside its own factory method:
var client = customizedSpec.build();
if (commonProperties.isInitialized()) {
client.initialize();
}
Spring therefore instantiates it during preInstantiateSingletons() whether or not anything consumes it, so the connection attempt happens at context refresh and its failure aborts application startup. The loop over the configured connections has no per-connection error handling, so the outcome is all-or-nothing regardless of how many other MCP servers are healthy.
This makes every configured MCP server a hard startup dependency. In a realistic setup with several optional external MCP servers — none of them critical to the application's core functionality — one of them being temporarily unavailable should not prevent the application from starting.
The attached reproducer shows this happens with no chat model, no ChatClient, no tools declared and no MCP tool ever invoked: McpClientAutoConfiguration is only conditional on spring.ai.mcp.client.enabled, not on ChatModel, so merely having the MCP client starter on the classpath plus one unreachable connection is enough.
Neither of the two properties that look like they should help provides a usable workaround:
-
spring.ai.mcp.client.toolcallback.enabled=false— no effect on startup. It only gatesMcpToolCallbackAutoConfiguration(whether MCP tools are exposed to the tool calling framework), notMcpClientAutoConfiguration#mcpSyncClients(), so the eagerinitialize()and the startup failure happen exactly as before. -
spring.ai.mcp.client.initialized=false— the application does start, but there is no per-connection isolation, so it trades a fast visible failure for a silent permanent one.SyncMcpToolCallbackProvider#getToolCallbacks()aggregateslistTools()across all configured clients with no per-client error handling, so one unavailable connection breaks tool resolution for every connection, including perfectly healthy ones. The reproducer asserts this explicitly: a healthy server'sechotool resolves fine on its own, and stops resolving as soon as an unrelated unavailable server is added to the configuration. Since the MCP tool callback provider is typically registered as a default tool of aChatClient, in a real application this means every tool-calling request fails — including requests that would only ever need tools from a healthy server, or local non-MCP tools — while the application still looks healthy to a liveness probe.
Environment
- Spring AI: 2.0.1
- Spring Boot: 4.1.1
- Java: 21+ (verified on 25)
- Starter:
spring-ai-starter-mcp-client(JDKHttpClientstreamable-http transport) - No vector store, no chat model involved
Steps to reproduce
Using the reproducer linked below:
git clone https://github.com/francisco-bru/spring-ai-mcp-startup-repro
cd spring-ai-mcp-startup-repro
mvn test # 4 passing tests asserting the behaviour described above
mvn spring-boot:run # or see the startup failure live
The application configures a single MCP server at http://localhost:1 (nothing is ever listening there, which keeps the reproducer self-contained: no external service, no Docker, no credentials) and fails to start with:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'mcpSyncClients'
defined in class path resource
[org/springframework/ai/mcp/client/common/autoconfigure/McpClientAutoConfiguration.class]:
Failed to instantiate [java.util.List]: Factory method 'mcpSyncClients' threw exception with message:
Client failed to initialize by explicit API call
at o.s.b.f.s.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:1121)
at o.s.c.s.AbstractApplicationContext.refresh(AbstractApplicationContext.java:621)
at o.s.boot.SpringApplication.run(SpringApplication.java:321)
Caused by: java.lang.RuntimeException: Client failed to initialize by explicit API call
at io.modelcontextprotocol.client.LifecycleInitializer.lambda$withInitialization$2(LifecycleInitializer.java:287)
at o.s.a.m.c.c.a.McpClientAutoConfiguration.mcpSyncClients(McpClientAutoConfiguration.java:190)
Caused by: java.net.ConnectException
Caused by: java.nio.channels.ClosedChannelException
Expected behavior
An unavailable MCP server should be able to degrade gracefully: the application starts, that server's tools are unavailable (ideally logged as a warning), and every other configured MCP server — and the rest of the application — keeps working.
At minimum, it would help to have a supported way to mark a connection as optional / non-fatal, and for tool resolution in SyncMcpToolCallbackProvider#getToolCallbacks() to isolate per-client failures rather than propagating one client's failure into the aggregated list.
Minimal Complete Reproducible example
👉 https://github.com/francisco-bru/spring-ai-mcp-startup-repro
Self-contained: no LLM, no API keys, no external services, no Docker. The only runtime dependency is spring-ai-starter-mcp-client. The healthy MCP server used by the isolation tests is started in-JVM on a random port by the tests themselves.
McpClientStartupFailureTests contains four passing tests:
| Test | Configuration | Result |
|---|---|---|
defaultConfiguration_failsToStart_whenMcpServerIsUnavailable |
defaults (initialized=true) |
Context refresh aborted while creating mcpSyncClients |
disablingToolCallbacks_doesNotPreventStartupFailure |
toolcallback.enabled=false |
Still fails to start, identically |
lazyInitialization_resolvesToolsOfHealthyServer |
initialized=false, healthy server only |
Control: healthy server's echo tool resolves fine |
lazyInitialization_hasNoPerConnectionIsolation |
initialized=false, healthy and unavailable server |
Starts, but getToolCallbacks() throws, so the healthy server's tools are lost too |
Additional context
I searched for existing issues first. This looks related to, but not covered by, the following:
- #3232 ("MCP Client fails application startup when server is unavailable - need graceful degradation option") was closed as fixed by commit 99ccf9b9072d2e072f7dfeb911adbfcc4d86acd4. That commit is already included in 2.0.1 (it is an ancestor of the
v2.0.1tag), yet the failure still reproduces exactly as originally reported. That fix defersgetToolCallbacks()for providers passed toChatClient.prompt()...toolCallbacks(...), but does not change the eagermcpSyncClients()/initialize()bean instantiation, which is where the startup failure actually originates. - #4459 attempted a broader fix and was closed without merging. Its discussion thread contains a user independently reporting exactly this: injecting
List<McpSyncClient>without ever callinggetToolCallbacks()still fails at startup when the server is down. - #4382 proposed an
InitToolCallbackResolverErrorHandlerextension point for exactly the tool-resolution-failure case described in point 2 above; also closed without merging. - #3410 is a different root cause (a capabilities deserialization error), but is a real-world instance of the same blast radius: the reporter notes the other configured MCP servers work fine and only one misbehaves, yet the whole application fails to start.
- #2740 (still open) covers the related lack of reconnection after an MCP server restarts.
Happy to help test or to contribute a PR if you can indicate the preferred direction (per-connection opt-out property, per-connection error isolation in mcpSyncClients(), per-client isolation in SyncMcpToolCallbackProvider#getToolCallbacks(), or an error-handler extension point).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with McpClientAutoConfiguration#mcpSyncClients() and SyncMcpToolCallbackProvider#getToolCallbacks(), using the linked reproducer and its McpClientStartupFailureTests as the behavioral baseline. Run mvn test and verify that an unavailable server no longer aborts startup or prevents healthy servers' tools and the rest of the application from working.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, spring-boot
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100