Skip to content

MCP Client Initialization Timing and Tool Callback Resolution Issues #4670

@tzolov

Description

@tzolov

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

  1. McpClientAutoConfiguration creates List<McpSyncClient> and List<McpAsyncClient> beans immediately
  2. These clients are configured using specifications derived from ClientMcpAnnotatedBeans
  3. 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

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

  1. User configures: chatClient.prompt().toolCallbacks(mcpToolCallbackProvider)
  2. During configuration, DefaultChatClientRequestSpec immediately calls provider.getToolCallbacks()
  3. At this point, MCP clients may not exist yet or the client list may be empty
  4. 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:

  1. Wait for MCP clients to be fully initialized
  2. Access the complete list of MCP clients (which may be populated by SmartInitializingSingleton)
  3. Query each client for available tools
  4. 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:

  1. MCP tools are discovered dynamically from external MCP servers
  2. MCP tool availability can change at runtime
  3. MCP tool callbacks need to be resolved fresh for each request
  4. 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

  1. MCP Clients: Should be created after all singleton beans are fully initialized, ensuring all MCP annotations are discovered
  2. Tool Callback Providers: Should be resolved lazily at execution time (during call() or stream()), not during configuration
  3. Static Tool Resolver: Should exclude MCP tool callback providers, allowing them to be resolved through the lazy mechanism
  4. List Reference Semantics: Should consistently support reference sharing when needed for deferred population patterns

Related Code

  • McpClientAutoConfiguration
  • McpToolCallbackAutoConfiguration
  • DefaultChatClient.DefaultChatClientRequestSpec
  • ToolCallingAutoConfiguration
  • SyncMcpToolCallbackProvider / AsyncMcpToolCallbackProvider
  • McpClientAnnotationScannerAutoConfiguration

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions