diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/McpClientAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/McpClientAutoConfiguration.java
index e6e60920c8b..0bed3f77765 100644
--- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/McpClientAutoConfiguration.java
+++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/McpClientAutoConfiguration.java
@@ -23,6 +23,15 @@
 import io.modelcontextprotocol.client.McpClient;
 import io.modelcontextprotocol.client.McpSyncClient;
 import io.modelcontextprotocol.spec.McpSchema;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springaicommunity.mcp.annotation.McpElicitation;
+import org.springaicommunity.mcp.annotation.McpLogging;
+import org.springaicommunity.mcp.annotation.McpProgress;
+import org.springaicommunity.mcp.annotation.McpPromptListChanged;
+import org.springaicommunity.mcp.annotation.McpResourceListChanged;
+import org.springaicommunity.mcp.annotation.McpSampling;
+import org.springaicommunity.mcp.annotation.McpToolListChanged;
 import org.springaicommunity.mcp.method.changed.prompt.AsyncPromptListChangedSpecification;
 import org.springaicommunity.mcp.method.changed.prompt.SyncPromptListChangedSpecification;
 import org.springaicommunity.mcp.method.changed.resource.AsyncResourceListChangedSpecification;
@@ -38,7 +47,10 @@
 import org.springaicommunity.mcp.method.sampling.AsyncSamplingSpecification;
 import org.springaicommunity.mcp.method.sampling.SyncSamplingSpecification;
 
+import org.springframework.ai.mcp.annotation.spring.AsyncMcpAnnotationProviders;
+import org.springframework.ai.mcp.annotation.spring.SyncMcpAnnotationProviders;
 import org.springframework.ai.mcp.client.common.autoconfigure.annotations.McpAsyncAnnotationCustomizer;
+import org.springframework.ai.mcp.client.common.autoconfigure.annotations.McpClientAnnotationScannerAutoConfiguration.ClientMcpAnnotatedBeans;
 import org.springframework.ai.mcp.client.common.autoconfigure.annotations.McpSyncAnnotationCustomizer;
 import org.springframework.ai.mcp.client.common.autoconfigure.configurer.McpAsyncClientConfigurer;
 import org.springframework.ai.mcp.client.common.autoconfigure.configurer.McpSyncClientConfigurer;
@@ -46,6 +58,7 @@
 import org.springframework.ai.mcp.customizer.McpAsyncClientCustomizer;
 import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer;
 import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.SmartInitializingSingleton;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -137,60 +150,45 @@ private String connectedClientName(String clientName, String serverConnectionNam
 	}
 
 	/**
-	 * Creates a list of {@link McpSyncClient} instances based on the available
-	 * transports.
+	 * Creates a {@link McpSyncClientInitializer} that defers client creation until all
+	 * singleton beans have been initialized.
 	 *
 	 * 
-	 * Each client is configured with:
-	 * 
-	 * - Client information (name and version) from common properties
-	 * 
- Request timeout settings
-	 * 
- Custom configurations through {@link McpSyncClientConfigurer}
-	 * 
-	 *
-	 *
-	 * If initialization is enabled in properties, the clients are automatically
-	 * initialized.
+	 * This ensures that all beans with MCP annotations have been scanned and registered
+	 * before the clients are created, preventing a timing issue where late-initialized
+	 * beans might miss registration.
 	 * @param mcpSyncClientConfigurer the configurer for customizing client creation
 	 * @param commonProperties common MCP client properties
 	 * @param transportsProvider provider of named MCP transports
-	 * @return list of configured MCP sync clients
+	 * @param annotatedBeans registry of beans with MCP annotations
+	 * @return the client initializer that creates clients after singleton instantiation
 	 */
 	@Bean
 	@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
 			matchIfMissing = true)
-	public List mcpSyncClients(McpSyncClientConfigurer mcpSyncClientConfigurer,
+	public McpSyncClientInitializer mcpSyncClientInitializer(McpSyncClientConfigurer mcpSyncClientConfigurer,
 			McpClientCommonProperties commonProperties,
-			ObjectProvider> transportsProvider) {
-
-		List mcpSyncClients = new ArrayList<>();
-
-		List namedTransports = transportsProvider.stream().flatMap(List::stream).toList();
-
-		if (!CollectionUtils.isEmpty(namedTransports)) {
-			for (NamedClientMcpTransport namedTransport : namedTransports) {
-
-				McpSchema.Implementation clientInfo = new McpSchema.Implementation(
-						this.connectedClientName(commonProperties.getName(), namedTransport.name()),
-						namedTransport.name(), commonProperties.getVersion());
-
-				McpClient.SyncSpec spec = McpClient.sync(namedTransport.transport())
-					.clientInfo(clientInfo)
-					.requestTimeout(commonProperties.getRequestTimeout());
-
-				spec = mcpSyncClientConfigurer.configure(namedTransport.name(), spec);
-
-				var client = spec.build();
-
-				if (commonProperties.isInitialized()) {
-					client.initialize();
-				}
-
-				mcpSyncClients.add(client);
-			}
-		}
+			ObjectProvider> transportsProvider,
+			ObjectProvider annotatedBeansProvider) {
+		return new McpSyncClientInitializer(this, mcpSyncClientConfigurer, commonProperties, transportsProvider,
+				annotatedBeansProvider);
+	}
 
-		return mcpSyncClients;
+	/**
+	 * Provides the list of {@link McpSyncClient} instances created by the initializer.
+	 *
+	 * 
+	 * This bean is available after all singleton beans have been initialized.
+	 * @param initializer the client initializer
+	 * @return list of configured MCP sync clients
+	 */
+	@Bean
+	@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
+			matchIfMissing = true)
+	public List mcpSyncClients(McpSyncClientInitializer initializer) {
+		// Return the client list directly - it will be populated by
+		// SmartInitializingSingleton
+		return initializer.getClients();
 	}
 
 	/**
@@ -222,81 +220,341 @@ McpSyncClientConfigurer mcpSyncClientConfigurer(ObjectProvider
+	 * This ensures that all beans with MCP annotations have been scanned and registered
+	 * before the clients are created, preventing a timing issue where late-initialized
+	 * beans might miss registration.
+	 * @param mcpAsyncClientConfigurer the configurer for customizing client creation
+	 * @param commonProperties common MCP client properties
+	 * @param transportsProvider provider of named MCP transports
+	 * @param annotatedBeans registry of beans with MCP annotations
+	 * @return the client initializer that creates clients after singleton instantiation
+	 */
 	@Bean
