diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfiguration.java
index ba91930e5..77077d432 100644
--- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfiguration.java
+++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfiguration.java
@@ -27,12 +27,8 @@ import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.ai.mcp.client.autoconfigure.configurer.McpAsyncClientConfigurer;
import org.springframework.ai.mcp.client.autoconfigure.configurer.McpSyncClientConfigurer;
import org.springframework.ai.mcp.client.autoconfigure.properties.McpClientCommonProperties;
-import org.springframework.ai.mcp.AsyncMcpToolCallbackProvider;
-import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.mcp.customizer.McpAsyncClientCustomizer;
import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer;
-import org.springframework.ai.tool.ToolCallback;
-import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -179,36 +175,6 @@ public class McpClientAutoConfiguration {
return mcpSyncClients;
}
- /**
- * Creates tool callbacks for all configured MCP clients.
- *
- *
- * These callbacks enable integration with Spring AI's tool execution framework,
- * allowing MCP tools to be used as part of AI interactions.
- * @param mcpClientsProvider provider of MCP sync clients
- * @return list of tool callbacks for MCP integration
- */
- @Bean
- @ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
- matchIfMissing = true)
- public ToolCallbackProvider toolCallbacks(ObjectProvider> mcpClientsProvider) {
- List mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
- return new SyncMcpToolCallbackProvider(mcpClients);
- }
-
- /**
- * @deprecated replaced by {@link #toolCallbacks(ObjectProvider)} that returns a
- * {@link ToolCallbackProvider} instead of a list of {@link ToolCallback}
- */
- @Deprecated
- @Bean
- @ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
- matchIfMissing = true)
- public List toolCallbacksDeprecated(ObjectProvider> mcpClientsProvider) {
- List mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
- return List.of(new SyncMcpToolCallbackProvider(mcpClients).getToolCallbacks());
- }
-
/**
* Record class that implements {@link AutoCloseable} to ensure proper cleanup of MCP
* clients.
@@ -292,25 +258,6 @@ public class McpClientAutoConfiguration {
return mcpSyncClients;
}
- /**
- * @deprecated replaced by {@link #asyncToolCallbacks(ObjectProvider)} that returns a
- * {@link ToolCallbackProvider} instead of a list of {@link ToolCallback}
- */
- @Deprecated
- @Bean
- @ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
- public List asyncToolCallbacksDeprecated(ObjectProvider> mcpClientsProvider) {
- List mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
- return List.of(new AsyncMcpToolCallbackProvider(mcpClients).getToolCallbacks());
- }
-
- @Bean
- @ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
- public ToolCallbackProvider asyncToolCallbacks(ObjectProvider> mcpClientsProvider) {
- List mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
- return new AsyncMcpToolCallbackProvider(mcpClients);
- }
-
public record CloseableMcpAsyncClients(List clients) implements AutoCloseable {
@Override
public void close() {
diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/McpToolCallbackAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/McpToolCallbackAutoConfiguration.java
new file mode 100644
index 000000000..65bd8ebbc
--- /dev/null
+++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/McpToolCallbackAutoConfiguration.java
@@ -0,0 +1,87 @@
+/*
+ * 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.client.autoconfigure;
+
+import java.util.List;
+
+import io.modelcontextprotocol.client.McpAsyncClient;
+import io.modelcontextprotocol.client.McpSyncClient;
+
+import org.springframework.ai.mcp.AsyncMcpToolCallbackProvider;
+import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
+import org.springframework.ai.mcp.client.autoconfigure.properties.McpClientCommonProperties;
+import org.springframework.ai.tool.ToolCallbackProvider;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Conditional;
+
+/**
+ */
+@AutoConfiguration(after = { McpClientAutoConfiguration.class })
+@EnableConfigurationProperties(McpClientCommonProperties.class)
+@Conditional(McpToolCallbackAutoConfiguration.McpToolCallbackAutoconfigurationCondition.class)
+public class McpToolCallbackAutoConfiguration {
+
+ public static class McpToolCallbackAutoconfigurationCondition extends AllNestedConditions {
+
+ public McpToolCallbackAutoconfigurationCondition() {
+ super(ConfigurationPhase.PARSE_CONFIGURATION);
+ }
+
+ @ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ static class McpAutoConfigEnabled {
+
+ }
+
+ @ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX + ".toolcallback", name = "enabled",
+ havingValue = "true", matchIfMissing = false)
+ static class ToolCallbackProviderEnabled {
+
+ }
+
+ }
+
+ /**
+ * Creates tool callbacks for all configured MCP clients.
+ *
+ *
+ * These callbacks enable integration with Spring AI's tool execution framework,
+ * allowing MCP tools to be used as part of AI interactions.
+ * @param syncMcpClients provider of MCP sync clients
+ * @return list of tool callbacks for MCP integration
+ */
+ @Bean
+ @ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
+ matchIfMissing = true)
+ public ToolCallbackProvider mcpToolCallbacks(ObjectProvider> syncMcpClients) {
+ List mcpClients = syncMcpClients.stream().flatMap(List::stream).toList();
+ return new SyncMcpToolCallbackProvider(mcpClients);
+ }
+
+ @Bean
+ @ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
+ public ToolCallbackProvider mcpAsyncToolCallbacks(ObjectProvider> mcpClientsProvider) {
+ List mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
+ return new AsyncMcpToolCallbackProvider(mcpClients);
+ }
+
+}
diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/NamedClientMcpTransport.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/NamedClientMcpTransport.java
index e5c7cbc0a..6d254e326 100644
--- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/NamedClientMcpTransport.java
+++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/NamedClientMcpTransport.java
@@ -15,7 +15,7 @@
*/
package org.springframework.ai.mcp.client.autoconfigure;
-import io.modelcontextprotocol.spec.ClientMcpTransport;
+import io.modelcontextprotocol.spec.McpClientTransport;
/**
* A named MCP client transport. Usually created by the transport auto-configurations, but
@@ -26,6 +26,6 @@ import io.modelcontextprotocol.spec.ClientMcpTransport;
* @author Christian Tzolov
* @since 1.0.0
*/
-public record NamedClientMcpTransport(String name, ClientMcpTransport transport) {
+public record NamedClientMcpTransport(String name, McpClientTransport transport) {
}
diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 78815050a..e740e0f7d 100644
--- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -17,5 +17,6 @@ org.springframework.ai.mcp.client.autoconfigure.StdioTransportAutoConfiguration
org.springframework.ai.mcp.client.autoconfigure.SseWebFluxTransportAutoConfiguration
org.springframework.ai.mcp.client.autoconfigure.SseHttpClientTransportAutoConfiguration
org.springframework.ai.mcp.client.autoconfigure.McpClientAutoConfiguration
+org.springframework.ai.mcp.client.autoconfigure.McpToolCallbackAutoConfiguration
diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfigurationIT.java
index 71f18858a..8372ebdd3 100644
--- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfigurationIT.java
+++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfigurationIT.java
@@ -23,7 +23,7 @@ import java.util.function.Function;
import com.fasterxml.jackson.core.type.TypeReference;
import io.modelcontextprotocol.client.McpAsyncClient;
import io.modelcontextprotocol.client.McpSyncClient;
-import io.modelcontextprotocol.spec.ClientMcpTransport;
+import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -44,8 +44,8 @@ import static org.assertj.core.api.Assertions.assertThat;
@Disabled
public class McpClientAutoConfigurationIT {
- private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
- .withConfiguration(AutoConfigurations.of(McpClientAutoConfiguration.class));
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(
+ AutoConfigurations.of(McpToolCallbackAutoConfiguration.class, McpClientAutoConfiguration.class));
@Test
void defaultConfiguration() {
@@ -131,7 +131,7 @@ public class McpClientAutoConfigurationIT {
@Bean
List testTransports() {
- return List.of(new NamedClientMcpTransport("test", Mockito.mock(ClientMcpTransport.class)));
+ return List.of(new NamedClientMcpTransport("test", Mockito.mock(McpClientTransport.class)));
}
}
@@ -157,7 +157,7 @@ public class McpClientAutoConfigurationIT {
}
- static class CustomClientTransport implements ClientMcpTransport {
+ static class CustomClientTransport implements McpClientTransport {
@Override
public void close() {
diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpToolCallbackAutoConfigurationTests.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpToolCallbackAutoConfigurationTests.java
new file mode 100644
index 000000000..86202c87c
--- /dev/null
+++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpToolCallbackAutoConfigurationTests.java
@@ -0,0 +1,88 @@
+/*
+ * 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.client.autoconfigure;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class McpToolCallbackAutoConfigurationTests {
+
+ private final ApplicationContextRunner applicationContext = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(McpToolCallbackAutoConfiguration.class));
+
+ @Test
+ void disabledByDeafault() {
+
+ this.applicationContext.run((context) -> {
+ assertThat(context).doesNotHaveBean("mcpToolCallbacks");
+ assertThat(context).doesNotHaveBean("mcpAsyncToolCallbacks");
+ });
+
+ this.applicationContext
+ .withPropertyValues("spring.ai.mcp.client.enabled=true", "spring.ai.mcp.client.type=SYNC")
+ .run((context) -> {
+ assertThat(context).doesNotHaveBean("mcpToolCallbacks");
+ assertThat(context).doesNotHaveBean("mcpAsyncToolCallbacks");
+ });
+
+ this.applicationContext
+ .withPropertyValues("spring.ai.mcp.client.enabled=true", "spring.ai.mcp.client.type=ASYNC")
+ .run((context) -> {
+ assertThat(context).doesNotHaveBean("mcpToolCallbacks");
+ assertThat(context).doesNotHaveBean("mcpAsyncToolCallbacks");
+ });
+ }
+
+ @Test
+ void enabledMcpToolCallbackAutoconfiguration() {
+
+ // sync
+ this.applicationContext.withPropertyValues("spring.ai.mcp.client.toolcallback.enabled=true").run((context) -> {
+ assertThat(context).hasBean("mcpToolCallbacks");
+ assertThat(context).doesNotHaveBean("mcpAsyncToolCallbacks");
+ });
+
+ this.applicationContext
+ .withPropertyValues("spring.ai.mcp.client.enabled=true", "spring.ai.mcp.client.toolcallback.enabled=true",
+ "spring.ai.mcp.client.type=SYNC")
+ .run((context) -> {
+ assertThat(context).hasBean("mcpToolCallbacks");
+ assertThat(context).doesNotHaveBean("mcpAsyncToolCallbacks");
+ });
+
+ // Async
+ this.applicationContext
+ .withPropertyValues("spring.ai.mcp.client.toolcallback.enabled=true", "spring.ai.mcp.client.type=ASYNC")
+ .run((context) -> {
+ assertThat(context).doesNotHaveBean("mcpToolCallbacks");
+ assertThat(context).hasBean("mcpAsyncToolCallbacks");
+ });
+
+ this.applicationContext
+ .withPropertyValues("spring.ai.mcp.client.enabled=true", "spring.ai.mcp.client.toolcallback.enabled=true",
+ "spring.ai.mcp.client.type=ASYNC")
+ .run((context) -> {
+ assertThat(context).doesNotHaveBean("mcpToolCallbacks");
+ assertThat(context).hasBean("mcpAsyncToolCallbacks");
+ });
+ }
+
+}
diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpToolCallbackAutoconfigurationConditionTests.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpToolCallbackAutoconfigurationConditionTests.java
new file mode 100644
index 000000000..682ee6c49
--- /dev/null
+++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpToolCallbackAutoconfigurationConditionTests.java
@@ -0,0 +1,102 @@
+/*
+ * 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.client.autoconfigure;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.mcp.client.autoconfigure.McpToolCallbackAutoConfiguration.McpToolCallbackAutoconfigurationCondition;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.context.annotation.Configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link McpToolCallbackAutoconfigurationCondition}.
+ */
+public class McpToolCallbackAutoconfigurationConditionTests {
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withUserConfiguration(TestConfiguration.class);
+
+ @Test
+ void matchesWhenBothPropertiesAreEnabled() {
+ this.contextRunner
+ .withPropertyValues("spring.ai.mcp.client.enabled=true", "spring.ai.mcp.client.toolcallback.enabled=true")
+ .run(context -> {
+ assertThat(context).hasBean("testBean");
+ });
+ }
+
+ @Test
+ void doesNotMatchWhenMcpClientIsDisabled() {
+ this.contextRunner
+ .withPropertyValues("spring.ai.mcp.client.enabled=false", "spring.ai.mcp.client.toolcallback.enabled=true")
+ .run(context -> {
+ assertThat(context).doesNotHaveBean("testBean");
+ });
+ }
+
+ @Test
+ void doesNotMatchWhenToolCallbackIsDisabled() {
+ this.contextRunner
+ .withPropertyValues("spring.ai.mcp.client.enabled=true", "spring.ai.mcp.client.toolcallback.enabled=false")
+ .run(context -> {
+ assertThat(context).doesNotHaveBean("testBean");
+ });
+ }
+
+ @Test
+ void doesNotMatchWhenBothPropertiesAreDisabled() {
+ this.contextRunner
+ .withPropertyValues("spring.ai.mcp.client.enabled=false", "spring.ai.mcp.client.toolcallback.enabled=false")
+ .run(context -> {
+ assertThat(context).doesNotHaveBean("testBean");
+ });
+ }
+
+ @Test
+ void doesNotMatchWhenToolCallbackPropertyIsMissing() {
+ // McpClientEnabled is true by default if missing, but ToolCallbackEnabled is
+ // false by default if missing
+ this.contextRunner.withPropertyValues("spring.ai.mcp.client.enabled=true").run(context -> {
+ assertThat(context).doesNotHaveBean("testBean");
+ });
+ }
+
+ @Test
+ void doesNotMatchWhenBothPropertiesAreMissing() {
+ // McpClientEnabled is true by default if missing, but ToolCallbackEnabled is
+ // false by default if missing
+ this.contextRunner.run(context -> {
+ assertThat(context).doesNotHaveBean("testBean");
+ });
+ }
+
+ @Configuration
+ @Conditional(McpToolCallbackAutoconfigurationCondition.class)
+ static class TestConfiguration {
+
+ @Bean
+ String testBean() {
+ return "testBean";
+ }
+
+ }
+
+}
diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpBackwardCompatibility.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpBackwardCompatibility.java
new file mode 100644
index 000000000..3d4e59be6
--- /dev/null
+++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpBackwardCompatibility.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.server.autoconfigure;
+
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+import io.modelcontextprotocol.server.McpServerFeatures;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpSchema;
+
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * @author Christian Tzolov
+ */
+@Deprecated
+@Configuration
+public class McpBackwardCompatibility {
+
+ @Bean
+ public List>> syncRootsChangeConsumerToHandler(
+ List>> rootsChangeConsumers) {
+
+ if (CollectionUtils.isEmpty(rootsChangeConsumers)) {
+ return List.of();
+ }
+
+ return rootsChangeConsumers.stream()
+ .map(c -> (BiConsumer>) ((exchange, roots) -> c.accept(roots)))
+ .toList();
+ }
+
+ @Bean
+ public List syncToolsRegistrationToSpecificaiton(
+ ObjectProvider> toolRegistrations) {
+
+ return toolRegistrations.stream()
+ .flatMap(List::stream)
+ .map(McpServerFeatures.SyncToolRegistration::toSpecification)
+ .toList();
+ }
+
+ @Bean
+ public List syncResourceRegistrationToSpecificaiton(
+ ObjectProvider> resourceRegistrations) {
+
+ return resourceRegistrations.stream()
+ .flatMap(List::stream)
+ .map(McpServerFeatures.SyncResourceRegistration::toSpecification)
+ .toList();
+ }
+
+ @Bean
+ public List syncPromptRegistrationToSpecificaiton(
+ ObjectProvider> promptRegistrations) {
+
+ return promptRegistrations.stream()
+ .flatMap(List::stream)
+ .map(McpServerFeatures.SyncPromptRegistration::toSpecification)
+ .toList();
+ }
+
+ // Async
+ @Bean
+ public List asyncToolsRegistrationToSpecificaiton(
+ ObjectProvider> toolRegistrations) {
+
+ return toolRegistrations.stream()
+ .flatMap(List::stream)
+ .map(McpServerFeatures.AsyncToolRegistration::toSpecification)
+ .toList();
+ }
+
+ @Bean
+ public List asyncResourceRegistrationToSpecificaiton(
+ ObjectProvider> resourceRegistrations) {
+
+ return resourceRegistrations.stream()
+ .flatMap(List::stream)
+ .map(McpServerFeatures.AsyncResourceRegistration::toSpecification)
+ .toList();
+ }
+
+ @Bean
+ public List asyncPromptRegistrationToSpecificaiton(
+ ObjectProvider> promptRegistrations) {
+
+ return promptRegistrations.stream()
+ .flatMap(List::stream)
+ .map(McpServerFeatures.AsyncPromptRegistration::toSpecification)
+ .toList();
+ }
+
+}
diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfiguration.java
index ae8e86ef5..64d80f420 100644
--- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfiguration.java
+++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfiguration.java
@@ -18,25 +18,27 @@ package org.springframework.ai.mcp.server.autoconfigure;
import java.util.ArrayList;
import java.util.List;
-import java.util.function.Consumer;
-import java.util.function.Function;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
import io.modelcontextprotocol.server.McpAsyncServer;
+import io.modelcontextprotocol.server.McpAsyncServerExchange;
import io.modelcontextprotocol.server.McpServer;
-import io.modelcontextprotocol.server.McpServer.AsyncSpec;
-import io.modelcontextprotocol.server.McpServer.SyncSpec;
+import io.modelcontextprotocol.server.McpServer.AsyncSpecification;
+import io.modelcontextprotocol.server.McpServer.SyncSpecification;
import io.modelcontextprotocol.server.McpServerFeatures;
-import io.modelcontextprotocol.server.McpServerFeatures.AsyncPromptRegistration;
-import io.modelcontextprotocol.server.McpServerFeatures.AsyncResourceRegistration;
-import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolRegistration;
-import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptRegistration;
-import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceRegistration;
-import io.modelcontextprotocol.server.McpServerFeatures.SyncToolRegistration;
+import io.modelcontextprotocol.server.McpServerFeatures.AsyncPromptSpecification;
+import io.modelcontextprotocol.server.McpServerFeatures.AsyncResourceSpecification;
+import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification;
+import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptSpecification;
+import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification;
+import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
import io.modelcontextprotocol.server.McpSyncServer;
-import io.modelcontextprotocol.server.transport.StdioServerTransport;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
+import io.modelcontextprotocol.server.transport.StdioServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.Implementation;
-import io.modelcontextprotocol.spec.ServerMcpTransport;
+import io.modelcontextprotocol.spec.McpServerTransportProvider;
import reactor.core.publisher.Mono;
import org.springframework.ai.mcp.McpToolUtils;
@@ -50,6 +52,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Import;
import org.springframework.core.log.LogAccessor;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MimeType;
@@ -100,11 +103,13 @@ import org.springframework.util.MimeType;
* @since 1.0.0
* @see McpServerProperties
* @see McpWebMvcServerAutoConfiguration
+ * @see McpWebFluxServerAutoConfiguration
* @see ToolCallback
*/
@AutoConfiguration(after = { McpWebMvcServerAutoConfiguration.class, McpWebFluxServerAutoConfiguration.class })
@ConditionalOnClass({ McpSchema.class, McpSyncServer.class })
@EnableConfigurationProperties(McpServerProperties.class)
+@Import(McpBackwardCompatibility.class)
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
matchIfMissing = true)
public class McpServerAutoConfiguration {
@@ -113,8 +118,8 @@ public class McpServerAutoConfiguration {
@Bean
@ConditionalOnMissingBean
- public ServerMcpTransport stdioServerTransport() {
- return new StdioServerTransport();
+ public McpServerTransportProvider stdioServerTransport() {
+ return new StdioServerTransportProvider();
}
@Bean
@@ -126,40 +131,46 @@ public class McpServerAutoConfiguration {
@Bean
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
matchIfMissing = true)
- public List syncTools(ObjectProvider> toolCalls,
- McpServerProperties serverProperties) {
- List tools = toolCalls.stream().flatMap(List::stream).toList();
+ public List syncTools(ObjectProvider> toolCalls,
+ List toolCallbacksList, McpServerProperties serverProperties) {
- return this.toSyncToolRegistration(tools, serverProperties);
+ List tools = new ArrayList<>(toolCalls.stream().flatMap(List::stream).toList());
+
+ if (!CollectionUtils.isEmpty(toolCallbacksList)) {
+ tools.addAll(toolCallbacksList);
+ }
+
+ return this.toSyncToolSpecifications(tools, serverProperties);
}
- private List toSyncToolRegistration(List tools,
+ private List toSyncToolSpecifications(List tools,
McpServerProperties serverProperties) {
return tools.stream().map(tool -> {
String toolName = tool.getToolDefinition().name();
MimeType mimeType = (serverProperties.getToolResponseMimeType().containsKey(toolName))
? MimeType.valueOf(serverProperties.getToolResponseMimeType().get(toolName)) : null;
- return McpToolUtils.toSyncToolRegistration(tool, mimeType);
+ return McpToolUtils.toSyncToolSpecification(tool, mimeType);
}).toList();
}
@Bean
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
matchIfMissing = true)
- public McpSyncServer mcpSyncServer(ServerMcpTransport transport,
+ public McpSyncServer mcpSyncServer(McpServerTransportProvider transportProvider,
McpSchema.ServerCapabilities.Builder capabilitiesBuilder, McpServerProperties serverProperties,
- ObjectProvider> tools, ObjectProvider> resources,
- ObjectProvider> prompts,
- ObjectProvider>> rootsChangeConsumers,
+ ObjectProvider> tools,
+ ObjectProvider> resources,
+ ObjectProvider> prompts,
+ ObjectProvider>> rootsChangeConsumers,
List toolCallbackProvider) {
McpSchema.Implementation serverInfo = new Implementation(serverProperties.getName(),
serverProperties.getVersion());
// Create the server with both tool and resource capabilities
- SyncSpec serverBuilder = McpServer.sync(transport).serverInfo(serverInfo);
+ SyncSpecification serverBuilder = McpServer.sync(transportProvider).serverInfo(serverInfo);
- List toolRegistrations = new ArrayList<>(tools.stream().flatMap(List::stream).toList());
+ List toolSpecifications = new ArrayList<>(tools.stream().flatMap(List::stream).toList());
List providerToolCallbacks = toolCallbackProvider.stream()
.map(pr -> List.of(pr.getToolCallbacks()))
@@ -168,33 +179,35 @@ public class McpServerAutoConfiguration {
.map(fc -> (ToolCallback) fc)
.toList();
- toolRegistrations.addAll(this.toSyncToolRegistration(providerToolCallbacks, serverProperties));
+ toolSpecifications.addAll(this.toSyncToolSpecifications(providerToolCallbacks, serverProperties));
- if (!CollectionUtils.isEmpty(toolRegistrations)) {
- serverBuilder.tools(toolRegistrations);
+ if (!CollectionUtils.isEmpty(toolSpecifications)) {
+ serverBuilder.tools(toolSpecifications);
capabilitiesBuilder.tools(serverProperties.isToolChangeNotification());
- logger.info("Registered tools: " + toolRegistrations.size() + ", notification: "
+ logger.info("Registered tools: " + toolSpecifications.size() + ", notification: "
+ serverProperties.isToolChangeNotification());
}
- List resourceRegistrations = resources.stream().flatMap(List::stream).toList();
- if (!CollectionUtils.isEmpty(resourceRegistrations)) {
- serverBuilder.resources(resourceRegistrations);
+ List resourceSpecifications = resources.stream().flatMap(List::stream).toList();
+ if (!CollectionUtils.isEmpty(resourceSpecifications)) {
+ serverBuilder.resources(resourceSpecifications);
capabilitiesBuilder.resources(false, serverProperties.isResourceChangeNotification());
- logger.info("Registered resources: " + resourceRegistrations.size() + ", notification: "
+ logger.info("Registered resources: " + resourceSpecifications.size() + ", notification: "
+ serverProperties.isResourceChangeNotification());
}
- List promptRegistrations = prompts.stream().flatMap(List::stream).toList();
- if (!CollectionUtils.isEmpty(promptRegistrations)) {
- serverBuilder.prompts(promptRegistrations);
+ List promptSpecifications = prompts.stream().flatMap(List::stream).toList();
+ if (!CollectionUtils.isEmpty(promptSpecifications)) {
+ serverBuilder.prompts(promptSpecifications);
capabilitiesBuilder.prompts(serverProperties.isPromptChangeNotification());
- logger.info("Registered prompts: " + promptRegistrations.size() + ", notification: "
+ logger.info("Registered prompts: " + promptSpecifications.size() + ", notification: "
+ serverProperties.isPromptChangeNotification());
}
rootsChangeConsumers.ifAvailable(consumer -> {
- serverBuilder.rootsChangeConsumer(consumer);
+ serverBuilder.rootsChangeHandler((exchange, roots) -> {
+ consumer.accept(exchange, roots);
+ });
logger.info("Registered roots change consumer");
});
@@ -205,40 +218,45 @@ public class McpServerAutoConfiguration {
@Bean
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
- public List asyncTools(ObjectProvider> toolCalls,
- McpServerProperties serverProperties) {
- var tools = toolCalls.stream().flatMap(List::stream).toList();
+ public List asyncTools(ObjectProvider> toolCalls,
+ List toolCallbackList, McpServerProperties serverProperties) {
- return this.toAsyncToolRegistration(tools, serverProperties);
+ List tools = new ArrayList<>(toolCalls.stream().flatMap(List::stream).toList());
+ if (!CollectionUtils.isEmpty(toolCallbackList)) {
+ tools.addAll(toolCallbackList);
+ }
+
+ return this.toAsyncToolSpecification(tools, serverProperties);
}
- private List toAsyncToolRegistration(List tools,
+ private List toAsyncToolSpecification(List tools,
McpServerProperties serverProperties) {
return tools.stream().map(tool -> {
String toolName = tool.getToolDefinition().name();
MimeType mimeType = (serverProperties.getToolResponseMimeType().containsKey(toolName))
? MimeType.valueOf(serverProperties.getToolResponseMimeType().get(toolName)) : null;
- return McpToolUtils.toAsyncToolRegistration(tool, mimeType);
+ return McpToolUtils.toAsyncToolSpecification(tool, mimeType);
}).toList();
}
@Bean
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
- public McpAsyncServer mcpAsyncServer(ServerMcpTransport transport,
+ public McpAsyncServer mcpAsyncServer(McpServerTransportProvider transportProvider,
McpSchema.ServerCapabilities.Builder capabilitiesBuilder, McpServerProperties serverProperties,
- ObjectProvider> tools,
- ObjectProvider> resources,
- ObjectProvider> prompts,
- ObjectProvider>> rootsChangeConsumer,
+ ObjectProvider> tools,
+ ObjectProvider> resources,
+ ObjectProvider> prompts,
+ ObjectProvider>> rootsChangeConsumer,
List toolCallbackProvider) {
McpSchema.Implementation serverInfo = new Implementation(serverProperties.getName(),
serverProperties.getVersion());
// Create the server with both tool and resource capabilities
- AsyncSpec serverBuilder = McpServer.async(transport).serverInfo(serverInfo);
+ AsyncSpecification serverBuilder = McpServer.async(transportProvider).serverInfo(serverInfo);
- List toolRegistrations = new ArrayList<>(tools.stream().flatMap(List::stream).toList());
+ List toolSpecifications = new ArrayList<>(
+ tools.stream().flatMap(List::stream).toList());
List providerToolCallbacks = toolCallbackProvider.stream()
.map(pr -> List.of(pr.getToolCallbacks()))
.flatMap(List::stream)
@@ -246,37 +264,37 @@ public class McpServerAutoConfiguration {
.map(fc -> (ToolCallback) fc)
.toList();
- toolRegistrations.addAll(this.toAsyncToolRegistration(providerToolCallbacks, serverProperties));
+ toolSpecifications.addAll(this.toAsyncToolSpecification(providerToolCallbacks, serverProperties));
- if (!CollectionUtils.isEmpty(toolRegistrations)) {
- serverBuilder.tools(toolRegistrations);
+ if (!CollectionUtils.isEmpty(toolSpecifications)) {
+ serverBuilder.tools(toolSpecifications);
capabilitiesBuilder.tools(serverProperties.isToolChangeNotification());
- logger.info("Registered tools: " + toolRegistrations.size() + ", notification: "
+ logger.info("Registered tools: " + toolSpecifications.size() + ", notification: "
+ serverProperties.isToolChangeNotification());
}
- List resourceRegistrations = resources.stream().flatMap(List::stream).toList();
- if (!CollectionUtils.isEmpty(resourceRegistrations)) {
- serverBuilder.resources(resourceRegistrations);
+ List resourceSpecifications = resources.stream().flatMap(List::stream).toList();
+ if (!CollectionUtils.isEmpty(resourceSpecifications)) {
+ serverBuilder.resources(resourceSpecifications);
capabilitiesBuilder.resources(false, serverProperties.isResourceChangeNotification());
- logger.info("Registered resources: " + resourceRegistrations.size() + ", notification: "
+ logger.info("Registered resources: " + resourceSpecifications.size() + ", notification: "
+ serverProperties.isResourceChangeNotification());
}
- List promptRegistrations = prompts.stream().flatMap(List::stream).toList();
- if (!CollectionUtils.isEmpty(promptRegistrations)) {
- serverBuilder.prompts(promptRegistrations);
+ List promptSpecifications = prompts.stream().flatMap(List::stream).toList();
+ if (!CollectionUtils.isEmpty(promptSpecifications)) {
+ serverBuilder.prompts(promptSpecifications);
capabilitiesBuilder.prompts(serverProperties.isPromptChangeNotification());
- logger.info("Registered prompts: " + promptRegistrations.size() + ", notification: "
+ logger.info("Registered prompts: " + promptSpecifications.size() + ", notification: "
+ serverProperties.isPromptChangeNotification());
}
rootsChangeConsumer.ifAvailable(consumer -> {
- Function, Mono> asyncConsumer = roots -> {
- consumer.accept(roots);
+ BiFunction, Mono> asyncConsumer = (exchange, roots) -> {
+ consumer.accept(exchange, roots);
return Mono.empty();
};
- serverBuilder.rootsChangeConsumer(asyncConsumer);
+ serverBuilder.rootsChangeHandler(asyncConsumer);
logger.info("Registered roots change consumer");
});
diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpWebFluxServerAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpWebFluxServerAutoConfiguration.java
index 81108cc2b..eadb2b7cd 100644
--- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpWebFluxServerAutoConfiguration.java
+++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpWebFluxServerAutoConfiguration.java
@@ -17,8 +17,8 @@
package org.springframework.ai.mcp.server.autoconfigure;
import com.fasterxml.jackson.databind.ObjectMapper;
-import io.modelcontextprotocol.server.transport.WebFluxSseServerTransport;
-import io.modelcontextprotocol.spec.ServerMcpTransport;
+import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider;
+import io.modelcontextprotocol.spec.McpServerTransportProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -34,8 +34,8 @@ import org.springframework.web.reactive.function.server.RouterFunction;
* server, providing reactive Server-Sent Events (SSE) communication through Spring
* WebFlux. It is activated when:
*
- * - The WebFluxSseServerTransport class is on the classpath (from mcp-spring-webflux
- * dependency)
+ * - The WebFluxSseServerTransportProvider class is on the classpath (from
+ * mcp-spring-webflux dependency)
* - Spring WebFlux's RouterFunction class is available (from
* spring-boot-starter-webflux)
* - The {@code spring.ai.mcp.server.transport} property is set to {@code WEBFLUX}
@@ -43,7 +43,8 @@ import org.springframework.web.reactive.function.server.RouterFunction;
*
* The configuration provides:
*
- * - A WebFluxSseServerTransport bean for handling reactive SSE communication
+ * - A WebFluxSseServerTransportProvider bean for handling reactive SSE
+ * communication
* - A RouterFunction bean that sets up the reactive SSE endpoint
*
*
@@ -61,25 +62,25 @@ import org.springframework.web.reactive.function.server.RouterFunction;
* @author Christian Tzolov
* @since 1.0.0
* @see McpServerProperties
- * @see WebFluxSseServerTransport
+ * @see WebFluxSseServerTransportProvider
*/
@AutoConfiguration
-@ConditionalOnClass({ WebFluxSseServerTransport.class })
-@ConditionalOnMissingBean(ServerMcpTransport.class)
+@ConditionalOnClass({ WebFluxSseServerTransportProvider.class })
+@ConditionalOnMissingBean(McpServerTransportProvider.class)
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "stdio", havingValue = "false",
matchIfMissing = true)
public class McpWebFluxServerAutoConfiguration {
@Bean
@ConditionalOnMissingBean
- public WebFluxSseServerTransport webFluxTransport(McpServerProperties serverProperties) {
- return new WebFluxSseServerTransport(new ObjectMapper(), serverProperties.getSseMessageEndpoint());
+ public WebFluxSseServerTransportProvider webFluxTransport(McpServerProperties serverProperties) {
+ return new WebFluxSseServerTransportProvider(new ObjectMapper(), serverProperties.getSseMessageEndpoint());
}
// Router function for SSE transport used by Spring WebFlux to start an HTTP server.
@Bean
- public RouterFunction> webfluxMcpRouterFunction(WebFluxSseServerTransport webFluxTransport) {
- return webFluxTransport.getRouterFunction();
+ public RouterFunction> webfluxMcpRouterFunction(WebFluxSseServerTransportProvider webFluxProvider) {
+ return webFluxProvider.getRouterFunction();
}
}
diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpWebMvcServerAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpWebMvcServerAutoConfiguration.java
index d7cd68bf4..828ef836a 100644
--- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpWebMvcServerAutoConfiguration.java
+++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpWebMvcServerAutoConfiguration.java
@@ -17,8 +17,8 @@
package org.springframework.ai.mcp.server.autoconfigure;
import com.fasterxml.jackson.databind.ObjectMapper;
-import io.modelcontextprotocol.server.transport.WebMvcSseServerTransport;
-import io.modelcontextprotocol.spec.ServerMcpTransport;
+import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider;
+import io.modelcontextprotocol.spec.McpServerTransportProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -57,25 +57,25 @@ import org.springframework.web.servlet.function.ServerResponse;
* @author Christian Tzolov
* @since 1.0.0
* @see McpServerProperties
- * @see WebMvcSseServerTransport
+ * @see WebMvcSseServerTransportProvider
*/
@AutoConfiguration
-@ConditionalOnClass({ WebMvcSseServerTransport.class })
-@ConditionalOnMissingBean(ServerMcpTransport.class)
+@ConditionalOnClass({ WebMvcSseServerTransportProvider.class })
+@ConditionalOnMissingBean(McpServerTransportProvider.class)
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "stdio", havingValue = "false",
matchIfMissing = true)
public class McpWebMvcServerAutoConfiguration {
@Bean
@ConditionalOnMissingBean
- public WebMvcSseServerTransport webMvcSseServerTransport(ObjectMapper objectMapper,
+ public WebMvcSseServerTransportProvider webMvcSseServerTransportProvider(ObjectMapper objectMapper,
McpServerProperties serverProperties) {
- return new WebMvcSseServerTransport(objectMapper, serverProperties.getSseMessageEndpoint());
+ return new WebMvcSseServerTransportProvider(objectMapper, serverProperties.getSseMessageEndpoint());
}
@Bean
- public RouterFunction mvcMcpRouterFunction(WebMvcSseServerTransport transport) {
- return transport.getRouterFunction();
+ public RouterFunction mvcMcpRouterFunction(WebMvcSseServerTransportProvider transportProvider) {
+ return transportProvider.getRouterFunction();
}
}
diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index d69e8625c..d2faa1cbf 100644
--- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -13,8 +13,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
-
org.springframework.ai.mcp.server.autoconfigure.McpServerAutoConfiguration
-org.springframework.ai.mcp.server.autoconfigure.McpWebMvcServerAutoConfiguration
org.springframework.ai.mcp.server.autoconfigure.McpWebFluxServerAutoConfiguration
-
+org.springframework.ai.mcp.server.autoconfigure.McpWebMvcServerAutoConfiguration
diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/test/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfigurationBackwardCompatibilityIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/test/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfigurationBackwardCompatibilityIT.java
new file mode 100644
index 000000000..63fae65dd
--- /dev/null
+++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/test/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfigurationBackwardCompatibilityIT.java
@@ -0,0 +1,318 @@
+/*
+ * 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.util.List;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.server.McpAsyncServer;
+import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification;
+import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptRegistration;
+import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceRegistration;
+import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
+import io.modelcontextprotocol.server.McpSyncServer;
+import io.modelcontextprotocol.server.transport.StdioServerTransportProvider;
+import io.modelcontextprotocol.spec.McpSchema;
+import io.modelcontextprotocol.spec.McpServerTransport;
+import io.modelcontextprotocol.spec.McpServerTransportProvider;
+import io.modelcontextprotocol.spec.ServerMcpTransport;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import reactor.core.publisher.Mono;
+
+import org.springframework.ai.mcp.SyncMcpToolCallback;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+@Deprecated
+public class McpServerAutoConfigurationBackwardCompatibilityIT {
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(McpServerAutoConfiguration.class));
+
+ @Test
+ void defaultConfiguration() {
+ this.contextRunner.run(context -> {
+ assertThat(context).hasSingleBean(McpSyncServer.class);
+ assertThat(context).hasSingleBean(McpServerTransportProvider.class);
+ assertThat(context.getBean(McpServerTransportProvider.class))
+ .isInstanceOf(StdioServerTransportProvider.class);
+
+ McpServerProperties properties = context.getBean(McpServerProperties.class);
+ assertThat(properties.getName()).isEqualTo("mcp-server");
+ assertThat(properties.getVersion()).isEqualTo("1.0.0");
+ assertThat(properties.getType()).isEqualTo(McpServerProperties.ServerType.SYNC);
+ assertThat(properties.isToolChangeNotification()).isTrue();
+ assertThat(properties.isResourceChangeNotification()).isTrue();
+ assertThat(properties.isPromptChangeNotification()).isTrue();
+ });
+ }
+
+ @Test
+ void asyncConfiguration() {
+ this.contextRunner
+ .withPropertyValues("spring.ai.mcp.server.type=ASYNC", "spring.ai.mcp.server.name=test-server",
+ "spring.ai.mcp.server.version=2.0.0")
+ .run(context -> {
+ assertThat(context).hasSingleBean(McpAsyncServer.class);
+ assertThat(context).doesNotHaveBean(McpSyncServer.class);
+
+ McpServerProperties properties = context.getBean(McpServerProperties.class);
+ assertThat(properties.getName()).isEqualTo("test-server");
+ assertThat(properties.getVersion()).isEqualTo("2.0.0");
+ assertThat(properties.getType()).isEqualTo(McpServerProperties.ServerType.ASYNC);
+ });
+ }
+
+ @Test
+ void transportConfiguration() {
+ this.contextRunner.withUserConfiguration(CustomTransportConfiguration.class).run(context -> {
+ assertThat(context).hasSingleBean(McpServerTransport.class);
+ assertThat(context.getBean(McpServerTransport.class)).isInstanceOf(CustomServerTransport.class);
+ });
+ }
+
+ @Test
+ void serverNotificationConfiguration() {
+ this.contextRunner
+ .withPropertyValues("spring.ai.mcp.server.tool-change-notification=false",
+ "spring.ai.mcp.server.resource-change-notification=false")
+ .run(context -> {
+ McpServerProperties properties = context.getBean(McpServerProperties.class);
+ assertThat(properties.isToolChangeNotification()).isFalse();
+ assertThat(properties.isResourceChangeNotification()).isFalse();
+ });
+ }
+
+ // @Test
+ void invalidConfigurationThrowsException() {
+ this.contextRunner.withPropertyValues("spring.ai.mcp.server.version=invalid-version").run(context -> {
+ assertThat(context).hasFailed();
+ assertThat(context).getFailure()
+ .hasRootCauseInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Invalid version format");
+ });
+ }
+
+ @Test
+ void disabledConfiguration() {
+ this.contextRunner.withPropertyValues("spring.ai.mcp.server.enabled=false").run(context -> {
+ assertThat(context).doesNotHaveBean(McpSyncServer.class);
+ assertThat(context).doesNotHaveBean(McpAsyncServer.class);
+ assertThat(context).doesNotHaveBean(ServerMcpTransport.class);
+ });
+ }
+
+ @Test
+ void notificationConfiguration() {
+ this.contextRunner
+ .withPropertyValues("spring.ai.mcp.server.tool-change-notification=false",
+ "spring.ai.mcp.server.resource-change-notification=false",
+ "spring.ai.mcp.server.prompt-change-notification=false")
+ .run(context -> {
+ McpServerProperties properties = context.getBean(McpServerProperties.class);
+ assertThat(properties.isToolChangeNotification()).isFalse();
+ assertThat(properties.isResourceChangeNotification()).isFalse();
+ assertThat(properties.isPromptChangeNotification()).isFalse();
+ });
+ }
+
+ @Test
+ void stdioConfiguration() {
+ this.contextRunner.withPropertyValues("spring.ai.mcp.server.stdio=true").run(context -> {
+ McpServerProperties properties = context.getBean(McpServerProperties.class);
+ assertThat(properties.isStdio()).isTrue();
+ });
+ }
+
+ @Test
+ void serverCapabilitiesConfiguration() {
+ this.contextRunner.run(context -> {
+ assertThat(context).hasSingleBean(McpSchema.ServerCapabilities.Builder.class);
+ McpSchema.ServerCapabilities.Builder builder = context.getBean(McpSchema.ServerCapabilities.Builder.class);
+ assertThat(builder).isNotNull();
+ });
+ }
+
+ @Test
+ void toolRegistrationConfiguration() {
+ this.contextRunner.withUserConfiguration(TestToolConfiguration.class).run(context -> {
+ List tools = context.getBean("syncTools", List.class);
+ assertThat(tools).hasSize(2);
+ });
+ }
+
+ @Test
+ void resourceRegistrationConfiguration() {
+ this.contextRunner.withUserConfiguration(TestResourceConfiguration.class).run(context -> {
+ McpSyncServer server = context.getBean(McpSyncServer.class);
+ assertThat(server).isNotNull();
+ });
+ }
+
+ @Test
+ void promptRegistrationConfiguration() {
+ this.contextRunner.withUserConfiguration(TestPromptConfiguration.class).run(context -> {
+ McpSyncServer server = context.getBean(McpSyncServer.class);
+ assertThat(server).isNotNull();
+ });
+ }
+
+ @Test
+ void asyncToolRegistrationConfiguration() {
+ this.contextRunner.withPropertyValues("spring.ai.mcp.server.type=ASYNC")
+ .withUserConfiguration(TestToolConfiguration.class)
+ .run(context -> {
+ List tools = context.getBean("asyncTools", List.class);
+ assertThat(tools).hasSize(2);
+ });
+ }
+
+ @Test
+ void customCapabilitiesBuilder() {
+ this.contextRunner.withUserConfiguration(CustomCapabilitiesConfiguration.class).run(context -> {
+ assertThat(context).hasSingleBean(McpSchema.ServerCapabilities.Builder.class);
+ assertThat(context.getBean(McpSchema.ServerCapabilities.Builder.class))
+ .isInstanceOf(CustomCapabilitiesBuilder.class);
+ });
+ }
+
+ @Test
+ void rootsChangeConsumerConfiguration() {
+ this.contextRunner.withUserConfiguration(TestRootsChangeConfiguration.class).run(context -> {
+ McpSyncServer server = context.getBean(McpSyncServer.class);
+ assertThat(server).isNotNull();
+ });
+ }
+
+ @Configuration
+ static class TestResourceConfiguration {
+
+ @Bean
+ List testResources() {
+ return List.of();
+ }
+
+ }
+
+ @Configuration
+ static class TestPromptConfiguration {
+
+ @Bean
+ List testPrompts() {
+ return List.of();
+ }
+
+ }
+
+ @Configuration
+ static class CustomCapabilitiesConfiguration {
+
+ @Bean
+ McpSchema.ServerCapabilities.Builder customCapabilitiesBuilder() {
+ return new CustomCapabilitiesBuilder();
+ }
+
+ }
+
+ static class CustomCapabilitiesBuilder extends McpSchema.ServerCapabilities.Builder {
+
+ // Custom implementation for testing
+
+ }
+
+ @Configuration
+ static class TestToolConfiguration {
+
+ @Bean
+ List testTool() {
+ McpSyncClient mockClient = Mockito.mock(McpSyncClient.class);
+ McpSchema.Tool mockTool = Mockito.mock(McpSchema.Tool.class);
+ McpSchema.CallToolResult mockResult = Mockito.mock(McpSchema.CallToolResult.class);
+
+ Mockito.when(mockTool.name()).thenReturn("test-tool");
+ Mockito.when(mockTool.description()).thenReturn("Test Tool");
+ Mockito.when(mockClient.callTool(Mockito.any(McpSchema.CallToolRequest.class))).thenReturn(mockResult);
+ when(mockClient.getClientInfo()).thenReturn(new McpSchema.Implementation("testClient", "1.0.0"));
+
+ return List.of(new SyncMcpToolCallback(mockClient, mockTool));
+ }
+
+ }
+
+ @Configuration
+ static class TestRootsChangeConfiguration {
+
+ @Bean
+ Consumer> rootsChangeConsumer() {
+ return roots -> {
+ // Test implementation
+ };
+ }
+
+ }
+
+ static class CustomServerTransport implements McpServerTransport {
+
+ @Override
+ public Mono connect(
+ Function, Mono> messageHandler) {
+ return Mono.empty(); // Test implementation
+ }
+
+ @Override
+ public Mono sendMessage(McpSchema.JSONRPCMessage message) {
+ return Mono.empty(); // Test implementation
+ }
+
+ @Override
+ public T unmarshalFrom(Object value, TypeReference type) {
+ return null; // Test implementation
+ }
+
+ @Override
+ public void close() {
+ // Test implementation
+ }
+
+ @Override
+ public Mono closeGracefully() {
+ return Mono.empty(); // Test implementation
+ }
+
+ }
+
+ @Configuration
+ static class CustomTransportConfiguration {
+
+ @Bean
+ McpServerTransport customTransport() {
+ return new CustomServerTransport();
+ }
+
+ }
+
+}
diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/test/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/test/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfigurationIT.java
index 84d08f644..29eff9e5d 100644
--- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/test/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfigurationIT.java
+++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/test/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfigurationIT.java
@@ -16,30 +16,34 @@
package org.springframework.ai.mcp.server.autoconfigure;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
import com.fasterxml.jackson.core.type.TypeReference;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.server.McpAsyncServer;
-import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolRegistration;
-import io.modelcontextprotocol.server.McpServerFeatures.SyncToolRegistration;
-import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceRegistration;
-import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptRegistration;
+import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification;
+import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptSpecification;
+import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification;
+import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
import io.modelcontextprotocol.server.McpSyncServer;
-import io.modelcontextprotocol.server.transport.StdioServerTransport;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
+import io.modelcontextprotocol.server.transport.StdioServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema;
+import io.modelcontextprotocol.spec.McpServerTransport;
+import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.spec.ServerMcpTransport;
-import org.mockito.Mockito;
import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import reactor.core.publisher.Mono;
+
import org.springframework.ai.mcp.SyncMcpToolCallback;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import reactor.core.publisher.Mono;
-
-import java.util.List;
-import java.util.function.Consumer;
-import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@@ -53,8 +57,9 @@ public class McpServerAutoConfigurationIT {
void defaultConfiguration() {
this.contextRunner.run(context -> {
assertThat(context).hasSingleBean(McpSyncServer.class);
- assertThat(context).hasSingleBean(ServerMcpTransport.class);
- assertThat(context.getBean(ServerMcpTransport.class)).isInstanceOf(StdioServerTransport.class);
+ assertThat(context).hasSingleBean(McpServerTransportProvider.class);
+ assertThat(context.getBean(McpServerTransportProvider.class))
+ .isInstanceOf(StdioServerTransportProvider.class);
McpServerProperties properties = context.getBean(McpServerProperties.class);
assertThat(properties.getName()).isEqualTo("mcp-server");
@@ -85,8 +90,8 @@ public class McpServerAutoConfigurationIT {
@Test
void transportConfiguration() {
this.contextRunner.withUserConfiguration(CustomTransportConfiguration.class).run(context -> {
- assertThat(context).hasSingleBean(ServerMcpTransport.class);
- assertThat(context.getBean(ServerMcpTransport.class)).isInstanceOf(CustomServerTransport.class);
+ assertThat(context).hasSingleBean(McpServerTransport.class);
+ assertThat(context.getBean(McpServerTransport.class)).isInstanceOf(CustomServerTransport.class);
});
}
@@ -155,8 +160,8 @@ public class McpServerAutoConfigurationIT {
@Test
void toolRegistrationConfiguration() {
this.contextRunner.withUserConfiguration(TestToolConfiguration.class).run(context -> {
- List tools = context.getBean("syncTools", List.class);
- assertThat(tools).hasSize(1);
+ List tools = context.getBean("syncTools", List.class);
+ assertThat(tools).hasSize(2);
});
}
@@ -181,8 +186,8 @@ public class McpServerAutoConfigurationIT {
this.contextRunner.withPropertyValues("spring.ai.mcp.server.type=ASYNC")
.withUserConfiguration(TestToolConfiguration.class)
.run(context -> {
- List tools = context.getBean("asyncTools", List.class);
- assertThat(tools).hasSize(1);
+ List tools = context.getBean("asyncTools", List.class);
+ assertThat(tools).hasSize(2);
});
}
@@ -196,8 +201,8 @@ public class McpServerAutoConfigurationIT {
}
@Test
- void rootsChangeConsumerConfiguration() {
- this.contextRunner.withUserConfiguration(TestRootsChangeConfiguration.class).run(context -> {
+ void rootsChangeHandlerConfiguration() {
+ this.contextRunner.withUserConfiguration(TestRootsHandlerConfiguration.class).run(context -> {
McpSyncServer server = context.getBean(McpSyncServer.class);
assertThat(server).isNotNull();
});
@@ -207,7 +212,7 @@ public class McpServerAutoConfigurationIT {
static class TestResourceConfiguration {
@Bean
- List testResources() {
+ List testResources() {
return List.of();
}
@@ -217,7 +222,7 @@ public class McpServerAutoConfigurationIT {
static class TestPromptConfiguration {
@Bean
- List testPrompts() {
+ List testPrompts() {
return List.of();
}
@@ -259,18 +264,18 @@ public class McpServerAutoConfigurationIT {
}
@Configuration
- static class TestRootsChangeConfiguration {
+ static class TestRootsHandlerConfiguration {
@Bean
- Consumer> rootsChangeConsumer() {
- return roots -> {
+ BiConsumer> rootsChangeHandler() {
+ return (exchange, roots) -> {
// Test implementation
};
}
}
- static class CustomServerTransport implements ServerMcpTransport {
+ static class CustomServerTransport implements McpServerTransport {
@Override
public Mono connect(
@@ -304,7 +309,7 @@ public class McpServerAutoConfigurationIT {
static class CustomTransportConfiguration {
@Bean
- ServerMcpTransport customTransport() {
+ McpServerTransport customTransport() {
return new CustomServerTransport();
}
diff --git a/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java b/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java
index 5925f1ebf..8e9a682ae 100644
--- a/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java
+++ b/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java
@@ -16,17 +16,21 @@
package org.springframework.ai.mcp;
import java.util.List;
+import java.util.Map;
import io.micrometer.common.util.StringUtils;
import io.modelcontextprotocol.client.McpAsyncClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.server.McpServerFeatures;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolRegistration;
+import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.Role;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
+import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.util.CollectionUtils;
@@ -46,7 +50,7 @@ import org.springframework.util.MimeType;
*
* This helper class provides methods to:
*
- * - Convert Spring AI's {@link ToolCallback} instances to MCP tool registrations
+ * - Convert Spring AI's {@link ToolCallback} instances to MCP tool specification
* - Generate JSON schemas for tool input validation
*
*
@@ -63,12 +67,14 @@ public final class McpToolUtils {
throw new IllegalArgumentException("Prefix or toolName cannot be null or empty");
}
- String input = prefix + "-" + toolName;
+ String input = prefix + "_" + toolName;
// Replace any character that isn't alphanumeric, underscore, or hyphen with
// concatenation
String formatted = input.replaceAll("[^a-zA-Z0-9_-]", "");
+ formatted = formatted.replaceAll("-", "_");
+
// If the string is longer than 64 characters, keep the last 64 characters
if (formatted.length() > 64) {
formatted = formatted.substring(formatted.length() - 64);
@@ -86,12 +92,28 @@ public final class McpToolUtils {
* @param toolCallbacks the list of tool callbacks to convert
* @return a list of MCP synchronous tool registrations
* @see #toSyncToolRegistration(ToolCallback)
+ * @deprecated Use {@link #toSyncToolSpecification(List)} instead.
*/
+ @Deprecated
public static List toSyncToolRegistration(
List toolCallbacks) {
return toolCallbacks.stream().map(McpToolUtils::toSyncToolRegistration).toList();
}
+ /**
+ * Converts a list of Spring AI tool callbacks to MCP synchronous tool specificaiton.
+ *
+ * This method processes multiple tool callbacks in bulk, converting each one to its
+ * corresponding MCP tool registration while maintaining synchronous execution
+ * semantics.
+ * @param toolCallbacks the list of tool callbacks to convert
+ * @return a list of MCP synchronous tool specificaiton
+ */
+ public static List toSyncToolSpecification(
+ List toolCallbacks) {
+ return toolCallbacks.stream().map(McpToolUtils::toSyncToolSpecification).toList();
+ }
+
/**
* Convenience method to convert a variable number of tool callbacks to MCP
* synchronous tool registrations.
@@ -101,13 +123,34 @@ public final class McpToolUtils {
* @param toolCallbacks the tool callbacks to convert
* @return a list of MCP synchronous tool registrations
* @see #toSyncToolRegistration(List)
+ * @deprecated Use {@link #toSyncToolSpecification(ToolCallback...)} instead.
*/
+ @Deprecated
+ public static List toSyncToolRegistrations(ToolCallback... toolCallbacks) {
+ return toSyncToolRegistration(List.of(toolCallbacks));
+ }
+
+ @Deprecated
public static List toSyncToolRegistration(ToolCallback... toolCallbacks) {
return toSyncToolRegistration(List.of(toolCallbacks));
}
/**
- * Converts a Spring AI FunctionCallback to an MCP SyncToolRegistration. This enables
+ * Convenience method to convert a variable number of tool callbacks to MCP
+ * synchronous tool specification.
+ *
+ * This is a varargs wrapper around {@link #toSyncToolSpecification(List)} for easier
+ * usage when working with individual callbacks.
+ * @param toolCallbacks the tool callbacks to convert
+ * @return a list of MCP synchronous tool specificaiton
+ */
+ public static List toSyncToolSpecifications(
+ ToolCallback... toolCallbacks) {
+ return toSyncToolSpecification(List.of(toolCallbacks));
+ }
+
+ /**
+ * Converts a Spring AI ToolCallback to an MCP SyncToolRegistration. This enables
* Spring AI functions to be exposed as MCP tools that can be discovered and invoked
* by language models.
*
@@ -121,18 +164,45 @@ public final class McpToolUtils {
* specifications
*
*
- * You can use the FunctionCallback builder to create a new instance of
- * FunctionCallback using either java.util.function.Function or Method reference.
+ * You can use the ToolCallback builder to create a new instance of ToolCallback using
+ * either java.util.function.Function or Method reference.
* @param toolCallback the Spring AI function callback to convert
* @return an MCP SyncToolRegistration that wraps the function callback
* @throws RuntimeException if there's an error during the function execution
+ * @deprecated Use {@link #toSyncToolSpecification(ToolCallback)} instead.
*/
+ @Deprecated
public static McpServerFeatures.SyncToolRegistration toSyncToolRegistration(ToolCallback toolCallback) {
return toSyncToolRegistration(toolCallback, null);
}
/**
- * Converts a Spring AI FunctionCallback to an MCP SyncToolRegistration. This enables
+ * Converts a Spring AI ToolCallback to an MCP SyncToolSpecification. This enables
+ * Spring AI functions to be exposed as MCP tools that can be discovered and invoked
+ * by language models.
+ *
+ *
+ * The conversion process:
+ *
+ * - Creates an MCP Tool with the function's name and input schema
+ * - Wraps the function's execution in a SyncToolSpecification that handles the MCP
+ * protocol
+ * - Provides error handling and result formatting according to MCP
+ * specifications
+ *
+ *
+ * You can use the ToolCallback builder to create a new instance of ToolCallback using
+ * either java.util.function.Function or Method reference.
+ * @param toolCallback the Spring AI function callback to convert
+ * @return an MCP SyncToolSpecification that wraps the function callback
+ * @throws RuntimeException if there's an error during the function execution
+ */
+ public static McpServerFeatures.SyncToolSpecification toSyncToolSpecification(ToolCallback toolCallback) {
+ return toSyncToolSpecification(toolCallback, null);
+ }
+
+ /**
+ * Converts a Spring AI ToolCallback to an MCP SyncToolRegistration. This enables
* Spring AI functions to be exposed as MCP tools that can be discovered and invoked
* by language models.
*
@@ -146,13 +216,15 @@ public final class McpToolUtils {
* specifications
*
*
- * You can use the FunctionCallback builder to create a new instance of
- * FunctionCallback using either java.util.function.Function or Method reference.
+ * You can use the ToolCallback builder to create a new instance of ToolCallback using
+ * either java.util.function.Function or Method reference.
* @param toolCallback the Spring AI function callback to convert
* @param mimeType the MIME type of the output content
* @return an MCP SyncToolRegistration that wraps the function callback
* @throws RuntimeException if there's an error during the function execution
+ * @deprecated Use {@link #toSyncToolSpecification(ToolCallback, MimeType)} instead.
*/
+ @Deprecated
public static McpServerFeatures.SyncToolRegistration toSyncToolRegistration(ToolCallback toolCallback,
MimeType mimeType) {
@@ -175,6 +247,48 @@ public final class McpToolUtils {
});
}
+ /**
+ * Converts a Spring AI ToolCallback to an MCP SyncToolSpecification. This enables
+ * Spring AI functions to be exposed as MCP tools that can be discovered and invoked
+ * by language models.
+ *
+ *
+ * The conversion process:
+ *
+ * - Creates an MCP Tool with the function's name and input schema
+ * - Wraps the function's execution in a SyncToolSpecification that handles the MCP
+ * protocol
+ * - Provides error handling and result formatting according to MCP
+ * specifications
+ *
+ * @param toolCallback the Spring AI function callback to convert
+ * @param mimeType the MIME type of the output content
+ * @return an MCP SyncToolRegistration that wraps the function callback
+ * @throws RuntimeException if there's an error during the function execution
+ */
+ public static McpServerFeatures.SyncToolSpecification toSyncToolSpecification(ToolCallback toolCallback,
+ MimeType mimeType) {
+
+ var tool = new McpSchema.Tool(toolCallback.getToolDefinition().name(),
+ toolCallback.getToolDefinition().description(), toolCallback.getToolDefinition().inputSchema());
+
+ return new McpServerFeatures.SyncToolSpecification(tool, (exchange, request) -> {
+ try {
+ String callResult = toolCallback.call(ModelOptionsUtils.toJsonString(request),
+ new ToolContext(Map.of("exchange", exchange)));
+ if (mimeType != null && mimeType.toString().startsWith("image")) {
+ return new McpSchema.CallToolResult(List
+ .of(new McpSchema.ImageContent(List.of(Role.ASSISTANT), null, callResult, mimeType.toString())),
+ false);
+ }
+ return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(callResult)), false);
+ }
+ catch (Exception e) {
+ return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(e.getMessage())), true);
+ }
+ });
+ }
+
/**
* Converts a list of Spring AI tool callbacks to MCP asynchronous tool registrations.
*
@@ -185,15 +299,32 @@ public final class McpToolUtils {
* @param toolCallbacks the list of tool callbacks to convert
* @return a list of MCP asynchronous tool registrations
* @see #toAsyncToolRegistration(ToolCallback)
+ * @deprecated Use {@link #toAsyncToolSpecification(List)} instead.
*/
+ @Deprecated
public static List toAsyncToolRegistration(
List toolCallbacks) {
return toolCallbacks.stream().map(McpToolUtils::toAsyncToolRegistration).toList();
}
+ /**
+ * Converts a list of Spring AI tool callbacks to MCP asynchronous tool specificaiton.
+ *
+ * This method processes multiple tool callbacks in bulk, converting each one to its
+ * corresponding MCP tool registration while adding asynchronous execution
+ * capabilities. The resulting specifications will execute their tools on a bounded
+ * elastic scheduler.
+ * @param toolCallbacks the list of tool callbacks to convert
+ * @return a list of MCP asynchronous tool specifications
+ */
+ public static List toAsyncToolSpecifications(
+ List toolCallbacks) {
+ return toolCallbacks.stream().map(McpToolUtils::toAsyncToolSpecification).toList();
+ }
+
/**
* Convenience method to convert a variable number of tool callbacks to MCP
- * asynchronous tool registrations.
+ * asynchronous tool specifications.
*
* This is a varargs wrapper around {@link #toAsyncToolRegistration(List)} for easier
* usage when working with individual callbacks.
@@ -205,6 +336,51 @@ public final class McpToolUtils {
return toAsyncToolRegistration(List.of(toolCallbacks));
}
+ /**
+ * Convenience method to convert a variable number of tool callbacks to MCP
+ * asynchronous tool specificaiton.
+ *
+ * This is a varargs wrapper around {@link #toAsyncToolSpecifications(List)} for
+ * easier usage when working with individual callbacks.
+ * @param toolCallbacks the tool callbacks to convert
+ * @return a list of MCP asynchronous tool specifications
+ * @see #toAsyncToolSpecifications(List)
+ */
+ public static List toAsyncToolSpecifications(
+ ToolCallback... toolCallbacks) {
+ return toAsyncToolSpecifications(List.of(toolCallbacks));
+ }
+
+ /**
+ * Converts a Spring AI tool callback to an MCP asynchronous tool registration.
+ *
+ * This method enables Spring AI tools to be exposed as asynchronous MCP tools that
+ * can be discovered and invoked by language models. The conversion process:
+ *
+ * - First converts the callback to a synchronous registration
+ * - Wraps the synchronous execution in a reactive Mono
+ * - Configures execution on a bounded elastic scheduler for non-blocking
+ * operation
+ *
+ *
+ * The resulting async registration will:
+ *
+ * - Execute the tool without blocking the calling thread
+ * - Handle errors and results asynchronously
+ * - Provide backpressure through Project Reactor
+ *
+ * @param toolCallback the Spring AI tool callback to convert
+ * @return an MCP asynchronous tool registration that wraps the tool callback
+ * @see McpServerFeatures.AsyncToolRegistration
+ * @see Mono
+ * @see Schedulers#boundedElastic()
+ * @deprecated Use {@link #toAsyncToolSpecification(ToolCallback)} instead.
+ */
+ @Deprecated
+ public static McpServerFeatures.AsyncToolRegistration toAsyncToolRegistration(ToolCallback toolCallback) {
+ return toAsyncToolRegistration(toolCallback, null);
+ }
+
/**
* Converts a Spring AI tool callback to an MCP asynchronous tool registration.
*
@@ -229,8 +405,8 @@ public final class McpToolUtils {
* @see Mono
* @see Schedulers#boundedElastic()
*/
- public static McpServerFeatures.AsyncToolRegistration toAsyncToolRegistration(ToolCallback toolCallback) {
- return toAsyncToolRegistration(toolCallback, null);
+ public static McpServerFeatures.AsyncToolSpecification toAsyncToolSpecification(ToolCallback toolCallback) {
+ return toAsyncToolSpecification(toolCallback, null);
}
/**
@@ -257,7 +433,9 @@ public final class McpToolUtils {
* @see McpServerFeatures.AsyncToolRegistration
* @see Mono
* @see Schedulers#boundedElastic()
+ * @deprecated Use {@link #toAsyncToolSpecification(ToolCallback, MimeType)} instead.
*/
+ @Deprecated
public static McpServerFeatures.AsyncToolRegistration toAsyncToolRegistration(ToolCallback toolCallback,
MimeType mimeType) {
@@ -268,6 +446,41 @@ public final class McpToolUtils {
.subscribeOn(Schedulers.boundedElastic()));
}
+ /**
+ * Converts a Spring AI tool callback to an MCP asynchronous tool specification.
+ *
+ * This method enables Spring AI tools to be exposed as asynchronous MCP tools that
+ * can be discovered and invoked by language models. The conversion process:
+ *
+ * - First converts the callback to a synchronous specificaiton
+ * - Wraps the synchronous execution in a reactive Mono
+ * - Configures execution on a bounded elastic scheduler for non-blocking
+ * operation
+ *
+ *
+ * The resulting async specificaiton will:
+ *
+ * - Execute the tool without blocking the calling thread
+ * - Handle errors and results asynchronously
+ * - Provide backpressure through Project Reactor
+ *
+ * @param toolCallback the Spring AI tool callback to convert
+ * @param mimeType the MIME type of the output content
+ * @return an MCP asynchronous tool specificaiotn that wraps the tool callback
+ * @see McpServerFeatures.AsyncToolSpecification
+ * @see Schedulers#boundedElastic()
+ */
+ public static McpServerFeatures.AsyncToolSpecification toAsyncToolSpecification(ToolCallback toolCallback,
+ MimeType mimeType) {
+
+ McpServerFeatures.SyncToolSpecification syncToolSpecification = toSyncToolSpecification(toolCallback, mimeType);
+
+ return new AsyncToolSpecification(syncToolSpecification.tool(),
+ (exchange, map) -> Mono
+ .fromCallable(() -> syncToolSpecification.call().apply(new McpSyncServerExchange(exchange), map))
+ .subscribeOn(Schedulers.boundedElastic()));
+ }
+
/**
* Convenience method to get tool callbacks from multiple synchronous MCP clients.
*
diff --git a/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackTests.java b/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackTests.java
index 747ee6352..47fb55ef1 100644
--- a/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackTests.java
+++ b/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackTests.java
@@ -56,7 +56,7 @@ class SyncMcpToolCallbackTests {
var toolDefinition = callback.getToolDefinition();
- assertThat(toolDefinition.name()).isEqualTo(clientInfo.name() + "-testTool");
+ assertThat(toolDefinition.name()).isEqualTo(clientInfo.name() + "_testTool");
assertThat(toolDefinition.description()).isEqualTo("Test tool description");
}
diff --git a/mcp/common/src/test/java/org/springframework/ai/mcp/ToolUtilsDeprecatedTests.java b/mcp/common/src/test/java/org/springframework/ai/mcp/ToolUtilsDeprecatedTests.java
new file mode 100644
index 000000000..2a7c8e067
--- /dev/null
+++ b/mcp/common/src/test/java/org/springframework/ai/mcp/ToolUtilsDeprecatedTests.java
@@ -0,0 +1,178 @@
+/*
+ * 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.lang.reflect.Constructor;
+import java.lang.reflect.Modifier;
+import java.util.List;
+import java.util.Map;
+
+import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolRegistration;
+import io.modelcontextprotocol.server.McpServerFeatures.SyncToolRegistration;
+import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
+import io.modelcontextprotocol.spec.McpSchema.TextContent;
+import org.junit.jupiter.api.Test;
+import reactor.test.StepVerifier;
+
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.definition.ToolDefinition;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * @deprecated used to test backward compatbility. Replaced by the {@link ToolUtilsTests}
+ * instead
+ */
+@Deprecated
+class ToolUtilsDeprecatedTests {
+
+ @Test
+ void constructorShouldBePrivate() throws Exception {
+ Constructor constructor = McpToolUtils.class.getDeclaredConstructor();
+ assertThat(Modifier.isPrivate(constructor.getModifiers())).isTrue();
+ constructor.setAccessible(true);
+ constructor.newInstance();
+ }
+
+ @Test
+ void toSyncToolRegistrationShouldConvertSingleCallback() {
+ // Arrange
+ ToolCallback callback = createMockToolCallback("test", "success");
+
+ // Act
+ SyncToolRegistration registration = McpToolUtils.toSyncToolRegistration(callback);
+
+ // Assert
+ assertThat(registration).isNotNull();
+ assertThat(registration.tool().name()).isEqualTo("test");
+
+ CallToolResult result = registration.call().apply(Map.of());
+ TextContent content = (TextContent) result.content().get(0);
+ assertThat(content.text()).isEqualTo("success");
+ assertThat(result.isError()).isFalse();
+ }
+
+ @Test
+ void toSyncToolRegistrationShouldHandleError() {
+ // Arrange
+ ToolCallback callback = createMockToolCallback("test", new RuntimeException("error"));
+
+ // Act
+ SyncToolRegistration registration = McpToolUtils.toSyncToolRegistration(callback);
+
+ // Assert
+ assertThat(registration).isNotNull();
+ CallToolResult result = registration.call().apply(Map.of());
+ TextContent content = (TextContent) result.content().get(0);
+ assertThat(content.text()).isEqualTo("error");
+ assertThat(result.isError()).isTrue();
+ }
+
+ @Test
+ void toSyncToolRegistrationShouldConvertMultipleCallbacks() {
+ // Arrange
+ ToolCallback callback1 = createMockToolCallback("test1", "success1");
+ ToolCallback callback2 = createMockToolCallback("test2", "success2");
+
+ // Act
+ List registrations = McpToolUtils.toSyncToolRegistrations(callback1, callback2);
+
+ // Assert
+ assertThat(registrations).hasSize(2);
+ assertThat(registrations.get(0).tool().name()).isEqualTo("test1");
+ assertThat(registrations.get(1).tool().name()).isEqualTo("test2");
+ }
+
+ @Test
+ void toAsyncToolRegistrationShouldConvertSingleCallback() {
+ // Arrange
+ ToolCallback callback = createMockToolCallback("test", "success");
+
+ // Act
+ AsyncToolRegistration registration = McpToolUtils.toAsyncToolRegistration(callback);
+
+ // Assert
+ assertThat(registration).isNotNull();
+ assertThat(registration.tool().name()).isEqualTo("test");
+
+ StepVerifier.create(registration.call().apply(Map.of())).assertNext(result -> {
+ TextContent content = (TextContent) result.content().get(0);
+ assertThat(content.text()).isEqualTo("success");
+ assertThat(result.isError()).isFalse();
+ }).verifyComplete();
+ }
+
+ @Test
+ void toAsyncToolRegistrationShouldHandleError() {
+ // Arrange
+ ToolCallback callback = createMockToolCallback("test", new RuntimeException("error"));
+
+ // Act
+ AsyncToolRegistration registration = McpToolUtils.toAsyncToolRegistration(callback);
+
+ // Assert
+ assertThat(registration).isNotNull();
+ StepVerifier.create(registration.call().apply(Map.of())).assertNext(result -> {
+ TextContent content = (TextContent) result.content().get(0);
+ assertThat(content.text()).isEqualTo("error");
+ assertThat(result.isError()).isTrue();
+ }).verifyComplete();
+ }
+
+ @Test
+ void toAsyncToolRegistrationShouldConvertMultipleCallbacks() {
+ // Arrange
+ ToolCallback callback1 = createMockToolCallback("test1", "success1");
+ ToolCallback callback2 = createMockToolCallback("test2", "success2");
+
+ // Act
+ List registrations = McpToolUtils.toAsyncToolRegistration(callback1, callback2);
+
+ // Assert
+ assertThat(registrations).hasSize(2);
+ assertThat(registrations.get(0).tool().name()).isEqualTo("test1");
+ assertThat(registrations.get(1).tool().name()).isEqualTo("test2");
+ }
+
+ private ToolCallback createMockToolCallback(String name, String result) {
+ ToolCallback callback = mock(ToolCallback.class);
+ ToolDefinition definition = ToolDefinition.builder()
+ .name(name)
+ .description("Test tool")
+ .inputSchema("{}")
+ .build();
+ when(callback.getToolDefinition()).thenReturn(definition);
+ when(callback.call(anyString())).thenReturn(result);
+ return callback;
+ }
+
+ private ToolCallback createMockToolCallback(String name, RuntimeException error) {
+ ToolCallback callback = mock(ToolCallback.class);
+ ToolDefinition definition = ToolDefinition.builder()
+ .name(name)
+ .description("Test tool")
+ .inputSchema("{}")
+ .build();
+ when(callback.getToolDefinition()).thenReturn(definition);
+ when(callback.call(anyString())).thenThrow(error);
+ return callback;
+ }
+
+}
diff --git a/mcp/common/src/test/java/org/springframework/ai/mcp/ToolUtilsTests.java b/mcp/common/src/test/java/org/springframework/ai/mcp/ToolUtilsTests.java
index 18af850ad..631274a1f 100644
--- a/mcp/common/src/test/java/org/springframework/ai/mcp/ToolUtilsTests.java
+++ b/mcp/common/src/test/java/org/springframework/ai/mcp/ToolUtilsTests.java
@@ -21,8 +21,10 @@ import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Map;
-import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolRegistration;
-import io.modelcontextprotocol.server.McpServerFeatures.SyncToolRegistration;
+import io.modelcontextprotocol.server.McpAsyncServerExchange;
+import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification;
+import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
import org.junit.jupiter.api.Test;
@@ -32,12 +34,60 @@ import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class ToolUtilsTests {
+ @Test
+ void prefixedToolNameShouldConcatenateWithUnderscore() {
+ String result = McpToolUtils.prefixedToolName("prefix", "toolName");
+ assertThat(result).isEqualTo("prefix_toolName");
+ }
+
+ @Test
+ void prefixedToolNameShouldReplaceSpecialCharacters() {
+ String result = McpToolUtils.prefixedToolName("pre.fix", "tool@Name");
+ assertThat(result).isEqualTo("prefix_toolName");
+ }
+
+ @Test
+ void prefixedToolNameShouldReplaceHyphensWithUnderscores() {
+ String result = McpToolUtils.prefixedToolName("pre-fix", "tool-name");
+ assertThat(result).isEqualTo("pre_fix_tool_name");
+ }
+
+ @Test
+ void prefixedToolNameShouldTruncateLongStrings() {
+ String longPrefix = "a".repeat(40);
+ String longToolName = "b".repeat(40);
+ String result = McpToolUtils.prefixedToolName(longPrefix, longToolName);
+ assertThat(result).hasSize(64);
+ assertThat(result).endsWith("_" + longToolName);
+ }
+
+ @Test
+ void prefixedToolNameShouldThrowExceptionForNullOrEmptyInputs() {
+ assertThatThrownBy(() -> McpToolUtils.prefixedToolName(null, "toolName"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Prefix or toolName cannot be null or empty");
+
+ assertThatThrownBy(() -> McpToolUtils.prefixedToolName("", "toolName"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Prefix or toolName cannot be null or empty");
+
+ assertThatThrownBy(() -> McpToolUtils.prefixedToolName("prefix", null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Prefix or toolName cannot be null or empty");
+
+ assertThatThrownBy(() -> McpToolUtils.prefixedToolName("prefix", ""))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Prefix or toolName cannot be null or empty");
+ }
+
@Test
void constructorShouldBePrivate() throws Exception {
Constructor constructor = McpToolUtils.class.getDeclaredConstructor();
@@ -47,103 +97,94 @@ class ToolUtilsTests {
}
@Test
- void toSyncToolRegistrationShouldConvertSingleCallback() {
- // Arrange
+ void toSyncToolSpecificaitonShouldConvertSingleCallback() {
+
ToolCallback callback = createMockToolCallback("test", "success");
- // Act
- SyncToolRegistration registration = McpToolUtils.toSyncToolRegistration(callback);
+ SyncToolSpecification toolSpecification = McpToolUtils.toSyncToolSpecification(callback);
- // Assert
- assertThat(registration).isNotNull();
- assertThat(registration.tool().name()).isEqualTo("test");
+ assertThat(toolSpecification).isNotNull();
+ assertThat(toolSpecification.tool().name()).isEqualTo("test");
- CallToolResult result = registration.call().apply(Map.of());
+ CallToolResult result = toolSpecification.call().apply(mock(McpSyncServerExchange.class), Map.of());
TextContent content = (TextContent) result.content().get(0);
assertThat(content.text()).isEqualTo("success");
assertThat(result.isError()).isFalse();
}
@Test
- void toSyncToolRegistrationShouldHandleError() {
- // Arrange
+ void toSyncToolSpecificationShouldHandleError() {
ToolCallback callback = createMockToolCallback("test", new RuntimeException("error"));
- // Act
- SyncToolRegistration registration = McpToolUtils.toSyncToolRegistration(callback);
+ SyncToolSpecification toolSpecification = McpToolUtils.toSyncToolSpecification(callback);
- // Assert
- assertThat(registration).isNotNull();
- CallToolResult result = registration.call().apply(Map.of());
+ assertThat(toolSpecification).isNotNull();
+ CallToolResult result = toolSpecification.call().apply(mock(McpSyncServerExchange.class), Map.of());
TextContent content = (TextContent) result.content().get(0);
assertThat(content.text()).isEqualTo("error");
assertThat(result.isError()).isTrue();
}
@Test
- void toSyncToolRegistrationShouldConvertMultipleCallbacks() {
- // Arrange
+ void toSyncToolSpecificationShouldConvertMultipleCallbacks() {
ToolCallback callback1 = createMockToolCallback("test1", "success1");
ToolCallback callback2 = createMockToolCallback("test2", "success2");
- // Act
- List registrations = McpToolUtils.toSyncToolRegistration(callback1, callback2);
+ List toolSpecification = McpToolUtils.toSyncToolSpecifications(callback1, callback2);
- // Assert
- assertThat(registrations).hasSize(2);
- assertThat(registrations.get(0).tool().name()).isEqualTo("test1");
- assertThat(registrations.get(1).tool().name()).isEqualTo("test2");
+ assertThat(toolSpecification).hasSize(2);
+ assertThat(toolSpecification.get(0).tool().name()).isEqualTo("test1");
+ assertThat(toolSpecification.get(1).tool().name()).isEqualTo("test2");
}
@Test
- void toAsyncToolRegistrationShouldConvertSingleCallback() {
- // Arrange
+ void toAsyncToolSpecificaitonShouldConvertSingleCallback() {
ToolCallback callback = createMockToolCallback("test", "success");
- // Act
- AsyncToolRegistration registration = McpToolUtils.toAsyncToolRegistration(callback);
+ AsyncToolSpecification toolSpecification = McpToolUtils.toAsyncToolSpecification(callback);
// Assert
- assertThat(registration).isNotNull();
- assertThat(registration.tool().name()).isEqualTo("test");
+ assertThat(toolSpecification).isNotNull();
+ assertThat(toolSpecification.tool().name()).isEqualTo("test");
- StepVerifier.create(registration.call().apply(Map.of())).assertNext(result -> {
- TextContent content = (TextContent) result.content().get(0);
- assertThat(content.text()).isEqualTo("success");
- assertThat(result.isError()).isFalse();
- }).verifyComplete();
+ StepVerifier.create(toolSpecification.call().apply(mock(McpAsyncServerExchange.class), Map.of()))
+ .assertNext(result -> {
+ TextContent content = (TextContent) result.content().get(0);
+ assertThat(content.text()).isEqualTo("success");
+ assertThat(result.isError()).isFalse();
+ })
+ .verifyComplete();
}
@Test
- void toAsyncToolRegistrationShouldHandleError() {
- // Arrange
+ void toAsyncToolSpecificationShouldHandleError() {
ToolCallback callback = createMockToolCallback("test", new RuntimeException("error"));
- // Act
- AsyncToolRegistration registration = McpToolUtils.toAsyncToolRegistration(callback);
+ AsyncToolSpecification toolSpecificaiton = McpToolUtils.toAsyncToolSpecification(callback);
- // Assert
- assertThat(registration).isNotNull();
- StepVerifier.create(registration.call().apply(Map.of())).assertNext(result -> {
- TextContent content = (TextContent) result.content().get(0);
- assertThat(content.text()).isEqualTo("error");
- assertThat(result.isError()).isTrue();
- }).verifyComplete();
+ assertThat(toolSpecificaiton).isNotNull();
+ StepVerifier.create(toolSpecificaiton.call().apply(mock(McpAsyncServerExchange.class), Map.of()))
+ .assertNext(result -> {
+ TextContent content = (TextContent) result.content().get(0);
+ assertThat(content.text()).isEqualTo("error");
+ assertThat(result.isError()).isTrue();
+ })
+ .verifyComplete();
}
@Test
- void toAsyncToolRegistrationShouldConvertMultipleCallbacks() {
+ void toAsyncToolSpecificationShouldConvertMultipleCallbacks() {
// Arrange
ToolCallback callback1 = createMockToolCallback("test1", "success1");
ToolCallback callback2 = createMockToolCallback("test2", "success2");
// Act
- List registrations = McpToolUtils.toAsyncToolRegistration(callback1, callback2);
+ List toolSpecifications = McpToolUtils.toAsyncToolSpecifications(callback1, callback2);
// Assert
- assertThat(registrations).hasSize(2);
- assertThat(registrations.get(0).tool().name()).isEqualTo("test1");
- assertThat(registrations.get(1).tool().name()).isEqualTo("test2");
+ assertThat(toolSpecifications).hasSize(2);
+ assertThat(toolSpecifications.get(0).tool().name()).isEqualTo("test1");
+ assertThat(toolSpecifications.get(1).tool().name()).isEqualTo("test2");
}
private ToolCallback createMockToolCallback(String name, String result) {
@@ -154,7 +195,7 @@ class ToolUtilsTests {
.inputSchema("{}")
.build();
when(callback.getToolDefinition()).thenReturn(definition);
- when(callback.call(anyString())).thenReturn(result);
+ when(callback.call(anyString(), any())).thenReturn(result);
return callback;
}
@@ -166,7 +207,7 @@ class ToolUtilsTests {
.inputSchema("{}")
.build();
when(callback.getToolDefinition()).thenReturn(definition);
- when(callback.call(anyString())).thenThrow(error);
+ when(callback.call(anyString(), any())).thenThrow(error);
return callback;
}
diff --git a/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/tool/VertexAiGeminiPaymentTransactionToolsIT.java b/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/tool/VertexAiGeminiPaymentTransactionToolsIT.java
new file mode 100644
index 000000000..47a245e44
--- /dev/null
+++ b/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/tool/VertexAiGeminiPaymentTransactionToolsIT.java
@@ -0,0 +1,238 @@
+/*
+ * 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.vertexai.gemini.tool;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.google.cloud.vertexai.Transport;
+import com.google.cloud.vertexai.VertexAI;
+import io.micrometer.observation.ObservationRegistry;
+import org.junit.jupiter.api.RepeatedTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.chat.client.advisor.api.AdvisedRequest;
+import org.springframework.ai.chat.client.advisor.api.AdvisedResponse;
+import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisor;
+import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisorChain;
+import org.springframework.ai.model.function.FunctionCallback;
+import org.springframework.ai.model.tool.ToolCallingManager;
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
+import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver;
+import org.springframework.ai.tool.resolution.SpringBeanToolCallbackResolver;
+import org.springframework.ai.tool.resolution.StaticToolCallbackResolver;
+import org.springframework.ai.tool.resolution.ToolCallbackResolver;
+import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel;
+import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatOptions;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.support.GenericApplicationContext;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Christian Tzolov
+ */
+@SpringBootTest
+@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_PROJECT_ID", matches = ".*")
+@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_LOCATION", matches = ".*")
+public class VertexAiGeminiPaymentTransactionToolsIT {
+
+ private static final Logger logger = LoggerFactory.getLogger(VertexAiGeminiPaymentTransactionToolsIT.class);
+
+ private static final Map DATASET = Map.of(new Transaction("001"), new Status("pending"),
+ new Transaction("002"), new Status("approved"), new Transaction("003"), new Status("rejected"));
+
+ @Autowired
+ ChatClient chatClient;
+
+ @Test
+ public void paymentStatuses() {
+ // @formatter:off
+ String content = this.chatClient.prompt()
+ .advisors(new LoggingAdvisor())
+ .tools(new MyTools())
+ .user("""
+ What is the status of my payment transactions 001, 002 and 003?
+ If requred invoke the function per transaction.
+ """).call().content();
+ // @formatter:on
+ logger.info("" + content);
+
+ assertThat(content).contains("001", "002", "003");
+ assertThat(content).contains("pending", "approved", "rejected");
+ }
+
+ @RepeatedTest(5)
+ public void streamingPaymentStatuses() {
+
+ Flux streamContent = this.chatClient.prompt()
+ .advisors(new LoggingAdvisor())
+ .tools(new MyTools())
+ .user("""
+ What is the status of my payment transactions 001, 002 and 003?
+ If requred invoke the function per transaction.
+ """)
+ .stream()
+ .content();
+
+ String content = streamContent.collectList().block().stream().collect(Collectors.joining());
+
+ logger.info(content);
+
+ assertThat(content).contains("001", "002", "003");
+ assertThat(content).contains("pending", "approved", "rejected");
+
+ // Quota rate
+ try {
+ Thread.sleep(1000);
+ }
+ catch (InterruptedException e) {
+ }
+ }
+
+ record TransactionStatusResponse(String id, String status) {
+
+ }
+
+ private static class LoggingAdvisor implements CallAroundAdvisor {
+
+ private final Logger logger = LoggerFactory.getLogger(LoggingAdvisor.class);
+
+ @Override
+ public String getName() {
+ return this.getClass().getSimpleName();
+ }
+
+ @Override
+ public int getOrder() {
+ return 0;
+ }
+
+ @Override
+ public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
+ var response = chain.nextAroundCall(before(advisedRequest));
+ observeAfter(response);
+ return response;
+ }
+
+ private AdvisedRequest before(AdvisedRequest request) {
+ logger.info("System text: \n" + request.systemText());
+ logger.info("System params: " + request.systemParams());
+ logger.info("User text: \n" + request.userText());
+ logger.info("User params:" + request.userParams());
+ logger.info("Function names: " + request.functionNames());
+
+ logger.info("Options: " + request.chatOptions().toString());
+
+ return request;
+ }
+
+ private void observeAfter(AdvisedResponse advisedResponse) {
+ logger.info("Response: " + advisedResponse.response());
+ }
+
+ }
+
+ record Transaction(String id) {
+ }
+
+ record Status(String name) {
+ }
+
+ record Transactions(List transactions) {
+ }
+
+ record Statuses(List statuses) {
+ }
+
+ public static class MyTools {
+
+ @Tool(description = "Get the list statuses of a list of payment transactions")
+ public Statuses paymentStatuses(Transactions transactions) {
+ logger.info("Transactions: " + transactions);
+ return new Statuses(transactions.transactions().stream().map(t -> DATASET.get(t)).toList());
+ }
+
+ }
+
+ @SpringBootConfiguration
+ public static class TestConfiguration {
+
+ @Bean
+ public ChatClient chatClient(VertexAiGeminiChatModel chatModel) {
+ return ChatClient.builder(chatModel).build();
+ }
+
+ @Bean
+ public VertexAI vertexAiApi() {
+
+ String projectId = System.getenv("VERTEX_AI_GEMINI_PROJECT_ID");
+ String location = System.getenv("VERTEX_AI_GEMINI_LOCATION");
+
+ return new VertexAI.Builder().setLocation(location)
+ .setProjectId(projectId)
+ .setTransport(Transport.REST)
+ // .setTransport(Transport.GRPC)
+ .build();
+ }
+
+ @Bean
+ public VertexAiGeminiChatModel vertexAiChatModel(VertexAI vertexAi, ToolCallingManager toolCallingManager) {
+
+ return VertexAiGeminiChatModel.builder()
+ .vertexAI(vertexAi)
+ .toolCallingManager(toolCallingManager)
+ .defaultOptions(VertexAiGeminiChatOptions.builder()
+ .model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH)
+ .temperature(0.1)
+ .build())
+ .build();
+ }
+
+ @Bean
+ ToolCallingManager toolCallingManager(GenericApplicationContext applicationContext,
+ List toolCallbacks, ObjectProvider observationRegistry) {
+
+ var staticToolCallbackResolver = new StaticToolCallbackResolver(toolCallbacks);
+ var springBeanToolCallbackResolver = SpringBeanToolCallbackResolver.builder()
+ .applicationContext(applicationContext)
+ .build();
+
+ ToolCallbackResolver toolCallbackResolver = new DelegatingToolCallbackResolver(
+ List.of(staticToolCallbackResolver, springBeanToolCallbackResolver));
+
+ return ToolCallingManager.builder()
+ .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
+ .toolCallbackResolver(toolCallbackResolver)
+ .toolExecutionExceptionProcessor(new DefaultToolExecutionExceptionProcessor(false))
+ .build();
+ }
+
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index b790468f2..fb2443abe 100644
--- a/pom.xml
+++ b/pom.xml
@@ -291,7 +291,7 @@
4.12.0
- 0.8.0-SNAPSHOT
+ 0.8.0
4.13.1
diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-client-boot-starter-docs.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-client-boot-starter-docs.adoc
index 1a05a805b..26f518d2e 100644
--- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-client-boot-starter-docs.adoc
+++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-client-boot-starter-docs.adoc
@@ -78,6 +78,10 @@ The common properties are prefixed with `spring.ai.mcp.client`:
|`root-change-notification`
|Enable/disable root change notifications for all clients
|`true`
+
+|`toolcallback.enabled`
+|Enable/disable the MCP tool callback integration with Spring AI's tool execution framework
+|`false`
|===
=== Stdio Transport Properties
@@ -302,7 +306,7 @@ The auto-configuration supports multiple transport types:
=== Integration with Spring AI
-The starter automatically configures tool callbacks that integrate with Spring AI's tool execution framework, allowing MCP tools to be used as part of AI interactions.
+The starter can configure tool callbacks that integrate with Spring AI's tool execution framework, allowing MCP tools to be used as part of AI interactions. This integration is opt-in and must be explicitly enabled with the `spring.ai.mcp.client.toolcallback.enabled=true` property.
== Usage Example
@@ -351,8 +355,7 @@ private List mcpSyncClients; // For sync client
private List mcpAsyncClients; // For async client
----
-Additionally, the registered MCP Tools with all MCP clients are provided as a list of ToolCallback
-through a ToolCallbackProvider instance:
+When tool callbacks are enabled, the registered MCP Tools with all MCP clients are provided as a ToolCallbackProvider instance:
[source,java]
----
@@ -361,6 +364,18 @@ private SyncMcpToolCallbackProvider toolCallbackProvider;
ToolCallback[] toolCallbacks = toolCallbackProvider.getToolCallbacks();
----
+Note that the tool callback functionality is disabled by default and must be explicitly enabled with:
+
+[source,yaml]
+----
+spring:
+ ai:
+ mcp:
+ client:
+ toolcallback:
+ enabled: true
+----
+
== Example Applications
- link:https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/web-search/brave-chatbot[Brave Wet Search Chatbot] - A chatbot that uses the Model Context Protocol to interact with a web search server.
diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-helpers.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-helpers.adoc
index ce756511f..bfa1f728b 100644
--- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-helpers.adoc
+++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-helpers.adoc
@@ -88,9 +88,9 @@ Flux callbacks = AsyncMcpToolCallbackProvider.asyncToolCallbacks(c
== McpToolUtils
-=== ToolCallbacks to ToolRegistrations
+=== ToolCallbacks to ToolSpecifications
-Converting Spring AI tool callbacks to MCP tool registrations:
+Converting Spring AI tool callbacks to MCP tool specifications:
[tabs]
======
@@ -99,15 +99,15 @@ Sync::
[source,java]
----
List toolCallbacks = // obtain tool callbacks
-List syncToolRegs = McpToolUtils.toSyncToolRegistration(toolCallbacks);
+List syncToolSpecs = McpToolUtils.toSyncToolSpecifications(toolCallbacks);
----
+
-then you can use the `McpServer.SyncSpec` to register the tool registrations:
+then you can use the `McpServer.SyncSpecification` to register the tool specifications:
+
[source,java]
----
-McpServer.SyncSpec syncSpec = ...
-syncSpec.tools(syncToolRegs);
+McpServer.SyncSpecification syncSpec = ...
+syncSpec.tools(syncToolSpecs);
----
Async::
@@ -115,15 +115,15 @@ Async::
[source,java]
----
List toolCallbacks = // obtain tool callbacks
-List asyncToolRegs = McpToolUtils.toAsyncToolRegistration(toolCallbacks);
+List asyncToolSpecificaitons = McpToolUtils.toAsyncToolSpecifications(toolCallbacks);
----
+
-then you can use the `McpServer.AsyncSpec` to register the tool registrations:
+then you can use the `McpServer.AsyncSpecification` to register the tool specifications:
+
[source,java]
----
-McpServer.AsyncSpec asyncSpec = ...
-asyncSpec.tools(asyncToolRegs);
+McpServer.AsyncSpecification asyncSpec = ...
+asyncSpec.tools(asyncToolSpecificaitons);
----
======
diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-overview.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-overview.adoc
index f31200ebf..5f038bf16 100644
--- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-overview.adoc
+++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-overview.adoc
@@ -8,6 +8,13 @@ The link:https://modelcontextprotocol.io/sdk/java[MCP Java SDK] provides a Java
`**Spring AI MCP**` extends the MCP Java SDK with Spring Boot integration, providing both xref:api/mcp/mcp-client-boot-starter-docs.adoc[client] and xref:api/mcp/mcp-server-boot-starter-docs.adoc[server] starters.
Bootstrap your AI applications with MCP support using link:https://start.spring.io[Spring Initializer].
+[NOTE]
+====
+Breaking Changes in MCP Java SDK 0.8.0 ⚠️
+
+MCP Java SDK version 0.8.0 introduces several breaking changes including a new session-based architecture. If you're upgrading from Java SDK 0.7.0, please refer to the https://github.com/modelcontextprotocol/java-sdk/blob/main/migration-0.8.0.md[Migration Guide] for detailed instructions.
+====
+
== MCP Java SDK Architecture
TIP: This section provides an overview for the link:https://modelcontextprotocol.io/sdk/java[MCP Java SDK architecture].
diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-server-boot-starter-docs.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-server-boot-starter-docs.adoc
index c8ffe56fc..eafc751a7 100644
--- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-server-boot-starter-docs.adoc
+++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-server-boot-starter-docs.adoc
@@ -7,7 +7,7 @@ The MCP Server Boot Starter offers:
* Automatic configuration of MCP server components
* Support for both synchronous and asynchronous operation modes
* Multiple transport layer options
-* Flexible tool, resource, and prompt registration
+* Flexible tool, resource, and prompt specification
* Change notification capabilities
== Starters
@@ -32,7 +32,7 @@ Full MCP Server features support with `STDIO` server transport.
The starter activates the `McpServerAutoConfiguration` auto-configuration responsible for:
* Configuring the basic server components
-* Handling tool, resource, and prompt registrations
+* Handling tool, resource, and prompt specifications
* Managing server capabilities and change notifications
* Providing both sync and async server implementations
@@ -50,7 +50,7 @@ Full MCP Server features support with `SSE` (Server-Sent Events) server transpor
The starter activates the `McpWebMvcServerAutoConfiguration` and `McpServerAutoConfiguration` auto-configurations to provide:
-* HTTP-based transport using Spring MVC (`WebMvcSseServerTransport`)
+* HTTP-based transport using Spring MVC (`WebMvcSseServerTransportProvider`)
* Automatically configured SSE endpoints
* Optional `STDIO` transport (enabled by setting `spring.ai.mcp.server.stdio=true`)
* Included `spring-boot-starter-web` and `mcp-spring-webmvc` dependencies
@@ -69,7 +69,7 @@ Full MCP Server features support with `SSE` (Server-Sent Events) server transpor
The starter activates the `McpWebFluxServerAutoConfiguration` and `McpServerAutoConfiguration` auto-configurations to provide:
-* Reactive transport using Spring WebFlux (`WebFluxSseServerTransport`)
+* Reactive transport using Spring WebFlux (`WebFluxSseServerTransportProvider`)
* Automatically configured reactive SSE endpoints
* Optional `STDIO` transport (enabled by setting `spring.ai.mcp.server.stdio=true`)
* Included `spring-boot-starter-webflux` and `mcp-spring-webflux` dependencies
@@ -89,7 +89,7 @@ All properties are prefixed with `spring.ai.mcp.server`:
|`resource-change-notification` |Enable resource change notifications |`true`
|`prompt-change-notification` |Enable prompt change notifications |`true`
|`tool-change-notification` |Enable tool change notifications |`true`
-|`tool-response-mime-type` |(optinal) response MIME type per tool name. For example `spring.ai.mcp.server.tool-response-mime-type.generateImage=image/png` will assosiate the `image/png` mime type with the `generateImage()` tool name |`-`
+|`tool-response-mime-type` |(optional) response MIME type per tool name. For example `spring.ai.mcp.server.tool-response-mime-type.generateImage=image/png` will associate the `image/png` mime type with the `generateImage()` tool name |`-`
|`sse-message-endpoint` |SSE endpoint path for web transport |`/mcp/message`
|===
@@ -98,11 +98,11 @@ All properties are prefixed with `spring.ai.mcp.server`:
* **Synchronous Server** - The default server type implemented using `McpSyncServer`.
It is designed for straightforward request-response patterns in your applications.
To enable this server type, set `spring.ai.mcp.server.type=SYNC` in your configuration.
-When activated, it automatically handles the configuration of synchronous tool registrations.
+When activated, it automatically handles the configuration of synchronous tool specifications.
* **Asynchronous Server** - The asynchronous server implementation uses `McpAsyncServer` and is optimized for non-blocking operations.
To enable this server type, configure your application with `spring.ai.mcp.server.type=ASYNC`.
-This server type automatically sets up asynchronous tool registrations with built-in Project Reactor support.
+This server type automatically sets up asynchronous tool specifications with built-in Project Reactor support.
== Transport Options
@@ -115,14 +115,14 @@ The MCP Server supports three transport mechanisms, each with its dedicated star
== Features and Capabilities
The MCP Server Boot Starter allows servers to expose tools, resources, and prompts to clients.
-It automatically converts custom capability handlers registered as Spring beans to sync/async registrations based on server type:
+It automatically converts custom capability handlers registered as Spring beans to sync/async specifications based on server type:
=== link:https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/[Tools]
Allows servers to expose tools that can be invoked by language models. The MCP Server Boot Starter provides:
* Change notification support
-* Tools are automatically converted to sync/async registrations based on server type
-* Automatic tool registration through Spring beans:
+* Tools are automatically converted to sync/async specifications based on server type
+* Automatic tool specification through Spring beans:
[source,java]
----
@@ -138,8 +138,8 @@ or using the low-level API:
[source,java]
----
@Bean
-public List myTools(...) {
- List tools = ...
+public List myTools(...) {
+ List tools = ...
return tools;
}
----
@@ -148,18 +148,18 @@ public List myTools(...) {
Provides a standardized way for servers to expose resources to clients.
-* Static and dynamic resource registration
+* Static and dynamic resource specifications
* Optional change notifications
* Support for resource templates
-* Automatic conversion between sync/async resource registrations
-* Automatic resource registration through Spring beans:
+* Automatic conversion between sync/async resource specifications
+* Automatic resource specification through Spring beans:
[source,java]
----
@Bean
-public List myResources(...) {
+public List myResources(...) {
var systemInfoResource = new McpSchema.Resource(...);
- var resourceRegistration = new McpServerFeatures.SyncResourceRegistration(systemInfoResource, request -> {
+ var resourceSpecification = new McpServerFeatures.SyncResourceSpecification(systemInfoResource, (exchange, request) -> {
try {
var systemInfo = Map.of(...);
String jsonContent = new ObjectMapper().writeValueAsString(systemInfo);
@@ -171,7 +171,7 @@ public List myResources(...) {
}
});
- return List.of(resourceRegistration);
+ return List.of(resourceSpecification);
}
----
@@ -181,24 +181,24 @@ Provides a standardized way for servers to expose prompt templates to clients.
* Change notification support
* Template versioning
-* Automatic conversion between sync/async prompt registrations
-* Automatic prompt registration through Spring beans:
+* Automatic conversion between sync/async prompt specifications
+* Automatic prompt specification through Spring beans:
[source,java]
----
@Bean
-public List myPrompts() {
+public List myPrompts() {
var prompt = new McpSchema.Prompt("greeting", "A friendly greeting prompt",
List.of(new McpSchema.PromptArgument("name", "The name to greet", true)));
- var promptRegistration = new McpServerFeatures.SyncPromptRegistration(prompt, getPromptRequest -> {
+ var promptSpecification = new McpServerFeatures.SyncPromptSpecification(prompt, (exchange, getPromptRequest) -> {
String nameArgument = (String) getPromptRequest.arguments().get("name");
if (nameArgument == null) { nameArgument = "friend"; }
var userMessage = new PromptMessage(Role.USER, new TextContent("Hello " + nameArgument + "! How can I assist you today?"));
return new GetPromptResult("A personalized greeting message", List.of(userMessage));
});
- return List.of(promptRegistration);
+ return List.of(promptSpecification);
}
----
@@ -213,8 +213,8 @@ When roots change, clients that support `listChanged` send a Root Change notific
[source,java]
----
@Bean
-public Consumer> rootsChangeConsumer() {
- return roots -> {
+public BiConsumer> rootsChangeHandler() {
+ return (exchange, roots) -> {
logger.info("Registering root resources: {}", roots);
};
}