-
Notifications
You must be signed in to change notification settings - Fork 2k
Description
There are critical timing and resolution issues in the Spring AI MCP client auto-configuration that prevent MCP-annotated beans from being properly registered and cause tool callbacks to be resolved prematurely.
Issue 1: MCP Client Initialization Timing
MCP clients are created and configured before all Spring singleton beans have been fully initialized. This creates a race condition where beans with MCP annotations (like @McpLogging
, @McpSampling
, @McpToolListChanged
, etc.) may not have been scanned and registered yet when the MCP clients are being configured.
Observed Behavior
McpClientAutoConfiguration
createsList<McpSyncClient>
andList<McpAsyncClient>
beans immediately- These clients are configured using specifications derived from
ClientMcpAnnotatedBeans
- However,
ClientMcpAnnotatedBeans
may not contain all annotated beans yet because:- Some
@Configuration
classes may initialize late - Some beans with MCP annotations may be created after the MCP clients are already configured
- The annotation scanner completes before all beans are fully instantiated
- Some
Impact
- Late-initializing beans with MCP annotations are not included in the MCP client configuration
- Specification beans (like
SyncLoggingSpecification
,AsyncSamplingSpecification
, etc.) are created with an incomplete view of the application context - MCP annotation customizers (
McpSyncAnnotationCustomizer
,McpAsyncAnnotationCustomizer
) are applied with missing specifications - This results in MCP clients that are missing handlers for annotated methods that were meant to be included
Issue 2: Eager Tool Callback Resolution in ChatClient
When ToolCallbackProvider
instances are added to a ChatClient
request spec, they are resolved immediately during the configuration phase by calling getToolCallbacks()
. This causes problems for providers that depend on resources or beans that may not be fully initialized yet.
Problematic Flow
- User configures:
chatClient.prompt().toolCallbacks(mcpToolCallbackProvider)
- During configuration,
DefaultChatClientRequestSpec
immediately callsprovider.getToolCallbacks()
- At this point, MCP clients may not exist yet or the client list may be empty
- Result: Empty or incomplete tool callback list is registered
Impact
MCP*ToolCallbackProvider
implementations try to access MCP client lists that may still be empty- Tool callbacks are computed before the providers are ready
- The lazy initialization pattern expected by Spring AI's architecture is broken
- Empty or incomplete tool callback lists are registered with the chat model
MCP tool callback providers need to:
- Wait for MCP clients to be fully initialized
- Access the complete list of MCP clients (which may be populated by
SmartInitializingSingleton
) - Query each client for available tools
- Create tool callbacks based on discovered tools
All of this must happen at execution time (when call()
or stream()
is invoked), not during configuration.
Issue 3: MCP Tool Callbacks in Static Tool Callback Resolver
The ToolCallingAutoConfiguration
includes all ToolCallbackProvider
implementations (including MCP providers) in the static StaticToolCallbackResolver
. This causes MCP tool callbacks to be resolved eagerly and included in the static resolver, which defeats the purpose of lazy resolution.
MCP tool callbacks should NOT be in the static resolver because:
- MCP tools are discovered dynamically from external MCP servers
- MCP tool availability can change at runtime
- MCP tool callbacks need to be resolved fresh for each request
- Static resolution breaks the lazy loading pattern required for MCP integration
Issue 4: List Reference Semantics in Tool Callback Providers
The SyncMcpToolCallbackProvider.Builder
creates a defensive copy of the MCP clients list, while AsyncMcpToolCallbackProvider.Builder
stores the reference directly. This inconsistency causes confusion and prevents proper sharing of list references when needed for late population.
Impact
- Inconsistent behavior between sync and async implementations
- Defensive copying breaks the ability to share a list reference that will be populated later
- Providers cannot "see" clients added to the list after construction
- Makes it impossible to implement the deferred initialization pattern
Expected Behavior
- MCP Clients: Should be created after all singleton beans are fully initialized, ensuring all MCP annotations are discovered
- Tool Callback Providers: Should be resolved lazily at execution time (during
call()
orstream()
), not during configuration - Static Tool Resolver: Should exclude MCP tool callback providers, allowing them to be resolved through the lazy mechanism
- List Reference Semantics: Should consistently support reference sharing when needed for deferred population patterns
Related Code
McpClientAutoConfiguration
McpToolCallbackAutoConfiguration
DefaultChatClient.DefaultChatClientRequestSpec
ToolCallingAutoConfiguration
SyncMcpToolCallbackProvider
/AsyncMcpToolCallbackProvider
McpClientAnnotationScannerAutoConfiguration