+ * This class acts as a bridge between the Model Context Protocol (MCP) and Spring AI's
+ * tool system, allowing MCP tools to be used seamlessly within Spring AI applications.
+ * It:
+ *
arguments = ModelOptionsUtils.jsonToMap(functionInput);
+ CallToolResult response = this.mcpClient
+ .callTool(new CallToolRequest(this.getToolDefinition().name(), arguments));
+ return ModelOptionsUtils.toJsonString(response.content());
+ }
+
+}
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
new file mode 100644
index 00000000000..e7f3b2319c0
--- /dev/null
+++ b/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java
@@ -0,0 +1,201 @@
+/*
+* Copyright 2025 - 2025 the original author or authors.
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* https://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+package org.springframework.ai.mcp;
+
+import java.util.List;
+
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.server.McpServerFeatures;
+import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolRegistration;
+import io.modelcontextprotocol.spec.McpSchema;
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Schedulers;
+
+import org.springframework.ai.model.ModelOptionsUtils;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Utility class that provides helper methods for working with Model Context Protocol
+ * (MCP) tools in a Spring AI environment. This class facilitates the integration between
+ * Spring AI's tool callbacks and MCP's tool system.
+ *
+ *
+ * The MCP tool system enables servers to expose executable functionality to language
+ * models, allowing them to interact with external systems, perform computations, and take
+ * actions in the real world. Each tool is uniquely identified by a name and includes
+ * metadata describing its schema.
+ *
+ *
+ * This helper class provides methods to:
+ *
+ * - Convert Spring AI's {@link ToolCallback} instances to MCP tool registrations
+ * - Generate JSON schemas for tool input validation
+ *
+ *
+ * @author Christian Tzolov
+ */
+public final class McpToolUtils {
+
+ private McpToolUtils() {
+ }
+
+ /**
+ * Converts a list of Spring AI tool callbacks to MCP synchronous tool registrations.
+ *
+ * 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 registrations
+ * @see #toSyncToolRegistration(ToolCallback)
+ */
+ public static List toSyncToolRegistration(
+ List toolCallbacks) {
+ return toolCallbacks.stream().map(McpToolUtils::toSyncToolRegistration).toList();
+ }
+
+ /**
+ * Convenience method to convert a variable number of tool callbacks to MCP
+ * synchronous tool registrations.
+ *
+ * This is a varargs wrapper around {@link #toSyncToolRegistration(List)} for easier
+ * usage when working with individual callbacks.
+ * @param toolCallbacks the tool callbacks to convert
+ * @return a list of MCP synchronous tool registrations
+ * @see #toSyncToolRegistration(List)
+ */
+ public static List toSyncToolRegistration(ToolCallback... toolCallbacks) {
+ return toSyncToolRegistration(List.of(toolCallbacks));
+ }
+
+ /**
+ * Converts a Spring AI FunctionCallback to an MCP SyncToolRegistration. 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 SyncToolRegistration that handles the MCP
+ * protocol
+ * - Provides error handling and result formatting according to MCP
+ * specifications
+ *
+ *
+ * You can use the FunctionCallback builder to create a new instance of
+ * FunctionCallback 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
+ */
+ public static McpServerFeatures.SyncToolRegistration toSyncToolRegistration(ToolCallback toolCallback) {
+ var tool = new McpSchema.Tool(toolCallback.getToolDefinition().name(),
+ toolCallback.getToolDefinition().description(), toolCallback.getToolDefinition().inputSchema());
+
+ return new McpServerFeatures.SyncToolRegistration(tool, request -> {
+ try {
+ String callResult = toolCallback.call(ModelOptionsUtils.toJsonString(request));
+ 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.
+ *
+ * This method processes multiple tool callbacks in bulk, converting each one to its
+ * corresponding MCP tool registration while adding asynchronous execution
+ * capabilities. The resulting registrations 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 registrations
+ * @see #toAsyncToolRegistration(ToolCallback)
+ */
+ public static List toAsyncToolRegistration(
+ List toolCallbacks) {
+ return toolCallbacks.stream().map(McpToolUtils::toAsyncToolRegistration).toList();
+ }
+
+ /**
+ * Convenience method to convert a variable number of tool callbacks to MCP
+ * asynchronous tool registrations.
+ *
+ * This is a varargs wrapper around {@link #toAsyncToolRegistration(List)} for easier
+ * usage when working with individual callbacks.
+ * @param toolCallbacks the tool callbacks to convert
+ * @return a list of MCP asynchronous tool registrations
+ * @see #toAsyncToolRegistration(List)
+ */
+ public static List toAsyncToolRegistration(ToolCallback... toolCallbacks) {
+ return toAsyncToolRegistration(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()
+ */
+ public static McpServerFeatures.AsyncToolRegistration toAsyncToolRegistration(ToolCallback toolCallback) {
+
+ McpServerFeatures.SyncToolRegistration syncToolRegistration = toSyncToolRegistration(toolCallback);
+ if (syncToolRegistration == null) {
+ return null;
+ }
+ return new AsyncToolRegistration(syncToolRegistration.tool(),
+ map -> Mono.fromCallable(() -> syncToolRegistration.call().apply(map))
+ .subscribeOn(Schedulers.boundedElastic()));
+ }
+
+ public static List getToolCallbacks(McpSyncClient... mcpClients) {
+ return getToolCallbacks(List.of(mcpClients));
+ }
+
+ public static List getToolCallbacks(List mcpClients) {
+
+ if (CollectionUtils.isEmpty(mcpClients)) {
+ return List.of();
+ }
+ return mcpClients.stream()
+ .map(mcpClient -> List.of((new SyncMcpToolCallbackProvider(mcpClient).getToolCallbacks())))
+ .flatMap(List::stream)
+ .toList();
+ }
+
+}
diff --git a/mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallbackProvider.java b/mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallbackProvider.java
new file mode 100644
index 00000000000..4ac9e2daab0
--- /dev/null
+++ b/mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallbackProvider.java
@@ -0,0 +1,121 @@
+/*
+* Copyright 2025 - 2025 the original author or authors.
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* https://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+package org.springframework.ai.mcp;
+
+import java.util.List;
+
+import io.modelcontextprotocol.client.McpAsyncClient;
+import io.modelcontextprotocol.client.McpSyncClient;
+
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.ToolCallbackProvider;
+import org.springframework.ai.tool.util.ToolUtils;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Implementation of {@link ToolCallbackProvider} that discovers and provides MCP tools.
+ *
+ * This class acts as a tool provider for Spring AI, automatically discovering tools from
+ * an MCP server and making them available as Spring AI tools. It:
+ *
+ * - Connects to an MCP server through a sync client
+ * - Lists and retrieves available tools from the server
+ * - Creates {@link McpToolCallback} instances for each discovered tool
+ * - Validates tool names to prevent duplicates
+ *
+ *
+ * Example usage:
{@code
+ * McpSyncClient mcpClient = // obtain MCP client
+ * ToolCallbackProvider provider = new McpToolCallbackProvider(mcpClient);
+ *
+ * // Get all available tools
+ * ToolCallback[] tools = provider.getToolCallbacks();
+ * }
+ *
+ * @author Christian Tzolov
+ * @since 1.0.0
+ * @see ToolCallbackProvider
+ * @see McpToolCallback
+ * @see McpSyncClient
+ */
+
+public class SyncMcpToolCallbackProvider implements ToolCallbackProvider {
+
+ private final McpSyncClient mcpClient;
+
+ /**
+ * Creates a new {@code McpToolCallbackProvider} instance.
+ * @param mcpClient the MCP client to use for discovering tools
+ */
+ public SyncMcpToolCallbackProvider(McpSyncClient mcpClient) {
+ this.mcpClient = mcpClient;
+ }
+
+ /**
+ * Discovers and returns all available tools from the MCP server.
+ *
+ * This method:
+ *
+ * - Retrieves the list of tools from the MCP server
+ * - Creates a {@link McpToolCallback} for each tool
+ * - Validates that there are no duplicate tool names
+ *
+ * @return an array of tool callbacks, one for each discovered tool
+ * @throws IllegalStateException if duplicate tool names are found
+ */
+ @Override
+ public ToolCallback[] getToolCallbacks() {
+
+ var toolCallbacks = this.mcpClient.listTools()
+ .tools()
+ .stream()
+ .map(tool -> new McpToolCallback(this.mcpClient, tool))
+ .toArray(ToolCallback[]::new);
+
+ validateToolCallbacks(toolCallbacks);
+
+ return toolCallbacks;
+
+ }
+
+ /**
+ * Validates that there are no duplicate tool names in the provided callbacks.
+ *
+ * This method ensures that each tool has a unique name, which is required for proper
+ * tool resolution and execution.
+ * @param toolCallbacks the tool callbacks to validate
+ * @throws IllegalStateException if duplicate tool names are found
+ */
+ private void validateToolCallbacks(ToolCallback[] toolCallbacks) {
+ List duplicateToolNames = ToolUtils.getDuplicateToolNames(toolCallbacks);
+ if (!duplicateToolNames.isEmpty()) {
+ throw new IllegalStateException(
+ "Multiple tools with the same name (%s)".formatted(String.join(", ", duplicateToolNames)));
+ }
+ }
+
+ public static List syncToolCallbacks(List mcpClients) {
+
+ if (CollectionUtils.isEmpty(mcpClients)) {
+ return List.of();
+ }
+ return mcpClients.stream()
+ .map(mcpClient -> List.of((new SyncMcpToolCallbackProvider(mcpClient).getToolCallbacks())))
+ .flatMap(List::stream)
+ .toList();
+ }
+
+}
diff --git a/mcp/common/src/main/resources/META-INF/spring/aot.factories b/mcp/common/src/main/resources/META-INF/spring/aot.factories
new file mode 100644
index 00000000000..dbf904981fb
--- /dev/null
+++ b/mcp/common/src/main/resources/META-INF/spring/aot.factories
@@ -0,0 +1,2 @@
+org.springframework.aot.hint.RuntimeHintsRegistrar=\
+ org.springframework.ai.mcp.McpHints
\ No newline at end of file
diff --git a/mcp/common/src/test/java/org/springframework/ai/mcp/McpToolCallbackProviderTests.java b/mcp/common/src/test/java/org/springframework/ai/mcp/McpToolCallbackProviderTests.java
new file mode 100644
index 00000000000..0fc86888df7
--- /dev/null
+++ b/mcp/common/src/test/java/org/springframework/ai/mcp/McpToolCallbackProviderTests.java
@@ -0,0 +1,99 @@
+/*
+ * 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 static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
+import io.modelcontextprotocol.spec.McpSchema.Tool;
+
+@ExtendWith(MockitoExtension.class)
+class McpToolCallbackProviderTests {
+
+ @Mock
+ private McpSyncClient mcpClient;
+
+ @Test
+ void getToolCallbacksShouldReturnEmptyArrayWhenNoTools() {
+ // Arrange
+ ListToolsResult listToolsResult = mock(ListToolsResult.class);
+ when(listToolsResult.tools()).thenReturn(List.of());
+ when(mcpClient.listTools()).thenReturn(listToolsResult);
+
+ SyncMcpToolCallbackProvider provider = new SyncMcpToolCallbackProvider(mcpClient);
+
+ // Act
+ var callbacks = provider.getToolCallbacks();
+
+ // Assert
+ assertThat(callbacks).isEmpty();
+ }
+
+ @Test
+ void getToolCallbacksShouldReturnCallbacksForEachTool() {
+ // Arrange
+ Tool tool1 = mock(Tool.class);
+ when(tool1.name()).thenReturn("tool1");
+
+ Tool tool2 = mock(Tool.class);
+ when(tool2.name()).thenReturn("tool2");
+
+ ListToolsResult listToolsResult = mock(ListToolsResult.class);
+ when(listToolsResult.tools()).thenReturn(List.of(tool1, tool2));
+ when(mcpClient.listTools()).thenReturn(listToolsResult);
+
+ SyncMcpToolCallbackProvider provider = new SyncMcpToolCallbackProvider(mcpClient);
+
+ // Act
+ var callbacks = provider.getToolCallbacks();
+
+ // Assert
+ assertThat(callbacks).hasSize(2);
+ }
+
+ @Test
+ void getToolCallbacksShouldThrowExceptionForDuplicateToolNames() {
+ // Arrange
+ Tool tool1 = mock(Tool.class);
+ when(tool1.name()).thenReturn("sameName");
+
+ Tool tool2 = mock(Tool.class);
+ when(tool2.name()).thenReturn("sameName");
+
+ ListToolsResult listToolsResult = mock(ListToolsResult.class);
+ when(listToolsResult.tools()).thenReturn(List.of(tool1, tool2));
+ when(mcpClient.listTools()).thenReturn(listToolsResult);
+
+ SyncMcpToolCallbackProvider provider = new SyncMcpToolCallbackProvider(mcpClient);
+
+ // Act & Assert
+ assertThatThrownBy(() -> provider.getToolCallbacks()).isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("Multiple tools with the same name");
+ }
+
+}
diff --git a/mcp/common/src/test/java/org/springframework/ai/mcp/McpToolCallbackTests.java b/mcp/common/src/test/java/org/springframework/ai/mcp/McpToolCallbackTests.java
new file mode 100644
index 00000000000..d0eefaf75e5
--- /dev/null
+++ b/mcp/common/src/test/java/org/springframework/ai/mcp/McpToolCallbackTests.java
@@ -0,0 +1,74 @@
+/*
+ * 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 io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
+import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
+import io.modelcontextprotocol.spec.McpSchema.Tool;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class McpToolCallbackTests {
+
+ @Mock
+ private McpSyncClient mcpClient;
+
+ @Mock
+ private Tool tool;
+
+ @Test
+ void getToolDefinitionShouldReturnCorrectDefinition() {
+ // Arrange
+ when(tool.name()).thenReturn("testTool");
+ when(tool.description()).thenReturn("Test tool description");
+
+ McpToolCallback callback = new McpToolCallback(mcpClient, tool);
+
+ // Act
+ var toolDefinition = callback.getToolDefinition();
+
+ // Assert
+ assertThat(toolDefinition.name()).isEqualTo("testTool");
+ assertThat(toolDefinition.description()).isEqualTo("Test tool description");
+ }
+
+ @Test
+ void callShouldHandleJsonInputAndOutput() {
+ // Arrange
+ when(tool.name()).thenReturn("testTool");
+ CallToolResult callResult = mock(CallToolResult.class);
+ when(mcpClient.callTool(any(CallToolRequest.class))).thenReturn(callResult);
+
+ McpToolCallback callback = new McpToolCallback(mcpClient, tool);
+
+ // Act
+ String response = callback.call("{\"param\":\"value\"}");
+
+ // Assert
+ assertThat(response).isNotNull();
+ }
+
+}
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
new file mode 100644
index 00000000000..18af850ad1f
--- /dev/null
+++ b/mcp/common/src/test/java/org/springframework/ai/mcp/ToolUtilsTests.java
@@ -0,0 +1,173 @@
+/*
+ * 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;
+
+class ToolUtilsTests {
+
+ @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.toSyncToolRegistration(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/pom.xml b/pom.xml
index c8b36b70257..3fb66c04f42 100644
--- a/pom.xml
+++ b/pom.xml
@@ -127,7 +127,11 @@
spring-ai-spring-boot-starters/spring-ai-starter-zhipuai
spring-ai-spring-boot-starters/spring-ai-starter-moonshot
+ spring-ai-spring-boot-starters/spring-ai-starter-mcp
+
spring-ai-integration-tests
+
+ mcp/common
@@ -249,6 +253,9 @@
1.5.1
0.0.6
+
+ 0.7.0-SNAPSHOT
+
3.11.0
3.1.2
@@ -850,11 +857,22 @@
+
+ Central Portal Snapshots
+ central-portal-snapshots
+ https://central.sonatype.com/repository/maven-snapshots/
+
+ false
+
+
+ true
+
+
maven-central
https://repo.maven.apache.org/maven2/
- false
+ true
true
diff --git a/spring-ai-bom/pom.xml b/spring-ai-bom/pom.xml
index fb8fee83f4e..5979f150857 100644
--- a/spring-ai-bom/pom.xml
+++ b/spring-ai-bom/pom.xml
@@ -42,6 +42,12 @@
+
+ org.springframework.ai
+ spring-ai-mcp
+ ${project.version}
+
+
org.springframework.ai
spring-ai-core
@@ -581,6 +587,12 @@
${project.version}
+
+ org.springframework.ai
+ spring-ai-mcp-spring-boot-starter
+ ${project.version}
+
+
diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/ChatClient.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/ChatClient.java
index fad1ff7c521..07bae3c30cc 100644
--- a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/ChatClient.java
+++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/ChatClient.java
@@ -35,6 +35,7 @@
import org.springframework.ai.converter.StructuredOutputConverter;
import org.springframework.ai.model.Media;
import org.springframework.ai.model.function.FunctionCallback;
+import org.springframework.ai.tool.ToolCallback;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
@@ -218,6 +219,8 @@ interface ChatClientRequestSpec {
ChatClientRequestSpec tools(FunctionCallback... toolCallbacks);
+ ChatClientRequestSpec tools(List toolCallbacks);
+
ChatClientRequestSpec tools(Object... toolObjects);
@Deprecated
@@ -283,6 +286,8 @@ interface Builder {
Builder defaultTools(FunctionCallback... toolCallbacks);
+ Builder defaultTools(List toolCallbacks);
+
Builder defaultTools(Object... toolObjects);
/**
diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java
index d6086ad9121..cb7599f0b45 100644
--- a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java
+++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java
@@ -33,6 +33,8 @@
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
+
+import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbacks;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;
@@ -853,6 +855,14 @@ public ChatClientRequestSpec tools(FunctionCallback... toolCallbacks) {
return this;
}
+ @Override
+ public ChatClientRequestSpec tools(List toolCallbacks) {
+ Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
+ Assert.noNullElements(toolCallbacks, "toolCallbacks cannot contain null elements");
+ this.functionCallbacks.addAll(toolCallbacks);
+ return this;
+ }
+
@Override
public ChatClientRequestSpec tools(Object... toolObjects) {
Assert.notNull(toolObjects, "toolObjects cannot be null");
diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/DefaultChatClientBuilder.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/DefaultChatClientBuilder.java
index 676e8f2243e..2349cd61945 100644
--- a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/DefaultChatClientBuilder.java
+++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/DefaultChatClientBuilder.java
@@ -35,6 +35,7 @@
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.model.function.FunctionCallback;
+import org.springframework.ai.tool.ToolCallback;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -160,6 +161,12 @@ public Builder defaultTools(FunctionCallback... toolCallbacks) {
return this;
}
+ @Override
+ public Builder defaultTools(List toolCallbacks) {
+ this.defaultRequest.tools(toolCallbacks);
+ return this;
+ }
+
@Override
public Builder defaultTools(Object... toolObjects) {
this.defaultRequest.tools(toolObjects);
diff --git a/spring-ai-docs/src/main/asciidoc/mcp.md b/spring-ai-docs/src/main/asciidoc/mcp.md
new file mode 100644
index 00000000000..5e41a616337
--- /dev/null
+++ b/spring-ai-docs/src/main/asciidoc/mcp.md
@@ -0,0 +1,202 @@
+# Model Context Protocol (MCP) Server
+
+The Spring AI MCP module provides integration with the Model Context Protocol, allowing you to expose your AI tools and resources through a standardized protocol. This module is particularly useful when you want to make your Spring AI tools and resources available to MCP-compatible clients.
+
+## Dependencies
+
+To use the MCP server functionality, add the following dependency to your project:
+
+```xml
+
+ org.springframework.ai
+ spring-ai-mcp-spring-boot-starter
+ ${spring-ai.version}
+
+```
+
+## Configuration Properties
+
+The MCP server can be configured using the following properties under the `spring.ai.mcp.server` prefix:
+
+| Property | Default | Description |
+|----------|---------|-------------|
+| `enabled` | `false` | Enable/disable the MCP server |
+| `name` | `"mcp-server"` | Name of the MCP server |
+| `version` | `"1.0.0"` | Version of the MCP server |
+| `type` | `SYNC` | Server type (`SYNC` or `ASYNC`) |
+| `resource-change-notification` | `true` | Enable/disable resource change notifications |
+| `tool-change-notification` | `true` | Enable/disable tool change notifications |
+| `prompt-change-notification` | `true` | Enable/disable prompt change notifications |
+| `transport` | `STDIO` | Transport type (`STDIO`, `WEBMVC`, or `WEBFLUX`) |
+| `sse-message-endpoint` | `"/mcp/message"` | Server-Sent Events (SSE) message endpoint for web transports |
+
+## Server Types
+
+The MCP server supports two operation modes:
+
+### 1. Synchronous Mode (Default)
+
+The synchronous mode is the default option, suitable for most use cases where tools and resources are accessed sequentially:
+
+```yaml
+spring:
+ ai:
+ mcp:
+ server:
+ type: SYNC
+```
+
+### 2. Asynchronous Mode
+
+The asynchronous mode is designed for reactive applications and scenarios requiring non-blocking operations:
+
+```yaml
+spring:
+ ai:
+ mcp:
+ server:
+ type: ASYNC
+```
+
+## Transport Options
+
+The MCP server supports three transport types:
+
+### 1. STDIO Transport (Default)
+
+The Standard Input/Output transport is the default option, suitable for command-line tools and local development:
+
+```yaml
+spring:
+ ai:
+ mcp:
+ server:
+ transport: STDIO
+```
+
+### 2. WebMvc Transport
+
+The WebMvc transport uses Spring MVC's Server-Sent Events (SSE) for communication:
+
+```yaml
+spring:
+ ai:
+ mcp:
+ server:
+ transport: WEBMVC
+ sse-message-endpoint: /mcp/message # Optional, defaults to /mcp/message
+```
+
+Required dependencies:
+```xml
+
+ io.modelcontextprotocol.sdk
+ mcp-spring-webmvc
+ ${mcp.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+```
+
+### 3. WebFlux Transport
+
+The WebFlux transport uses Spring WebFlux's Server-Sent Events for reactive communication:
+
+```yaml
+spring:
+ ai:
+ mcp:
+ server:
+ transport: WEBFLUX
+ sse-message-endpoint: /mcp/message # Optional, defaults to /mcp/message
+```
+
+Required dependencies:
+```xml
+
+ io.modelcontextprotocol.sdk
+ mcp-spring-webflux
+ ${mcp.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+```
+
+## Core Features
+
+The MCP server provides several core features:
+
+### Tools
+
+- Extensible tool registration system supporting both sync and async execution
+- Automatic tool discovery and registration through Spring's component scanning
+- Change notification support for tool updates
+
+### Resources
+
+- Static and dynamic resource management
+- Optional change notifications for resource updates
+- Support for both sync and async resource access
+
+### Prompts
+
+- Configurable prompt templates
+- Change notification support for template updates
+- Integration with Spring AI's prompt system
+
+## Usage Example
+
+Here's an example of configuring the MCP server with WebMvc transport and custom settings:
+
+```yaml
+spring:
+ ai:
+ mcp:
+ server:
+ enabled: true
+ name: "My AI Tools Server"
+ version: "1.0.0"
+ type: SYNC
+ transport: WEBMVC
+ sse-message-endpoint: /ai/mcp/events
+ resource-change-notification: true
+ tool-change-notification: true
+ prompt-change-notification: false
+```
+
+## Auto-configuration
+
+The MCP server auto-configuration is provided through:
+
+1. `MpcServerAutoConfiguration`: Core server configuration supporting both sync and async modes
+2. `MpcWebMvcServerAutoConfiguration`: WebMvc transport configuration (activated when WebMvc dependencies are present)
+3. `MpcWebFluxServerAutoConfiguration`: WebFlux transport configuration (activated when WebFlux dependencies are present)
+
+The auto-configuration will automatically set up the appropriate server type and transport based on your configuration and available dependencies.
+
+## Implementing Tools and Resources
+
+To expose your Spring AI tools and resources through the MCP server:
+
+1. Implement the `ToolCallback` interface for your AI tools:
+```java
+@Component
+public class MyAiTool implements ToolCallback {
+ // Implementation
+}
+```
+
+2. The auto-configuration will automatically discover and register your tools with the MCP server, converting them to either sync or async implementations based on your server type configuration.
+
+## Monitoring
+
+The MCP server provides notifications for changes in:
+- Tools (when tools are added or removed)
+- Resources (when resources are updated)
+- Prompts (when prompt templates change)
+
+You can enable/disable these notifications using the configuration properties. The notification system works with both sync and async server types, providing consistent change tracking regardless of the chosen operation mode.
diff --git a/spring-ai-spring-boot-autoconfigure/pom.xml b/spring-ai-spring-boot-autoconfigure/pom.xml
index 1ed0b73b640..94cbf519ff7 100644
--- a/spring-ai-spring-boot-autoconfigure/pom.xml
+++ b/spring-ai-spring-boot-autoconfigure/pom.xml
@@ -45,6 +45,29 @@
true
+
+
+ org.springframework.ai
+ spring-ai-mcp
+ ${project.parent.version}
+ true
+
+
+
+ io.modelcontextprotocol.sdk
+ mcp-spring-webflux
+ 0.7.0-SNAPSHOT
+ true
+
+
+
+ io.modelcontextprotocol.sdk
+ mcp-spring-webmvc
+ 0.7.0-SNAPSHOT
+ true
+
+
+
org.springframework.ai
spring-ai-openai
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/client/stdio/McpStdioClientProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/client/stdio/McpStdioClientProperties.java
new file mode 100644
index 00000000000..ccfc0e985c3
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/client/stdio/McpStdioClientProperties.java
@@ -0,0 +1,209 @@
+/*
+ * 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.autoconfigure.mcp.client.stdio;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.client.transport.ServerParameters;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.core.io.Resource;
+import org.springframework.util.Assert;
+
+/**
+ * Configuration properties for the Model Context Protocol (MCP) stdio client.
+ *
+ * This class manages configuration settings for MCP stdio client connections, including
+ * server parameters, timeouts, and connection details. It supports both direct
+ * configuration through properties and configuration through external resource files.
+ *
+ * @author Christian Tzolov
+ * @since 1.0.0
+ */
+@ConfigurationProperties(McpStdioClientProperties.CONFIG_PREFIX)
+public class McpStdioClientProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.mcp.client.stdio";
+
+ /**
+ * Enable/disable the MCP client.
+ *
+ * When set to false, the MCP client and all its components will not be initialized.
+ */
+ private boolean enabled = false;
+
+ /**
+ * The version of the MCP client instance.
+ *
+ * This version is reported to clients and used for compatibility checks.
+ */
+ private String version = "1.0.0";
+
+ /**
+ * The timeout duration for MCP client requests.
+ *
+ * Defaults to 20 seconds.
+ */
+ private Duration requestTimeout = Duration.ofSeconds(20);
+
+ /**
+ * Flag to enable/disable root change notifications.
+ *
+ * When enabled, the client will be notified of changes to the root configuration.
+ * Defaults to true.
+ */
+ private boolean rootChangeNotification = true;
+
+ /**
+ * Resource containing the MCP servers configuration.
+ *
+ * This resource should contain a JSON configuration defining the MCP servers and
+ * their parameters.
+ */
+ private Resource serversConfiguration;
+
+ /**
+ * Map of MCP stdio connections configurations.
+ *
+ * Each entry represents a named connection with its specific configuration
+ * parameters.
+ */
+ private Map stdioConnections = new HashMap<>();
+
+ /**
+ * Flag to indicate if the MCP client has to be initialized.
+ */
+ private boolean initialize = true;
+
+ public Resource getServersConfiguration() {
+ return this.serversConfiguration;
+ }
+
+ public void setServersConfiguration(Resource stdioConnectionResources) {
+ this.serversConfiguration = stdioConnectionResources;
+ }
+
+ public Map getStdioConnections() {
+ return this.stdioConnections;
+ }
+
+ public boolean isRootChangeNotification() {
+ return this.rootChangeNotification;
+ }
+
+ public void setRootChangeNotification(boolean rootChangeNotification) {
+ this.rootChangeNotification = rootChangeNotification;
+ }
+
+ public Duration getRequestTimeout() {
+ return this.requestTimeout;
+ }
+
+ public void setRequestTimeout(Duration requestTimeout) {
+ Assert.notNull(requestTimeout, "Request timeout must not be null");
+ this.requestTimeout = requestTimeout;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getVersion() {
+ return this.version;
+ }
+
+ public void setVersion(String version) {
+ Assert.hasText(version, "Version must not be empty");
+ this.version = version;
+ }
+
+ public boolean isInitialize() {
+ return this.initialize;
+ }
+
+ public void setInitialize(boolean initialize) {
+ this.initialize = initialize;
+ }
+
+ /**
+ * Record representing the parameters for an MCP server connection.
+ *
+ * Includes the command to execute, command arguments, and environment variables.
+ */
+ @JsonInclude(JsonInclude.Include.NON_ABSENT)
+ public static record Parameters(
+ /**
+ * The command to execute for the MCP server.
+ */
+ @JsonProperty("command") String command,
+ /**
+ * List of command arguments.
+ */
+ @JsonProperty("args") List args,
+ /**
+ * Map of environment variables for the server process.
+ */
+ @JsonProperty("env") Map env) {
+ }
+
+ private Map resourceToServerParameters() {
+ try {
+ Map> stdioConnection = new ObjectMapper().readValue(
+ this.serversConfiguration.getInputStream(),
+ new TypeReference