-	@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
-			matchIfMissing = true)
-	public McpSyncClientCustomizer mcpAnnotationMcpSyncClientCustomizer(List loggingSpecs,
-			List samplingSpecs, List elicitationSpecs,
-			List progressSpecs,
-			List syncToolListChangedSpecifications,
-			List syncResourceListChangedSpecifications,
-			List syncPromptListChangedSpecifications) {
-		return new McpSyncAnnotationCustomizer(samplingSpecs, loggingSpecs, elicitationSpecs, progressSpecs,
-				syncToolListChangedSpecifications, syncResourceListChangedSpecifications,
-				syncPromptListChangedSpecifications);
+	@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
+	public McpAsyncClientInitializer mcpAsyncClientInitializer(McpAsyncClientConfigurer mcpAsyncClientConfigurer,
+			McpClientCommonProperties commonProperties,
+			ObjectProvider> transportsProvider,
+			ObjectProvider annotatedBeansProvider) {
+		return new McpAsyncClientInitializer(this, mcpAsyncClientConfigurer, commonProperties, transportsProvider,
+				annotatedBeansProvider);
 	}
 
-	// Async client configuration
+	/**
+	 * Provides the list of {@link McpAsyncClient} instances created by the initializer.
+	 *
+	 * 
+	 * This bean is available after all singleton beans have been initialized.
+	 * @param initializer the client initializer
+	 * @return list of configured MCP async clients
+	 */
+	@Bean
+	@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
+	public List mcpAsyncClients(McpAsyncClientInitializer initializer) {
+		// Return the client list directly - it will be populated by
+		// SmartInitializingSingleton
+		return initializer.getClients();
+	}
 
 	@Bean
 	@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
-	public List mcpAsyncClients(McpAsyncClientConfigurer mcpAsyncClientConfigurer,
-			McpClientCommonProperties commonProperties,
-			ObjectProvider> transportsProvider) {
+	public CloseableMcpAsyncClients makeAsyncClientsClosable(List clients) {
+		return new CloseableMcpAsyncClients(clients);
+	}
+
+	@Bean
+	@ConditionalOnMissingBean
+	@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
+	McpAsyncClientConfigurer mcpAsyncClientConfigurer(ObjectProvider customizerProvider) {
+		return new McpAsyncClientConfigurer(customizerProvider.orderedStream().toList());
+	}
+
+	/**
+	 * Initializer for MCP synchronous clients that implements
+	 * {@link SmartInitializingSingleton}.
+	 *
+	 * 
+	 * This class defers the creation of MCP sync clients until after all singleton beans
+	 * have been initialized. This ensures that all beans with MCP-annotated methods have
+	 * been scanned and registered in the {@link ClientMcpAnnotatedBeans} registry before
+	 * the specifications are created and clients are configured.
+	 *
+	 * 
+	 * The initialization process:
+	 * 
+	 * - Wait for all singleton beans to be instantiated
+	 * 
- Re-evaluate specifications from the complete registry
+	 * 
- Create and configure MCP clients with all registered specifications
+	 * 
- Initialize clients if configured to do so
+	 * 
+	 */
+	public static class McpSyncClientInitializer implements SmartInitializingSingleton {
+
+		private static final Logger logger = LoggerFactory.getLogger(McpSyncClientInitializer.class);
+
+		private final McpClientAutoConfiguration configuration;
+
+		private final McpSyncClientConfigurer configurer;
+
+		private final McpClientCommonProperties properties;
+
+		private final ObjectProvider> transportsProvider;
+
+		private final ObjectProvider annotatedBeansProvider;
+
+		final List clients = new ArrayList<>();
+
+		private long initializationTimestamp = -1;
+
+		public McpSyncClientInitializer(McpClientAutoConfiguration configuration, McpSyncClientConfigurer configurer,
+				McpClientCommonProperties properties, ObjectProvider> transportsProvider,
+				ObjectProvider annotatedBeansProvider) {
+			this.configuration = configuration;
+			this.configurer = configurer;
+			this.properties = properties;
+			this.transportsProvider = transportsProvider;
+			this.annotatedBeansProvider = annotatedBeansProvider;
+		}
+
+		@Override
+		public void afterSingletonsInstantiated() {
+			// Record when initialization starts
+			this.initializationTimestamp = System.nanoTime();
+
+			logger.debug("Creating MCP sync clients after all singleton beans have been instantiated");
+
+			McpSyncClientCustomizer annotationCustomizer = null;
 
-		List mcpAsyncClients = new ArrayList<>();
+			// Only create annotation customizer if annotated beans registry is available
+			ClientMcpAnnotatedBeans annotatedBeans = this.annotatedBeansProvider.getIfAvailable();
+			if (annotatedBeans != null) {
+				// Re-create specifications from the now-complete registry
+				List loggingSpecs = SyncMcpAnnotationProviders
+					.loggingSpecifications(annotatedBeans.getBeansByAnnotation(McpLogging.class));
 
-		List namedTransports = transportsProvider.stream().flatMap(List::stream).toList();
+				List samplingSpecs = SyncMcpAnnotationProviders
+					.samplingSpecifications(annotatedBeans.getBeansByAnnotation(McpSampling.class));
 
-		if (!CollectionUtils.isEmpty(namedTransports)) {
-			for (NamedClientMcpTransport namedTransport : namedTransports) {
+				List elicitationSpecs = SyncMcpAnnotationProviders
+					.elicitationSpecifications(annotatedBeans.getBeansByAnnotation(McpElicitation.class));
 
-				McpSchema.Implementation clientInfo = new McpSchema.Implementation(
-						this.connectedClientName(commonProperties.getName(), namedTransport.name()),
-						commonProperties.getVersion());
+				List progressSpecs = SyncMcpAnnotationProviders
+					.progressSpecifications(annotatedBeans.getBeansByAnnotation(McpProgress.class));
 
-				McpClient.AsyncSpec spec = McpClient.async(namedTransport.transport())
-					.clientInfo(clientInfo)
-					.requestTimeout(commonProperties.getRequestTimeout());
+				List toolListChangedSpecs = SyncMcpAnnotationProviders
+					.toolListChangedSpecifications(annotatedBeans.getBeansByAnnotation(McpToolListChanged.class));
 
-				spec = mcpAsyncClientConfigurer.configure(namedTransport.name(), spec);
+				List resourceListChangedSpecs = SyncMcpAnnotationProviders
+					.resourceListChangedSpecifications(
+							annotatedBeans.getBeansByAnnotation(McpResourceListChanged.class));
 
-				var client = spec.build();
+				List promptListChangedSpecs = SyncMcpAnnotationProviders
+					.promptListChangedSpecifications(annotatedBeans.getBeansByAnnotation(McpPromptListChanged.class));
+
+				// Create the annotation customizer with fresh specifications
+				annotationCustomizer = new McpSyncAnnotationCustomizer(samplingSpecs, loggingSpecs, elicitationSpecs,
+						progressSpecs, toolListChangedSpecs, resourceListChangedSpecs, promptListChangedSpecs);
+			}
+
+			// Create the clients using the base configurer and annotation customizer (if
+			// available)
+			List createdClients = createClients(this.configurer, annotationCustomizer);
+			this.clients.addAll(createdClients);
+
+			logger.info("Created {} MCP sync client(s)", this.clients.size());
+		}
 
-				if (commonProperties.isInitialized()) {
-					client.initialize().block();
+		private List createClients(McpSyncClientConfigurer configurer,
+				McpSyncClientCustomizer annotationCustomizer) {
+			List mcpSyncClients = new ArrayList<>();
+
+			List namedTransports = this.transportsProvider.stream()
+				.flatMap(List::stream)
+				.toList();
+
+			if (!CollectionUtils.isEmpty(namedTransports)) {
+				for (NamedClientMcpTransport namedTransport : namedTransports) {
+
+					McpSchema.Implementation clientInfo = new McpSchema.Implementation(
+							this.configuration.connectedClientName(this.properties.getName(), namedTransport.name()),
+							namedTransport.name(), this.properties.getVersion());
+
+					McpClient.SyncSpec spec = McpClient.sync(namedTransport.transport())
+						.clientInfo(clientInfo)
+						.requestTimeout(this.properties.getRequestTimeout());
+
+					spec = configurer.configure(namedTransport.name(), spec);
+
+					// Apply annotation customizer after other customizers (if available)
+					if (annotationCustomizer != null) {
+						annotationCustomizer.customize(namedTransport.name(), spec);
+					}
+
+					var client = spec.build();
+
+					if (this.properties.isInitialized()) {
+						client.initialize();
+					}
+
+					mcpSyncClients.add(client);
 				}
+			}
 
-				mcpAsyncClients.add(client);
+			return mcpSyncClients;
+		}
+
+		public List getClients() {
+			if (this.clients == null) {
+				throw new IllegalStateException(
+						"MCP sync clients not yet initialized. They are created after all singleton beans are instantiated.");
 			}
+			return this.clients;
 		}
 
-		return mcpAsyncClients;
-	}
+		/**
+		 * Returns the timestamp (in nanoseconds) when afterSingletonsInstantiated() was
+		 * called. This can be used in tests to verify SmartInitializingSingleton timing.
+		 * @return the initialization timestamp, or -1 if not yet initialized
+		 */
+		public long getInitializationTimestamp() {
+			return this.initializationTimestamp;
+		}
 
-	@Bean
-	@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
-	public CloseableMcpAsyncClients makeAsyncClientsClosable(List clients) {
-		return new CloseableMcpAsyncClients(clients);
 	}
 
-	@Bean
-	@ConditionalOnMissingBean
-	@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
-	McpAsyncClientConfigurer mcpAsyncClientConfigurer(ObjectProvider customizerProvider) {
-		return new McpAsyncClientConfigurer(customizerProvider.orderedStream().toList());
-	}
+	/**
+	 * Initializer for MCP asynchronous clients that implements
+	 * {@link SmartInitializingSingleton}.
+	 *
+	 * 
+	 * This class defers the creation of MCP async clients until after all singleton beans
+	 * have been initialized. This ensures that all beans with MCP-annotated methods have
+	 * been scanned and registered in the {@link ClientMcpAnnotatedBeans} registry before
+	 * the specifications are created and clients are configured.
+	 */
+	public static class McpAsyncClientInitializer implements SmartInitializingSingleton {
+
+		private static final Logger logger = LoggerFactory.getLogger(McpAsyncClientInitializer.class);
+
+		private final McpClientAutoConfiguration configuration;
+
+		private final McpAsyncClientConfigurer configurer;
+
+		private final McpClientCommonProperties properties;
+
+		private final ObjectProvider> transportsProvider;
+
+		private final ObjectProvider annotatedBeansProvider;
+
+		final List clients = new ArrayList<>();
+
+		public McpAsyncClientInitializer(McpClientAutoConfiguration configuration, McpAsyncClientConfigurer configurer,
+				McpClientCommonProperties properties, ObjectProvider> transportsProvider,
+				ObjectProvider annotatedBeansProvider) {
+			this.configuration = configuration;
+			this.configurer = configurer;
+			this.properties = properties;
+			this.transportsProvider = transportsProvider;
+			this.annotatedBeansProvider = annotatedBeansProvider;
+		}
+
+		@Override
+		public void afterSingletonsInstantiated() {
+			logger.debug("Creating MCP async clients after all singleton beans have been instantiated");
+
+			McpAsyncClientCustomizer annotationCustomizer = null;
+
+			// Only create annotation customizer if annotated beans registry is available
+			ClientMcpAnnotatedBeans annotatedBeans = this.annotatedBeansProvider.getIfAvailable();
+			if (annotatedBeans != null) {
+				// Re-create specifications from the now-complete registry
+				List loggingSpecs = AsyncMcpAnnotationProviders
+					.loggingSpecifications(annotatedBeans.getAllAnnotatedBeans());
+
+				List samplingSpecs = AsyncMcpAnnotationProviders
+					.samplingSpecifications(annotatedBeans.getAllAnnotatedBeans());
+
+				List elicitationSpecs = AsyncMcpAnnotationProviders
+					.elicitationSpecifications(annotatedBeans.getAllAnnotatedBeans());
+
+				List progressSpecs = AsyncMcpAnnotationProviders
+					.progressSpecifications(annotatedBeans.getAllAnnotatedBeans());
+
+				List toolListChangedSpecs = AsyncMcpAnnotationProviders
+					.toolListChangedSpecifications(annotatedBeans.getAllAnnotatedBeans());
+
+				List resourceListChangedSpecs = AsyncMcpAnnotationProviders
+					.resourceListChangedSpecifications(annotatedBeans.getAllAnnotatedBeans());
+
+				List promptListChangedSpecs = AsyncMcpAnnotationProviders
+					.promptListChangedSpecifications(annotatedBeans.getAllAnnotatedBeans());
+
+				// Create the annotation customizer with fresh specifications
+				annotationCustomizer = new McpAsyncAnnotationCustomizer(samplingSpecs, loggingSpecs, elicitationSpecs,
+						progressSpecs, toolListChangedSpecs, resourceListChangedSpecs, promptListChangedSpecs);
+			}
+
+			// Create the clients using the base configurer and annotation customizer (if
+			// available)
+			List createdClients = createClients(this.configurer, annotationCustomizer);
+			this.clients.addAll(createdClients);
+
+			logger.info("Created {} MCP async client(s)", this.clients.size());
+		}
+
+		private List createClients(McpAsyncClientConfigurer configurer,
+				McpAsyncClientCustomizer annotationCustomizer) {
+			List mcpAsyncClients = new ArrayList<>();
+
+			List namedTransports = this.transportsProvider.stream()
+				.flatMap(List::stream)
+				.toList();
+
+			if (!CollectionUtils.isEmpty(namedTransports)) {
+				for (NamedClientMcpTransport namedTransport : namedTransports) {
+
+					McpSchema.Implementation clientInfo = new McpSchema.Implementation(
+							this.configuration.connectedClientName(this.properties.getName(), namedTransport.name()),
+							this.properties.getVersion());
+
+					McpClient.AsyncSpec spec = McpClient.async(namedTransport.transport())
+						.clientInfo(clientInfo)
+						.requestTimeout(this.properties.getRequestTimeout());
+
+					spec = configurer.configure(namedTransport.name(), spec);
+
+					// Apply annotation customizer after other customizers (if available)
+					if (annotationCustomizer != null) {
+						annotationCustomizer.customize(namedTransport.name(), spec);
+					}
+
+					var client = spec.build();
+
+					if (this.properties.isInitialized()) {
+						client.initialize().block();
+					}
+
+					mcpAsyncClients.add(client);
+				}
+			}
+
+			return mcpAsyncClients;
+		}
+
+		public List getClients() {
+			if (this.clients == null) {
+				throw new IllegalStateException(
+						"MCP async clients not yet initialized. They are created after all singleton beans are instantiated.");
+			}
+			return this.clients;
+		}
 
-	@Bean
-	@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
-	public McpAsyncClientCustomizer mcpAnnotationMcpAsyncClientCustomizer(List loggingSpecs,
-			List samplingSpecs, List elicitationSpecs,
-			List progressSpecs,
-			List toolListChangedSpecs,
-			List resourceListChangedSpecs,
-			List promptListChangedSpecs) {
-		return new McpAsyncAnnotationCustomizer(samplingSpecs, loggingSpecs, elicitationSpecs, progressSpecs,
-				toolListChangedSpecs, resourceListChangedSpecs, promptListChangedSpecs);
 	}
 
 	/**
@@ -313,13 +571,24 @@ public record CloseableMcpSyncClients(List clients) implements Au
 		public void close() {
 			this.clients.forEach(McpSyncClient::close);
 		}
+
 	}
 
+	/**
+	 * Record class that implements {@link AutoCloseable} to ensure proper cleanup of MCP
+	 * async clients.
+	 *
+	 * 
+	 * This class is responsible for closing all MCP async clients when the application
+	 * context is closed, preventing resource leaks.
+	 */
 	public record CloseableMcpAsyncClients(List clients) implements AutoCloseable {
+
 		@Override
 		public void close() {
 			this.clients.forEach(McpAsyncClient::close);
 		}
+
 	}
 
 }
diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/McpToolCallbackAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/McpToolCallbackAutoConfiguration.java
index 89143c324c0..8272ace23ca 100644
--- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/McpToolCallbackAutoConfiguration.java
+++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/McpToolCallbackAutoConfiguration.java
@@ -60,9 +60,17 @@ public McpToolNamePrefixGenerator defaultMcpToolNamePrefixGenerator() {
 	 * 
 	 * These callbacks enable integration with Spring AI's tool execution framework,
 	 * allowing MCP tools to be used as part of AI interactions.
+	 *
+	 * 
+	 * IMPORTANT: This method receives the same list reference that is populated by
+	 * {@link McpClientAutoConfiguration.McpSyncClientInitializer} in its
+	 * {@code afterSingletonsInstantiated()} method. This ensures that when
+	 * {@code getToolCallbacks()} is called, even if it's called before full
+	 * initialization completes, it will eventually see the populated list.
 	 * @param syncClientsToolFilter list of {@link McpToolFilter}s for the sync client to
 	 * filter the discovered tools
-	 * @param syncMcpClients provider of MCP sync clients
+	 * @param syncMcpClients the MCP sync clients list (same reference as returned by
+	 * mcpSyncClients() bean method)
 	 * @param mcpToolNamePrefixGenerator the tool name prefix generator
 	 * @return list of tool callbacks for MCP integration
 	 */
@@ -70,15 +78,14 @@ public McpToolNamePrefixGenerator defaultMcpToolNamePrefixGenerator() {
 	@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
 			matchIfMissing = true)
 	public SyncMcpToolCallbackProvider mcpToolCallbacks(ObjectProvider syncClientsToolFilter,
-			ObjectProvider> syncMcpClients,
-			ObjectProvider mcpToolNamePrefixGenerator,
+			List syncMcpClients, ObjectProvider mcpToolNamePrefixGenerator,
 			ObjectProvider toolContextToMcpMetaConverter) {
 
-		List mcpClients = syncMcpClients.stream().flatMap(List::stream).toList();
-
+		// Use mcpClientsReference to share the list reference - it will be populated by
+		// SmartInitializingSingleton
 		return SyncMcpToolCallbackProvider.builder()
-			.mcpClients(mcpClients)
-			.toolFilter(syncClientsToolFilter.getIfUnique((() -> (McpSyncClient, tool) -> true)))
+			.mcpClientsReference(syncMcpClients)
+			.toolFilter(syncClientsToolFilter.getIfUnique((() -> (mcpClient, tool) -> true)))
 			.toolNamePrefixGenerator(
 					mcpToolNamePrefixGenerator.getIfUnique(() -> McpToolNamePrefixGenerator.noPrefix()))
 			.toolContextToMcpMetaConverter(
@@ -86,19 +93,34 @@ public SyncMcpToolCallbackProvider mcpToolCallbacks(ObjectProvider
+	 * IMPORTANT: This method receives the same list reference that is populated by
+	 * {@link McpClientAutoConfiguration.McpAsyncClientInitializer} in its
+	 * {@code afterSingletonsInstantiated()} method.
+	 * @param asyncClientsToolFilter tool filter for async clients
+	 * @param mcpClients the MCP async clients list (same reference as returned by
+	 * mcpAsyncClients() bean method)
+	 * @param toolNamePrefixGenerator the tool name prefix generator
+	 * @param toolContextToMcpMetaConverter converter for tool context to MCP metadata
+	 * @return async tool callback provider
+	 */
 	@Bean
 	@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
 	public AsyncMcpToolCallbackProvider mcpAsyncToolCallbacks(ObjectProvider asyncClientsToolFilter,
-			ObjectProvider> mcpClientsProvider,
-			ObjectProvider toolNamePrefixGenerator,
-			ObjectProvider toolContextToMcpMetaConverter) { // TODO
-		List mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
+			List mcpClients, ObjectProvider toolNamePrefixGenerator,
+			ObjectProvider toolContextToMcpMetaConverter) {
+
+		// Use mcpClientsReference to share the list reference - it will be populated by
+		// SmartInitializingSingleton
 		return AsyncMcpToolCallbackProvider.builder()
-			.toolFilter(asyncClientsToolFilter.getIfUnique(() -> (McpAsyncClient, tool) -> true))
+			.toolFilter(asyncClientsToolFilter.getIfUnique(() -> (mcpClient, tool) -> true))
 			.toolNamePrefixGenerator(toolNamePrefixGenerator.getIfUnique(() -> McpToolNamePrefixGenerator.noPrefix()))
 			.toolContextToMcpMetaConverter(
 					toolContextToMcpMetaConverter.getIfUnique(() -> ToolContextToMcpMetaConverter.defaultConverter()))
-			.mcpClients(mcpClients)
+			.mcpClientsReference(mcpClients)
 			.build();
 	}
 
diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientSpecificationFactoryAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientSpecificationFactoryAutoConfiguration.java
index 620028f0e63..b4e430db0ce 100644
--- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientSpecificationFactoryAutoConfiguration.java
+++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientSpecificationFactoryAutoConfiguration.java
@@ -51,13 +51,33 @@
 import org.springframework.context.annotation.Configuration;
 
 /**
+ * Auto-configuration for MCP client specification factory.
+ *
+ * 
+ * Note: This configuration is now obsolete and disabled by default.
+ * Specification creation has been moved to
+ * {@link org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration.McpSyncClientInitializer}
+ * and
+ * {@link org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration.McpAsyncClientInitializer}
+ * which use {@link org.springframework.beans.factory.SmartInitializingSingleton} to defer
+ * client creation until after all singleton beans have been initialized. This ensures
+ * that all beans with MCP-annotated methods are scanned before specifications are
+ * created.
+ *
+ * 
+ * This class is kept for backwards compatibility but can be safely removed in future
+ * versions.
+ *
  * @author Christian Tzolov
  * @author Fu Jian
+ * @deprecated Since 1.1.0, specifications are now created dynamically after all singleton
+ * beans are initialized. This class will be removed in a future release.
  */
+@Deprecated(since = "1.1.0", forRemoval = true)
 @AutoConfiguration(after = McpClientAnnotationScannerAutoConfiguration.class)
 @ConditionalOnClass(McpLogging.class)
 @ConditionalOnProperty(prefix = McpClientAnnotationScannerProperties.CONFIG_PREFIX, name = "enabled",
-		havingValue = "true", matchIfMissing = true)
+		havingValue = "false") // Disabled by default - changed from "true" to "false"
 public class McpClientSpecificationFactoryAutoConfiguration {
 
 	@Configuration(proxyBeanMethods = false)
diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/McpClientAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/McpClientAutoConfigurationIT.java
index 1d1fbb92ae4..d0689d718d2 100644
--- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/McpClientAutoConfigurationIT.java
+++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/McpClientAutoConfigurationIT.java
@@ -88,28 +88,39 @@ public class McpClientAutoConfigurationIT {
 			AutoConfigurations.of(McpToolCallbackAutoConfiguration.class, McpClientAutoConfiguration.class));
 
 	/**
-	 * Tests the default MCP client auto-configuration.
-	 *
-	 * Note: We use 'spring.ai.mcp.client.initialized=false' to prevent the
-	 * auto-configuration from calling client.initialize() explicitly, which would cause a
-	 * 20-second timeout waiting for real MCP protocol communication. This allows us to
-	 * test bean creation and auto-configuration behavior without requiring a full MCP
-	 * server connection.
+	 * Tests that MCP clients are created after all singleton beans have been initialized,
+	 * verifying the SmartInitializingSingleton timing behavior.
+	 * 
+	 * This test uses a LateInitBean that records its initialization timestamp, and then
+	 * verifies that the MCP client initializer was called AFTER the late bean was
+	 * constructed. This proves that
+	 * SmartInitializingSingleton.afterSingletonsInstantiated() is called after all
+	 * singleton beans (including late-initializing ones) have been fully created.
 	 */
 	@Test
-	void defaultConfiguration() {
-		this.contextRunner.withUserConfiguration(TestTransportConfiguration.class)
+	void clientsCreatedAfterAllSingletons() {
+		this.contextRunner.withUserConfiguration(TestTransportConfiguration.class, LateInitBeanWithTimestamp.class)
 			.withPropertyValues("spring.ai.mcp.client.initialized=false")
 			.run(context -> {
+				// Get the late-init bean and its construction timestamp
+				LateInitBeanWithTimestamp lateBean = context.getBean(LateInitBeanWithTimestamp.class);
+				long lateBeanTimestamp = lateBean.getInitTimestamp();
+
+				// Get the initializer and its execution timestamp
+				var initializer = context.getBean(McpClientAutoConfiguration.McpSyncClientInitializer.class);
+				long initializerTimestamp = initializer.getInitializationTimestamp();
+
+				// Verify clients were created
 				List clients = context.getBean("mcpSyncClients", List.class);
-				assertThat(clients).hasSize(1);
+				assertThat(clients).isNotNull();
 
-				McpClientCommonProperties properties = context.getBean(McpClientCommonProperties.class);
-				assertThat(properties.getName()).isEqualTo("spring-ai-mcp-client");
-				assertThat(properties.getVersion()).isEqualTo("1.0.0");
-				assertThat(properties.getType()).isEqualTo(McpClientCommonProperties.ClientType.SYNC);
-				assertThat(properties.getRequestTimeout()).isEqualTo(Duration.ofSeconds(20));
-				assertThat(properties.isInitialized()).isFalse();
+				// THE KEY ASSERTION: Initializer must have been called AFTER late bean
+				// was constructed
+				// This proves SmartInitializingSingleton.afterSingletonsInstantiated()
+				// timing
+				assertThat(initializerTimestamp)
+					.as("MCP client initializer should be called AFTER all singleton beans are initialized")
+					.isGreaterThan(lateBeanTimestamp);
 			});
 	}
 
@@ -224,6 +235,54 @@ void closeableWrappersCreation() {
 				.hasSingleBean(McpClientAutoConfiguration.CloseableMcpSyncClients.class));
 	}
 
+	/**
+	 * Tests that SmartInitializingSingleton initializers are created and function
+	 * correctly for sync clients.
+	 */
+	@Test
+	void smartInitializingSingletonBehavior() {
+		this.contextRunner.withUserConfiguration(TestTransportConfiguration.class)
+			.withPropertyValues("spring.ai.mcp.client.initialized=false")
+			.run(context -> {
+				// Verify that McpSyncClientInitializer bean exists
+				assertThat(context).hasBean("mcpSyncClientInitializer");
+				assertThat(context.getBean("mcpSyncClientInitializer"))
+					.isInstanceOf(McpClientAutoConfiguration.McpSyncClientInitializer.class);
+
+				// Verify that clients list exists and was created by initializer
+				List clients = context.getBean("mcpSyncClients", List.class);
+				assertThat(clients).isNotNull();
+
+				// Verify the initializer has completed
+				var initializer = context.getBean(McpClientAutoConfiguration.McpSyncClientInitializer.class);
+				assertThat(initializer.getClients()).isSameAs(clients);
+			});
+	}
+
+	/**
+	 * Tests that SmartInitializingSingleton initializers are created and function
+	 * correctly for async clients.
+	 */
+	@Test
+	void smartInitializingSingletonForAsyncClients() {
+		this.contextRunner.withUserConfiguration(TestTransportConfiguration.class)
+			.withPropertyValues("spring.ai.mcp.client.type=ASYNC", "spring.ai.mcp.client.initialized=false")
+			.run(context -> {
+				// Verify that McpAsyncClientInitializer bean exists
+				assertThat(context).hasBean("mcpAsyncClientInitializer");
+				assertThat(context.getBean("mcpAsyncClientInitializer"))
+					.isInstanceOf(McpClientAutoConfiguration.McpAsyncClientInitializer.class);
+
+				// Verify that clients list exists and was created by initializer
+				List clients = context.getBean("mcpAsyncClients", List.class);
+				assertThat(clients).isNotNull();
+
+				// Verify the initializer has completed
+				var initializer = context.getBean(McpClientAutoConfiguration.McpAsyncClientInitializer.class);
+				assertThat(initializer.getClients()).isSameAs(clients);
+			});
+	}
+
 	@Configuration
 	static class TestTransportConfiguration {
 
@@ -265,6 +324,55 @@ McpSyncClientCustomizer testCustomizer() {
 
 	}
 
+	@Configuration
+	static class LateInitBean {
+
+		private final boolean initialized;
+
+		LateInitBean() {
+			// Simulate late initialization
+			this.initialized = true;
+		}
+
+		@Bean
+		String lateInitBean() {
+			// This bean method ensures the configuration is instantiated
+			return "late-init-marker";
+		}
+
+		boolean isInitialized() {
+			return this.initialized;
+		}
+
+	}
+
+	/**
+	 * A configuration bean that records when it was initialized. Used to verify
+	 * SmartInitializingSingleton timing - that the MCP client initializer is called AFTER
+	 * all singleton beans (including this one) have been constructed.
+	 */
+	@Configuration
+	static class LateInitBeanWithTimestamp {
+
+		private final long initTimestamp;
+
+		LateInitBeanWithTimestamp() {
+			// Record when this bean was constructed
+			this.initTimestamp = System.nanoTime();
+		}
+
+		@Bean
+		String lateInitMarker() {
+			// This bean method ensures the configuration is instantiated
+			return "late-init-marker";
+		}
+
+		long getInitTimestamp() {
+			return this.initTimestamp;
+		}
+
+	}
+
 	static class CustomClientTransport implements McpClientTransport {
 
 		@Override
diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientListChangedAnnotationsScanningIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientListChangedAnnotationsScanningIT.java
index d00e3cc6b35..2b85a654c8d 100644
--- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientListChangedAnnotationsScanningIT.java
+++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientListChangedAnnotationsScanningIT.java
@@ -45,45 +45,42 @@
 public class McpClientListChangedAnnotationsScanningIT {
 
 	private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
-		.withConfiguration(AutoConfigurations.of(McpClientAnnotationScannerAutoConfiguration.class,
-				McpClientSpecificationFactoryAutoConfiguration.class));
+		.withConfiguration(AutoConfigurations.of(McpClientAnnotationScannerAutoConfiguration.class));
 
 	@ParameterizedTest
 	@ValueSource(strings = { "SYNC", "ASYNC" })
 	void shouldScanAllThreeListChangedAnnotations(String clientType) {
-		String prefix = clientType.toLowerCase();
-
 		this.contextRunner.withUserConfiguration(AllListChangedConfiguration.class)
 			.withPropertyValues("spring.ai.mcp.client.type=" + clientType)
 			.run(context -> {
-				// Verify all three annotations were scanned
+				// Verify all three annotations were scanned and registered
 				McpClientAnnotationScannerAutoConfiguration.ClientMcpAnnotatedBeans annotatedBeans = context
 					.getBean(McpClientAnnotationScannerAutoConfiguration.ClientMcpAnnotatedBeans.class);
 				assertThat(annotatedBeans.getBeansByAnnotation(McpToolListChanged.class)).hasSize(1);
 				assertThat(annotatedBeans.getBeansByAnnotation(McpResourceListChanged.class)).hasSize(1);
 				assertThat(annotatedBeans.getBeansByAnnotation(McpPromptListChanged.class)).hasSize(1);
 
-				// Verify all three specification beans were created
-				assertThat(context).hasBean(prefix + "ToolListChangedSpecs");
-				assertThat(context).hasBean(prefix + "ResourceListChangedSpecs");
-				assertThat(context).hasBean(prefix + "PromptListChangedSpecs");
+				// Verify the annotation scanner configuration is present
+				assertThat(context).hasSingleBean(McpClientAnnotationScannerAutoConfiguration.class);
+
+				// Note: Specification beans are no longer created as separate beans.
+				// They are now created dynamically in McpClientAutoConfiguration
+				// initializers
+				// after all singleton beans have been instantiated.
 			});
 	}
 
 	@ParameterizedTest
 	@ValueSource(strings = { "SYNC", "ASYNC" })
 	void shouldNotScanAnnotationsWhenScannerDisabled(String clientType) {
-		String prefix = clientType.toLowerCase();
-
 		this.contextRunner.withUserConfiguration(AllListChangedConfiguration.class)
 			.withPropertyValues("spring.ai.mcp.client.type=" + clientType,
 					"spring.ai.mcp.client.annotation-scanner.enabled=false")
 			.run(context -> {
-				// Verify scanner beans were not created
+				// Verify scanner configuration was not created when disabled
 				assertThat(context).doesNotHaveBean(McpClientAnnotationScannerAutoConfiguration.class);
-				assertThat(context).doesNotHaveBean(prefix + "ToolListChangedSpecs");
-				assertThat(context).doesNotHaveBean(prefix + "ResourceListChangedSpecs");
-				assertThat(context).doesNotHaveBean(prefix + "PromptListChangedSpecs");
+				assertThat(context)
+					.doesNotHaveBean(McpClientAnnotationScannerAutoConfiguration.ClientMcpAnnotatedBeans.class);
 			});
 	}
 
diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/autoconfigure/StreamableMcpAnnotationsWithLLMIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/autoconfigure/StreamableMcpAnnotationsWithLLMIT.java
new file mode 100644
index 00000000000..c0628a6c098
--- /dev/null
+++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/autoconfigure/StreamableMcpAnnotationsWithLLMIT.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright 2025-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.mcp.server.autoconfigure;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import io.modelcontextprotocol.server.McpSyncServer;
+import io.modelcontextprotocol.server.transport.WebFluxStreamableServerTransportProvider;
+import io.modelcontextprotocol.spec.McpSchema;
+import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
+import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
+import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
+import io.modelcontextprotocol.spec.McpSchema.ProgressNotification;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springaicommunity.mcp.annotation.McpElicitation;
+import org.springaicommunity.mcp.annotation.McpLogging;
+import org.springaicommunity.mcp.annotation.McpProgress;
+import org.springaicommunity.mcp.annotation.McpSampling;
+import org.springaicommunity.mcp.annotation.McpTool;
+import org.springaicommunity.mcp.annotation.McpToolParam;
+import org.springaicommunity.mcp.context.McpSyncRequestContext;
+import org.springaicommunity.mcp.context.StructuredElicitResult;
+import reactor.netty.DisposableServer;
+import reactor.netty.http.server.HttpServer;
+
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration;
+import org.springframework.ai.mcp.client.common.autoconfigure.McpToolCallbackAutoConfiguration;
+import org.springframework.ai.mcp.client.common.autoconfigure.annotations.McpClientAnnotationScannerAutoConfiguration;
+import org.springframework.ai.mcp.client.common.autoconfigure.annotations.McpClientSpecificationFactoryAutoConfiguration;
+import org.springframework.ai.mcp.client.webflux.autoconfigure.StreamableHttpWebFluxTransportAutoConfiguration;
+import org.springframework.ai.mcp.server.common.autoconfigure.McpServerAutoConfiguration;
+import org.springframework.ai.mcp.server.common.autoconfigure.ToolCallbackConverterAutoConfiguration;
+import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerAnnotationScannerAutoConfiguration;
+import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerSpecificationFactoryAutoConfiguration;
+import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerProperties;
+import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerStreamableHttpProperties;
+import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration;
+import org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration;
+import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration;
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.tool.ToolCallbackProvider;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.server.reactive.HttpHandler;
+import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
+import org.springframework.test.util.TestSocketUtils;
+import org.springframework.web.reactive.function.server.RouterFunction;
+import org.springframework.web.reactive.function.server.RouterFunctions;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".*")
+public class StreamableMcpAnnotationsWithLLMIT {
+
+	private final ApplicationContextRunner serverContextRunner = new ApplicationContextRunner()
+		.withPropertyValues("spring.ai.mcp.server.protocol=STREAMABLE")
+		.withConfiguration(AutoConfigurations.of(McpServerAutoConfiguration.class,
+				ToolCallbackConverterAutoConfiguration.class, McpServerStreamableHttpWebFluxAutoConfiguration.class,
+				McpServerAnnotationScannerAutoConfiguration.class,
+				McpServerSpecificationFactoryAutoConfiguration.class));
+
+	private final ApplicationContextRunner clientApplicationContext = new ApplicationContextRunner()
+		.withPropertyValues("spring.ai.anthropic.apiKey=" + System.getenv("ANTHROPIC_API_KEY"))
+		.withConfiguration(anthropicAutoConfig(McpToolCallbackAutoConfiguration.class, McpClientAutoConfiguration.class,
+				StreamableHttpWebFluxTransportAutoConfiguration.class,
+				McpClientAnnotationScannerAutoConfiguration.class, McpClientSpecificationFactoryAutoConfiguration.class,
+				AnthropicChatAutoConfiguration.class, ChatClientAutoConfiguration.class));
+
+	private static AutoConfigurations anthropicAutoConfig(Class>... additional) {
+		Class>[] dependencies = { SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class,
+				RestClientAutoConfiguration.class, WebClientAutoConfiguration.class };
+		Class>[] all = Stream.concat(Arrays.stream(dependencies), Arrays.stream(additional)).toArray(Class>[]::new);
+		return AutoConfigurations.of(all);
+	}
+
+	private static AtomicInteger toolCouter = new AtomicInteger(0);
+
+	@Test
+	void clientServerCapabilities() {
+
+		int serverPort = TestSocketUtils.findAvailableTcpPort();
+
+		this.serverContextRunner.withUserConfiguration(TestMcpServerConfiguration.class)
+			.withPropertyValues(// @formatter:off
+				"spring.ai.mcp.server.name=test-mcp-server",
+				"spring.ai.mcp.server.version=1.0.0",
+				"spring.ai.mcp.server.streamable-http.keep-alive-interval=1s",
+				"spring.ai.mcp.server.streamable-http.mcp-endpoint=/mcp") // @formatter:on
+			.run(serverContext -> {
+				// Verify all required beans are present
+				assertThat(serverContext).hasSingleBean(WebFluxStreamableServerTransportProvider.class);
+				assertThat(serverContext).hasSingleBean(RouterFunction.class);
+				assertThat(serverContext).hasSingleBean(McpSyncServer.class);
+
+				// Verify server properties are configured correctly
+				McpServerProperties properties = serverContext.getBean(McpServerProperties.class);
+				assertThat(properties.getName()).isEqualTo("test-mcp-server");
+				assertThat(properties.getVersion()).isEqualTo("1.0.0");
+
+				McpServerStreamableHttpProperties streamableHttpProperties = serverContext
+					.getBean(McpServerStreamableHttpProperties.class);
+				assertThat(streamableHttpProperties.getMcpEndpoint()).isEqualTo("/mcp");
+				assertThat(streamableHttpProperties.getKeepAliveInterval()).isEqualTo(Duration.ofSeconds(1));
+
+				var httpServer = startHttpServer(serverContext, serverPort);
+
+				this.clientApplicationContext.withUserConfiguration(TestMcpClientConfiguration.class)
+					.withPropertyValues(// @formatter:off
+						"spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:" + serverPort,
+						"spring.ai.mcp.client.initialized=false") // @formatter:on
+					.run(clientContext -> {
+
+						ChatClient.Builder builder = clientContext.getBean(ChatClient.Builder.class);
+
+						ToolCallbackProvider tcp = clientContext.getBean(ToolCallbackProvider.class);
+
+						assertThat(builder).isNotNull();
+
+						ChatClient chatClient = builder.defaultToolCallbacks(tcp)
+							.defaultToolContext(Map.of("progressToken", "test-progress-token"))
+							.build();
+
+						String cResponse = chatClient.prompt()
+							.user("What is the weather in Amsterdam right now")
+							.call()
+							.content();
+
+						assertThat(cResponse).isNotEmpty();
+						assertThat(cResponse).contains("22");
+
+						assertThat(toolCouter.get()).isEqualTo(1);
+
+						// PROGRESS
+						TestMcpClientConfiguration.TestContext testContext = clientContext
+							.getBean(TestMcpClientConfiguration.TestContext.class);
+						assertThat(testContext.progressLatch.await(5, TimeUnit.SECONDS))
+							.as("Should receive progress notifications in reasonable time")
+							.isTrue();
+						assertThat(testContext.progressNotifications).hasSize(3);
+
+						Map notificationMap = testContext.progressNotifications
+							.stream()
+							.collect(Collectors.toMap(n -> n.message(), n -> n));
+
+						// First notification should be 0.0/1.0 progress
+						assertThat(notificationMap.get("tool call start").progressToken())
+							.isEqualTo("test-progress-token");
+						assertThat(notificationMap.get("tool call start").progress()).isEqualTo(0.0);
+						assertThat(notificationMap.get("tool call start").total()).isEqualTo(1.0);
+						assertThat(notificationMap.get("tool call start").message()).isEqualTo("tool call start");
+
+						// Second notification should be 1.0/1.0 progress
+						assertThat(notificationMap.get("elicitation completed").progressToken())
+							.isEqualTo("test-progress-token");
+						assertThat(notificationMap.get("elicitation completed").progress()).isEqualTo(0.5);
+						assertThat(notificationMap.get("elicitation completed").total()).isEqualTo(1.0);
+						assertThat(notificationMap.get("elicitation completed").message())
+							.isEqualTo("elicitation completed");
+
+						// Third notification should be 0.5/1.0 progress
+						assertThat(notificationMap.get("sampling completed").progressToken())
+							.isEqualTo("test-progress-token");
+						assertThat(notificationMap.get("sampling completed").progress()).isEqualTo(1.0);
+						assertThat(notificationMap.get("sampling completed").total()).isEqualTo(1.0);
+						assertThat(notificationMap.get("sampling completed").message()).isEqualTo("sampling completed");
+
+					});
+
+				stopHttpServer(httpServer);
+			});
+	}
+
+	// Helper methods to start and stop the HTTP server
+	private static DisposableServer startHttpServer(ApplicationContext serverContext, int port) {
+		WebFluxStreamableServerTransportProvider mcpStreamableServerTransport = serverContext
+			.getBean(WebFluxStreamableServerTransportProvider.class);
+		HttpHandler httpHandler = RouterFunctions.toHttpHandler(mcpStreamableServerTransport.getRouterFunction());
+		ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
+		return HttpServer.create().port(port).handle(adapter).bindNow();
+	}
+
+	private static void stopHttpServer(DisposableServer server) {
+		if (server != null) {
+			server.disposeNow();
+		}
+	}
+
+	record ElicitInput(String message) {
+	}
+
+	public static class TestMcpServerConfiguration {
+
+		@Bean
+		public McpServerHandlers serverSideSpecProviders() {
+			return new McpServerHandlers();
+		}
+
+		public static class McpServerHandlers {
+
+			@McpTool(description = "Provides weather information by city name")
+			public String weather(McpSyncRequestContext ctx, @McpToolParam String cityName) {
+
+				toolCouter.incrementAndGet();
+
+				ctx.info("Weather called!");
+
+				ctx.progress(p -> p.progress(0.0).total(1.0).message("tool call start"));
+
+				ctx.ping(); // call client ping
+
+				// call elicitation
+				var elicitationResult = ctx.elicit(e -> e.message("Test message"), ElicitInput.class);
+
+				ctx.progress(p -> p.progress(0.50).total(1.0).message("elicitation completed"));
+
+				// call sampling
+				CreateMessageResult samplingResponse = ctx.sample(s -> s.message("Test Sampling Message")
+					.modelPreferences(pref -> pref.modelHints("OpenAi", "Ollama")
+						.costPriority(1.0)
+						.speedPriority(1.0)
+						.intelligencePriority(1.0)));
+
+				ctx.progress(p -> p.progress(1.0).total(1.0).message("sampling completed"));
+
+				ctx.info("Tool1 Done!");
+
+				return "Weahter is 22C with rain " + samplingResponse.toString() + ", " + elicitationResult.toString();
+			}
+
+		}
+
+	}
+
+	public static class TestMcpClientConfiguration {
+
+		@Bean
+		public TestContext testContext() {
+			return new TestContext();
+		}
+
+		@Bean
+		public TestMcpClientHandlers mcpClientHandlers(TestContext testContext) {
+			return new TestMcpClientHandlers(testContext);
+		}
+
+		public static class TestContext {
+
+			final AtomicReference loggingNotificationRef = new AtomicReference<>();
+
+			final CountDownLatch progressLatch = new CountDownLatch(3);
+
+			final List progressNotifications = new CopyOnWriteArrayList<>();
+
+		}
+
+		public static class TestMcpClientHandlers {
+
+			private static final Logger logger = LoggerFactory.getLogger(TestMcpClientHandlers.class);
+
+			private TestContext testContext;
+
+			public TestMcpClientHandlers(TestContext testContext) {
+				this.testContext = testContext;
+			}
+
+			@McpProgress(clients = "server1")
+			public void progressHandler(ProgressNotification progressNotification) {
+				logger.info("MCP PROGRESS: [{}] progress: {} total: {} message: {}",
+						progressNotification.progressToken(), progressNotification.progress(),
+						progressNotification.total(), progressNotification.message());
+				this.testContext.progressNotifications.add(progressNotification);
+				this.testContext.progressLatch.countDown();
+			}
+
+			@McpLogging(clients = "server1")
+			public void loggingHandler(LoggingMessageNotification loggingMessage) {
+				this.testContext.loggingNotificationRef.set(loggingMessage);
+				logger.info("MCP LOGGING: [{}] {}", loggingMessage.level(), loggingMessage.data());
+			}
+
+			@McpSampling(clients = "server1")
+			public CreateMessageResult samplingHandler(CreateMessageRequest llmRequest) {
+				logger.info("MCP SAMPLING: {}", llmRequest);
+
+				String userPrompt = ((McpSchema.TextContent) llmRequest.messages().get(0).content()).text();
+				String modelHint = llmRequest.modelPreferences().hints().get(0).name();
+
+				return CreateMessageResult.builder()
+					.content(new McpSchema.TextContent("Response " + userPrompt + " with model hint " + modelHint))
+					.build();
+			}
+
+			@McpElicitation(clients = "server1")
+			public StructuredElicitResult elicitationHandler(McpSchema.ElicitRequest request) {
+				logger.info("MCP ELICITATION: {}", request);
+				ElicitInput elicitData = new ElicitInput(request.message());
+				return StructuredElicitResult.builder().structuredContent(elicitData).build();
+			}
+
+		}
+
+	}
+
+}
diff --git a/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java b/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java
index a7dfbc74f40..16bc1318400 100644
--- a/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java
+++ b/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java
@@ -66,7 +66,11 @@ ToolCallbackResolver toolCallbackResolver(GenericApplicationContext applicationC
 			List toolCallbacks, List tcbProviders) {
 
 		List allFunctionAndToolCallbacks = new ArrayList<>(toolCallbacks);
-		tcbProviders.stream().map(pr -> List.of(pr.getToolCallbacks())).forEach(allFunctionAndToolCallbacks::addAll);
+		tcbProviders.stream()
+			.filter(pr -> !pr.getClass().getSimpleName().equals("SyncMcpToolCallbackProvider"))
+			.filter(pr -> !pr.getClass().getSimpleName().equals("AsyncMcpToolCallbackProvider"))
+			.map(pr -> List.of(pr.getToolCallbacks()))
+			.forEach(allFunctionAndToolCallbacks::addAll);
 
 		var staticToolCallbackResolver = new StaticToolCallbackResolver(allFunctionAndToolCallbacks);
 
diff --git a/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/test/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfigurationTests.java b/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/test/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfigurationTests.java
index af42d744158..7a2ff8da64f 100644
--- a/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/test/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfigurationTests.java
+++ b/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/test/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfigurationTests.java
@@ -185,6 +185,43 @@ void throwExceptionOnErrorEnabled() {
 			});
 	}
 
+	@Test
+	void mcpToolCallbackProvidersAreFilteredOut() {
+		new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(ToolCallingAutoConfiguration.class))
+			.withUserConfiguration(ConfigWithMcpProviders.class)
+			.run(context -> {
+				var toolCallbackResolver = context.getBean(ToolCallbackResolver.class);
+				assertThat(toolCallbackResolver).isInstanceOf(DelegatingToolCallbackResolver.class);
+
+				// Regular ToolCallbackProvider should be resolved
+				assertThat(toolCallbackResolver.resolve("regularTool")).isNotNull();
+				assertThat(toolCallbackResolver.resolve("regularTool").getToolDefinition().name())
+					.isEqualTo("regularTool");
+
+				// MCP tools should NOT be resolved (filtered out from static resolver)
+				// They will be resolved lazily through ChatClient
+				assertThat(toolCallbackResolver.resolve("syncMcpTool")).isNull();
+				assertThat(toolCallbackResolver.resolve("asyncMcpTool")).isNull();
+			});
+	}
+
+	@Test
+	void nonMcpToolCallbackProvidersAreIncluded() {
+		new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(ToolCallingAutoConfiguration.class))
+			.withUserConfiguration(ConfigWithRegularProvider.class)
+			.run(context -> {
+				var toolCallbackResolver = context.getBean(ToolCallbackResolver.class);
+				assertThat(toolCallbackResolver).isInstanceOf(DelegatingToolCallbackResolver.class);
+
+				// All tools from regular ToolCallbackProvider should be resolved
+				assertThat(toolCallbackResolver.resolve("tool1")).isNotNull();
+				assertThat(toolCallbackResolver.resolve("tool1").getToolDefinition().name()).isEqualTo("tool1");
+
+				assertThat(toolCallbackResolver.resolve("tool2")).isNotNull();
+				assertThat(toolCallbackResolver.resolve("tool2").getToolDefinition().name()).isEqualTo("tool2");
+			});
+	}
+
 	static class WeatherService {
 
 		@Tool(description = "Get the weather in location. Return temperature in 36°F or 36°C format.")
@@ -275,4 +312,77 @@ public record Response(String temperature) {
 
 	}
 
+	@Configuration
+	static class ConfigWithMcpProviders {
+
+		@Bean
+		public ToolCallbackProvider regularProvider() {
+			return new StaticToolCallbackProvider(FunctionToolCallback.builder("regularTool", request -> "OK")
+				.description("Regular tool")
+				.inputType(Request.class)
+				.build());
+		}
+
+		@Bean
+		public ToolCallbackProvider syncMcpProvider() {
+			return new SyncMcpToolCallbackProvider();
+		}
+
+		@Bean
+		public ToolCallbackProvider asyncMcpProvider() {
+			return new AsyncMcpToolCallbackProvider();
+		}
+
+		public record Request(String input) {
+		}
+
+	}
+
+	@Configuration
+	static class ConfigWithRegularProvider {
+
+		@Bean
+		public ToolCallbackProvider multiToolProvider() {
+			return new StaticToolCallbackProvider(
+					FunctionToolCallback.builder("tool1", request -> "Result 1")
+						.description("Tool 1")
+						.inputType(Request.class)
+						.build(),
+					FunctionToolCallback.builder("tool2", request -> "Result 2")
+						.description("Tool 2")
+						.inputType(Request.class)
+						.build());
+		}
+
+		public record Request(String input) {
+		}
+
+	}
+
+	// Mock classes that simulate MCP providers - must match exact names that filter
+	// checks
+	static class SyncMcpToolCallbackProvider implements ToolCallbackProvider {
+
+		@Override
+		public ToolCallback[] getToolCallbacks() {
+			return new ToolCallback[] { FunctionToolCallback.builder("syncMcpTool", request -> "Sync")
+				.description("Sync MCP tool")
+				.inputType(Config.Request.class)
+				.build() };
+		}
+
+	}
+
+	static class AsyncMcpToolCallbackProvider implements ToolCallbackProvider {
+
+		@Override
+		public ToolCallback[] getToolCallbacks() {
+			return new ToolCallback[] { FunctionToolCallback.builder("asyncMcpTool", request -> "Async")
+				.description("Async MCP tool")
+				.inputType(Config.Request.class)
+				.build() };
+		}
+
+	}
+
 }
diff --git a/mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallbackProvider.java b/mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallbackProvider.java
index 6e67275aa78..a94fd680afd 100644
--- a/mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallbackProvider.java
+++ b/mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallbackProvider.java
@@ -223,7 +223,25 @@ public Builder toolFilter(McpToolFilter toolFilter) {
 		}
 
 		/**
-		 * Sets MCP clients.
+		 * Sets MCP clients by reference - the list reference will be shared.
+		 * 
+		 * Use this method when the list will be populated later (e.g., by
+		 * {@code SmartInitializingSingleton}). The provider will see any clients added to
+		 * the list after construction.
+		 * @param mcpClients list of MCP clients (reference will be stored)
+		 * @return this builder
+		 */
+		public Builder mcpClientsReference(List mcpClients) {
+			Assert.notNull(mcpClients, "MCP clients list must not be null");
+			this.mcpClients = mcpClients;
+			return this;
+		}
+
+		/**
+		 * Sets MCP clients for tool discovery (stores reference directly).
+		 * 
+		 * Note: Unlike the sync version, this method does not create a defensive copy.
+		 * Use {@link #mcpClientsReference(List)} for clarity when sharing references.
 		 * @param mcpClients list of MCP clients
 		 * @return this builder
 		 */
@@ -237,7 +255,9 @@ public Builder mcpClients(List mcpClients) {
 		 * Sets MCP clients.
 		 * @param mcpClients MCP clients as varargs
 		 * @return this builder
+		 * @deprecated Plese use the mcpClientsReference instead!
 		 */
+		@Deprecated
 		public Builder mcpClients(McpAsyncClient... mcpClients) {
 			Assert.notNull(mcpClients, "MCP clients must not be null");
 			this.mcpClients = List.of(mcpClients);
diff --git a/mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallbackProvider.java b/mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallbackProvider.java
index 298f07596be..e423eb8101b 100644
--- a/mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallbackProvider.java
+++ b/mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallbackProvider.java
@@ -191,10 +191,30 @@ public static final class Builder {
 			.defaultConverter();
 
 		/**
-		 * Sets MCP clients for tool discovery (replaces existing).
+		 * Sets MCP clients by reference - the list reference will be shared.
+		 * 
+		 * Use this method when the list will be populated later (e.g., by
+		 * {@code SmartInitializingSingleton}). The provider will see any clients added to
+		 * the list after construction.
+		 * @param mcpClients list of MCP clients (reference will be stored)
+		 * @return this builder
+		 */
+		public Builder mcpClientsReference(List mcpClients) {
+			Assert.notNull(mcpClients, "MCP clients list must not be null");
+			this.mcpClients = mcpClients;
+			return this;
+		}
+
+		/**
+		 * Sets MCP clients for tool discovery (creates defensive copy).
+		 * 
+		 * Use this method when passing a fully populated, immutable list. A defensive
+		 * copy will be created to prevent external modifications.
 		 * @param mcpClients list of MCP clients
 		 * @return this builder
+		 * @deprecated Plese use the mcpClientsReference instead!
 		 */
+		@Deprecated
 		public Builder mcpClients(List mcpClients) {
 			Assert.notNull(mcpClients, "MCP clients list must not be null");
 			this.mcpClients = new ArrayList<>(mcpClients);
@@ -205,7 +225,9 @@ public Builder mcpClients(List mcpClients) {
 		 * Sets MCP clients for tool discovery (replaces existing).
 		 * @param mcpClients MCP clients array
 		 * @return this builder
+		 * @deprecated Plese use the mcpClientsReference instead!
 		 */
+		@Deprecated
 		public Builder mcpClients(McpSyncClient... mcpClients) {
 			Assert.notNull(mcpClients, "MCP clients array must not be null");
 			this.mcpClients = new java.util.ArrayList<>(List.of(mcpClients));
diff --git a/mcp/common/src/test/java/org/springframework/ai/mcp/AsyncMcpToolCallbackProviderListReferenceTest.java b/mcp/common/src/test/java/org/springframework/ai/mcp/AsyncMcpToolCallbackProviderListReferenceTest.java
new file mode 100644
index 00000000000..7e5f2996786
--- /dev/null
+++ b/mcp/common/src/test/java/org/springframework/ai/mcp/AsyncMcpToolCallbackProviderListReferenceTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2025-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.mcp;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.modelcontextprotocol.client.McpAsyncClient;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Mono;
+
+import org.springframework.ai.tool.ToolCallback;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests that {@link AsyncMcpToolCallbackProvider} correctly maintains list references
+ * when using {@code mcpClientsReference()}.
+ *
+ * @author Christian Tzolov
+ */
+class AsyncMcpToolCallbackProviderListReferenceTest {
+
+	@Test
+	void testMcpClientsReferenceSharesList() {
+		// Create an empty list that will be populated later
+		List clientsList = new ArrayList<>();
+
+		// Create provider with reference to empty list
+		AsyncMcpToolCallbackProvider provider = AsyncMcpToolCallbackProvider.builder()
+			.mcpClientsReference(clientsList)
+			.build();
+
+		// Initially, no tool callbacks should be available
+		ToolCallback[] callbacks = provider.getToolCallbacks();
+		assertThat(callbacks).isEmpty();
+
+		// Now simulate SmartInitializingSingleton populating the list
+		McpAsyncClient mockClient = mock(McpAsyncClient.class);
+		McpSchema.Tool mockTool = mock(McpSchema.Tool.class);
+		when(mockTool.name()).thenReturn("test_tool");
+		when(mockTool.description()).thenReturn("Test tool");
+
+		McpSchema.ListToolsResult toolsResult = mock(McpSchema.ListToolsResult.class);
+		when(toolsResult.tools()).thenReturn(List.of(mockTool));
+		when(mockClient.listTools()).thenReturn(Mono.just(toolsResult));
+
+		// Mock connection info
+		when(mockClient.getClientCapabilities()).thenReturn(mock(McpSchema.ClientCapabilities.class));
+		when(mockClient.getClientInfo()).thenReturn(mock(McpSchema.Implementation.class));
+		when(mockClient.getCurrentInitializationResult()).thenReturn(mock(McpSchema.InitializeResult.class));
+
+		clientsList.add(mockClient);
+
+		// Now the provider should see the client and return tool callbacks
+		callbacks = provider.getToolCallbacks();
+		assertThat(callbacks).hasSize(1);
+		assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("test_tool");
+	}
+
+	@Test
+	void testMcpClientsStoresReference() {
+		// Create a list with a client
+		McpAsyncClient mockClient = mock(McpAsyncClient.class);
+		McpSchema.Tool mockTool = mock(McpSchema.Tool.class);
+		when(mockTool.name()).thenReturn("test_tool");
+		when(mockTool.description()).thenReturn("Test tool");
+
+		McpSchema.ListToolsResult toolsResult = mock(McpSchema.ListToolsResult.class);
+		when(toolsResult.tools()).thenReturn(List.of(mockTool));
+		when(mockClient.listTools()).thenReturn(Mono.just(toolsResult));
+
+		// Mock connection info
+		when(mockClient.getClientCapabilities()).thenReturn(mock(McpSchema.ClientCapabilities.class));
+		when(mockClient.getClientInfo()).thenReturn(mock(McpSchema.Implementation.class));
+		when(mockClient.getCurrentInitializationResult()).thenReturn(mock(McpSchema.InitializeResult.class));
+
+		List clientsList = new ArrayList<>();
+		clientsList.add(mockClient);
+
+		// Create provider - async version stores reference (no defensive copy)
+		AsyncMcpToolCallbackProvider provider = AsyncMcpToolCallbackProvider.builder().mcpClients(clientsList).build();
+
+		// Provider should see the initial client
+		ToolCallback[] callbacks = provider.getToolCallbacks();
+		assertThat(callbacks).hasSize(1);
+
+		// Add another client to the original list
+		McpAsyncClient mockClient2 = mock(McpAsyncClient.class);
+		McpSchema.Tool mockTool2 = mock(McpSchema.Tool.class);
+		when(mockTool2.name()).thenReturn("test_tool_2");
+		when(mockTool2.description()).thenReturn("Test tool 2");
+
+		McpSchema.ListToolsResult toolsResult2 = mock(McpSchema.ListToolsResult.class);
+		when(toolsResult2.tools()).thenReturn(List.of(mockTool2));
+		when(mockClient2.listTools()).thenReturn(Mono.just(toolsResult2));
+
+		when(mockClient2.getClientCapabilities()).thenReturn(mock(McpSchema.ClientCapabilities.class));
+		when(mockClient2.getClientInfo()).thenReturn(mock(McpSchema.Implementation.class));
+		when(mockClient2.getCurrentInitializationResult()).thenReturn(mock(McpSchema.InitializeResult.class));
+
+		clientsList.add(mockClient2);
+
+		// Provider shares the reference, so should see both clients
+		callbacks = provider.getToolCallbacks();
+		assertThat(callbacks).hasSize(2);
+	}
+
+}
diff --git a/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackProviderListReferenceTest.java b/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackProviderListReferenceTest.java
new file mode 100644
index 00000000000..b5c3aae69a9
--- /dev/null
+++ b/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackProviderListReferenceTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2025-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.mcp;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.tool.ToolCallback;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests that {@link SyncMcpToolCallbackProvider} correctly maintains list references when
+ * using {@code mcpClientsReference()}.
+ *
+ * @author Christian Tzolov
+ */
+class SyncMcpToolCallbackProviderListReferenceTest {
+
+	@Test
+	void testMcpClientsReferenceSharesList() {
+		// Create an empty list that will be populated later
+		List clientsList = new ArrayList<>();
+
+		// Create provider with reference to empty list
+		SyncMcpToolCallbackProvider provider = SyncMcpToolCallbackProvider.builder()
+			.mcpClientsReference(clientsList)
+			.build();
+
+		// Initially, no tool callbacks should be available
+		ToolCallback[] callbacks = provider.getToolCallbacks();
+		assertThat(callbacks).isEmpty();
+
+		// Now simulate SmartInitializingSingleton populating the list
+		McpSyncClient mockClient = mock(McpSyncClient.class);
+		McpSchema.Tool mockTool = mock(McpSchema.Tool.class);
+		when(mockTool.name()).thenReturn("test_tool");
+		when(mockTool.description()).thenReturn("Test tool");
+
+		McpSchema.ListToolsResult toolsResult = mock(McpSchema.ListToolsResult.class);
+		when(toolsResult.tools()).thenReturn(List.of(mockTool));
+		when(mockClient.listTools()).thenReturn(toolsResult);
+
+		// Mock connection info
+		when(mockClient.getClientCapabilities()).thenReturn(mock(McpSchema.ClientCapabilities.class));
+		when(mockClient.getClientInfo()).thenReturn(mock(McpSchema.Implementation.class));
+		when(mockClient.getCurrentInitializationResult()).thenReturn(mock(McpSchema.InitializeResult.class));
+
+		clientsList.add(mockClient);
+
+		// Now the provider should see the client and return tool callbacks
+		callbacks = provider.getToolCallbacks();
+		assertThat(callbacks).hasSize(1);
+		assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("test_tool");
+	}
+
+	@Test
+	void testMcpClientsCreatesCopy() {
+		// Create a list with a client
+		McpSyncClient mockClient = mock(McpSyncClient.class);
+		McpSchema.Tool mockTool = mock(McpSchema.Tool.class);
+		when(mockTool.name()).thenReturn("test_tool");
+		when(mockTool.description()).thenReturn("Test tool");
+
+		McpSchema.ListToolsResult toolsResult = mock(McpSchema.ListToolsResult.class);
+		when(toolsResult.tools()).thenReturn(List.of(mockTool));
+		when(mockClient.listTools()).thenReturn(toolsResult);
+
+		// Mock connection info
+		when(mockClient.getClientCapabilities()).thenReturn(mock(McpSchema.ClientCapabilities.class));
+		when(mockClient.getClientInfo()).thenReturn(mock(McpSchema.Implementation.class));
+		when(mockClient.getCurrentInitializationResult()).thenReturn(mock(McpSchema.InitializeResult.class));
+
+		List clientsList = new ArrayList<>();
+		clientsList.add(mockClient);
+
+		// Create provider with defensive copy
+		SyncMcpToolCallbackProvider provider = SyncMcpToolCallbackProvider.builder().mcpClients(clientsList).build();
+
+		// Provider should see the initial client
+		ToolCallback[] callbacks = provider.getToolCallbacks();
+		assertThat(callbacks).hasSize(1);
+
+		// Clear the original list
+		clientsList.clear();
+
+		// Provider still has its copy, so should still return the tool
+		callbacks = provider.getToolCallbacks();
+		assertThat(callbacks).hasSize(1);
+	}
+
+}
diff --git a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java
index 20b207d5c5c..6b740c25821 100644
--- a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java
+++ b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java
@@ -618,6 +618,8 @@ public static class DefaultChatClientRequestSpec implements ChatClientRequestSpe
 
 		private final List toolCallbacks = new ArrayList<>();
 
+		private final List toolCallbackProviders = new ArrayList<>();
+
 		private final List messages = new ArrayList<>();
 
 		private final Map userParams = new HashMap<>();
@@ -648,16 +650,17 @@ public static class DefaultChatClientRequestSpec implements ChatClientRequestSpe
 		/* copy constructor */
 		DefaultChatClientRequestSpec(DefaultChatClientRequestSpec ccr) {
 			this(ccr.chatModel, ccr.userText, ccr.userParams, ccr.userMetadata, ccr.systemText, ccr.systemParams,
-					ccr.systemMetadata, ccr.toolCallbacks, ccr.messages, ccr.toolNames, ccr.media, ccr.chatOptions,
-					ccr.advisors, ccr.advisorParams, ccr.observationRegistry, ccr.observationConvention,
-					ccr.toolContext, ccr.templateRenderer);
+					ccr.systemMetadata, ccr.toolCallbacks, ccr.toolCallbackProviders, ccr.messages, ccr.toolNames,
+					ccr.media, ccr.chatOptions, ccr.advisors, ccr.advisorParams, ccr.observationRegistry,
+					ccr.observationConvention, ccr.toolContext, ccr.templateRenderer);
 		}
 
 		public DefaultChatClientRequestSpec(ChatModel chatModel, @Nullable String userText,
 				Map userParams, Map userMetadata, @Nullable String systemText,
 				Map systemParams, Map systemMetadata, List toolCallbacks,
-				List messages, List toolNames, List media, @Nullable ChatOptions chatOptions,
-				List advisors, Map advisorParams, ObservationRegistry observationRegistry,
+				List toolCallbackProviders, List messages, List toolNames,
+				List media, @Nullable ChatOptions chatOptions, List advisors,
+				Map advisorParams, ObservationRegistry observationRegistry,
 				@Nullable ChatClientObservationConvention observationConvention, Map toolContext,
 				@Nullable TemplateRenderer templateRenderer) {
 
@@ -667,6 +670,7 @@ public DefaultChatClientRequestSpec(ChatModel chatModel, @Nullable String userTe
 			Assert.notNull(systemParams, "systemParams cannot be null");
 			Assert.notNull(systemMetadata, "systemMetadata cannot be null");
 			Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
+			Assert.notNull(toolCallbackProviders, "toolCallbackProviders cannot be null");
 			Assert.notNull(messages, "messages cannot be null");
 			Assert.notNull(toolNames, "toolNames cannot be null");
 			Assert.notNull(media, "media cannot be null");
@@ -689,6 +693,7 @@ public DefaultChatClientRequestSpec(ChatModel chatModel, @Nullable String userTe
 
 			this.toolNames.addAll(toolNames);
 			this.toolCallbacks.addAll(toolCallbacks);
+			this.toolCallbackProviders.addAll(toolCallbackProviders);
 			this.messages.addAll(messages);
 			this.media.addAll(media);
 			this.advisors.addAll(advisors);
@@ -885,9 +890,10 @@ public ChatClientRequestSpec tools(Object... toolObjects) {
 		public ChatClientRequestSpec toolCallbacks(ToolCallbackProvider... toolCallbackProviders) {
 			Assert.notNull(toolCallbackProviders, "toolCallbackProviders cannot be null");
 			Assert.noNullElements(toolCallbackProviders, "toolCallbackProviders cannot contain null elements");
-			for (ToolCallbackProvider toolCallbackProvider : toolCallbackProviders) {
-				this.toolCallbacks.addAll(List.of(toolCallbackProvider.getToolCallbacks()));
-			}
+			// Store providers for lazy resolution - don't call getToolCallbacks() yet!
+			// This allows providers that depend on SmartInitializingSingleton to complete
+			// their initialization before tool callbacks are resolved.
+			this.toolCallbackProviders.addAll(Arrays.asList(toolCallbackProviders));
 			return this;
 		}
 
@@ -988,6 +994,8 @@ public ChatClientRequestSpec templateRenderer(TemplateRenderer templateRenderer)
 
 		@Override
 		public CallResponseSpec call() {
+			// Resolve tool callbacks lazily before building the request
+			resolveToolCallbacksBeforeExecution();
 			BaseAdvisorChain advisorChain = buildAdvisorChain();
 			return new DefaultCallResponseSpec(DefaultChatClientUtils.toChatClientRequest(this), advisorChain,
 					this.observationRegistry, this.observationConvention);
@@ -995,11 +1003,30 @@ public CallResponseSpec call() {
 
 		@Override
 		public StreamResponseSpec stream() {
+			// Resolve tool callbacks lazily before building the request
+			resolveToolCallbacksBeforeExecution();
 			BaseAdvisorChain advisorChain = buildAdvisorChain();
 			return new DefaultStreamResponseSpec(DefaultChatClientUtils.toChatClientRequest(this), advisorChain,
 					this.observationRegistry, this.observationConvention);
 		}
 
+		/**
+		 * Resolves tool callback providers and adds the results to the toolCallbacks
+		 * list. This method should be called right before execution (call/stream) to
+		 * ensure that all providers have had a chance to complete their initialization,
+		 * including those that depend on SmartInitializingSingleton.
+		 */
+		private void resolveToolCallbacksBeforeExecution() {
+			if (!this.toolCallbackProviders.isEmpty()) {
+				// Resolve all providers and add their callbacks
+				for (ToolCallbackProvider provider : this.toolCallbackProviders) {
+					this.toolCallbacks.addAll(List.of(provider.getToolCallbacks()));
+				}
+				// Clear providers list to avoid re-processing on subsequent calls
+				this.toolCallbackProviders.clear();
+			}
+		}
+
 		private BaseAdvisorChain buildAdvisorChain() {
 			// At the stack bottom add the model call advisors.
 			// They play the role of the last advisors in the advisor chain.
diff --git a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClientBuilder.java b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClientBuilder.java
index a937356e543..6778dc222e5 100644
--- a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClientBuilder.java
+++ b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClientBuilder.java
@@ -65,8 +65,8 @@ public DefaultChatClientBuilder(ChatModel chatModel, ObservationRegistry observa
 		Assert.notNull(chatModel, "the " + ChatModel.class.getName() + " must be non-null");
 		Assert.notNull(observationRegistry, "the " + ObservationRegistry.class.getName() + " must be non-null");
 		this.defaultRequest = new DefaultChatClientRequestSpec(chatModel, null, Map.of(), Map.of(), null, Map.of(),
-				Map.of(), List.of(), List.of(), List.of(), List.of(), null, List.of(), Map.of(), observationRegistry,
-				customObservationConvention, Map.of(), null);
+				Map.of(), List.of(), List.of(), List.of(), List.of(), List.of(), null, List.of(), Map.of(),
+				observationRegistry, customObservationConvention, Map.of(), null);
 	}
 
 	public ChatClient build() {
diff --git a/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/DefaultChatClientTests.java b/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/DefaultChatClientTests.java
index 07adcf72b48..45a1342503b 100644
--- a/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/DefaultChatClientTests.java
+++ b/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/DefaultChatClientTests.java
@@ -30,6 +30,7 @@
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
 import reactor.core.publisher.Flux;
 
 import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
@@ -50,6 +51,7 @@
 import org.springframework.ai.converter.StructuredOutputConverter;
 import org.springframework.ai.template.TemplateRenderer;
 import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.ToolCallbackProvider;
 import org.springframework.ai.tool.function.FunctionToolCallback;
 import org.springframework.core.ParameterizedTypeReference;
 import org.springframework.core.convert.support.DefaultConversionService;
@@ -61,12 +63,16 @@
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 /**
  * Unit tests for {@link DefaultChatClient}.
  *
  * @author Thomas Vitale
+ * @author Christian Tzolov
  */
 class DefaultChatClientTests {
 
@@ -1474,15 +1480,15 @@ void buildChatClientRequestSpec() {
 		ChatModel chatModel = mock(ChatModel.class);
 		DefaultChatClient.DefaultChatClientRequestSpec spec = new DefaultChatClient.DefaultChatClientRequestSpec(
 				chatModel, null, Map.of(), Map.of(), null, Map.of(), Map.of(), List.of(), List.of(), List.of(),
-				List.of(), null, List.of(), Map.of(), ObservationRegistry.NOOP, null, Map.of(), null);
+				List.of(), List.of(), null, List.of(), Map.of(), ObservationRegistry.NOOP, null, Map.of(), null);
 		assertThat(spec).isNotNull();
 	}
 
 	@Test
 	void whenChatModelIsNullThenThrow() {
 		assertThatThrownBy(() -> new DefaultChatClient.DefaultChatClientRequestSpec(null, null, Map.of(), Map.of(),
-				null, Map.of(), Map.of(), List.of(), List.of(), List.of(), List.of(), null, List.of(), Map.of(),
-				ObservationRegistry.NOOP, null, Map.of(), null))
+				null, Map.of(), Map.of(), List.of(), List.of(), List.of(), List.of(), List.of(), null, List.of(),
+				Map.of(), ObservationRegistry.NOOP, null, Map.of(), null))
 			.isInstanceOf(IllegalArgumentException.class)
 			.hasMessage("chatModel cannot be null");
 	}
@@ -1490,8 +1496,8 @@ void whenChatModelIsNullThenThrow() {
 	@Test
 	void whenObservationRegistryIsNullThenThrow() {
 		assertThatThrownBy(() -> new DefaultChatClient.DefaultChatClientRequestSpec(mock(ChatModel.class), null,
-				Map.of(), Map.of(), null, Map.of(), Map.of(), List.of(), List.of(), List.of(), List.of(), null,
-				List.of(), Map.of(), null, null, Map.of(), null))
+				Map.of(), Map.of(), null, Map.of(), Map.of(), List.of(), List.of(), List.of(), List.of(), List.of(),
+				null, List.of(), Map.of(), null, null, Map.of(), null))
 			.isInstanceOf(IllegalArgumentException.class)
 			.hasMessage("observationRegistry cannot be null");
 	}
@@ -2197,6 +2203,135 @@ void whenUserConsumerWithNullParamValueThenThrow() {
 			.hasMessage("value cannot be null");
 	}
 
+	@Test
+	void whenToolCallbackProvidersAddedThenStored() {
+		ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
+		ToolCallbackProvider provider = mock(ToolCallbackProvider.class);
+
+		ChatClient.ChatClientRequestSpec spec = chatClient.prompt().toolCallbacks(provider);
+
+		// Verify provider was stored (not resolved yet)
+		assertThat(spec).isInstanceOf(DefaultChatClient.DefaultChatClientRequestSpec.class);
+	}
+
+	@Test
+	void whenToolCallbackProvidersResolvedLazily() {
+		ChatModel chatModel = mock(ChatModel.class);
+		given(chatModel.call(ArgumentMatchers.any(Prompt.class)))
+			.willReturn(new ChatResponse(List.of(new Generation(new AssistantMessage("response")))));
+
+		ToolCallback mockToolCallback = mock(ToolCallback.class);
+		ToolCallbackProvider mockProvider = mock(ToolCallbackProvider.class);
+		when(mockProvider.getToolCallbacks()).thenReturn(new ToolCallback[] { mockToolCallback });
+
+		ChatClient chatClient = new DefaultChatClientBuilder(chatModel).build();
+
+		// Add provider during configuration
+		ChatClient.ChatClientRequestSpec spec = chatClient.prompt("question").toolCallbacks(mockProvider);
+
+		// Provider should NOT be resolved yet (getToolCallbacks not called during
+		// configuration)
+		verify(mockProvider, never()).getToolCallbacks();
+
+		// Execute call - this should trigger lazy resolution
+		spec.call().content();
+
+		// NOW provider should be resolved (getToolCallbacks called during execution)
+		verify(mockProvider, times(1)).getToolCallbacks();
+	}
+
+	@Test
+	void whenMultipleToolCallbackProvidersResolvedLazily() {
+		ChatModel chatModel = mock(ChatModel.class);
+		given(chatModel.call(org.mockito.ArgumentMatchers.any(Prompt.class)))
+			.willReturn(new ChatResponse(List.of(new Generation(new AssistantMessage("response")))));
+
+		ToolCallback mockToolCallback1 = mock(ToolCallback.class);
+		ToolCallback mockToolCallback2 = mock(ToolCallback.class);
+
+		ToolCallbackProvider mockProvider1 = mock(ToolCallbackProvider.class);
+		when(mockProvider1.getToolCallbacks()).thenReturn(new ToolCallback[] { mockToolCallback1 });
+
+		ToolCallbackProvider mockProvider2 = mock(ToolCallbackProvider.class);
+		when(mockProvider2.getToolCallbacks()).thenReturn(new ToolCallback[] { mockToolCallback2 });
+
+		ChatClient chatClient = new DefaultChatClientBuilder(chatModel).build();
+
+		// Add multiple providers
+		ChatClient.ChatClientRequestSpec spec = chatClient.prompt("question")
+			.toolCallbacks(mockProvider1, mockProvider2);
+
+		// Execute call
+		spec.call().content();
+
+		// Both providers should be resolved
+		verify(mockProvider1, times(1)).getToolCallbacks();
+		verify(mockProvider2, times(1)).getToolCallbacks();
+	}
+
+	@Test
+	void whenToolCallbackProvidersResolvedLazilyInStream() {
+		ChatModel chatModel = mock(ChatModel.class);
+		given(chatModel.stream(org.mockito.ArgumentMatchers.any(Prompt.class)))
+			.willReturn(Flux.just(new ChatResponse(List.of(new Generation(new AssistantMessage("response"))))));
+
+		ToolCallback mockToolCallback = mock(ToolCallback.class);
+		ToolCallbackProvider mockProvider = mock(ToolCallbackProvider.class);
+		when(mockProvider.getToolCallbacks()).thenReturn(new ToolCallback[] { mockToolCallback });
+
+		ChatClient chatClient = new DefaultChatClientBuilder(chatModel).build();
+
+		// Add provider during configuration
+		ChatClient.ChatClientRequestSpec spec = chatClient.prompt("question").toolCallbacks(mockProvider);
+
+		// Provider should NOT be resolved yet
+		verify(mockProvider, never()).getToolCallbacks();
+
+		// Execute stream - this should trigger lazy resolution
+		spec.stream().content().blockLast();
+
+		// NOW provider should be resolved
+		verify(mockProvider, times(1)).getToolCallbacks();
+	}
+
+	@Test
+	void whenToolCallbackProviderIsNullThenThrow() {
+		ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
+		ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
+
+		assertThatThrownBy(() -> spec.toolCallbacks((ToolCallbackProvider) null))
+			.isInstanceOf(IllegalArgumentException.class)
+			.hasMessage("toolCallbackProviders cannot contain null elements");
+	}
+
+	@Test
+	void whenToolCallbackProvidersWithMixedCallbacksAndProviders() {
+		ChatModel chatModel = mock(ChatModel.class);
+		given(chatModel.call(org.mockito.ArgumentMatchers.any(Prompt.class)))
+			.willReturn(new ChatResponse(List.of(new Generation(new AssistantMessage("response")))));
+
+		// Direct callback
+		ToolCallback directCallback = mock(ToolCallback.class);
+
+		// Provider callback
+		ToolCallback providerCallback = mock(ToolCallback.class);
+		ToolCallbackProvider mockProvider = mock(ToolCallbackProvider.class);
+		when(mockProvider.getToolCallbacks()).thenReturn(new ToolCallback[] { providerCallback });
+
+		ChatClient chatClient = new DefaultChatClientBuilder(chatModel).build();
+
+		// Add both direct callbacks and providers
+		ChatClient.ChatClientRequestSpec spec = chatClient.prompt("question")
+			.toolCallbacks(directCallback)
+			.toolCallbacks(mockProvider);
+
+		// Execute call
+		spec.call().content();
+
+		// Provider should be resolved
+		verify(mockProvider, times(1)).getToolCallbacks();
+	}
+
 	record Person(String name) {
 	}