Add Spring AI MCP Integration

Adds comprehensive Model Context Protocol (MCP) integration to Spring AI, including:

Core Features:
- MCP client implementation with Spring AI tool calling capabilities
- Spring-friendly abstractions for MCP clients and servers
- Both synchronous and asynchronous MCP server operation modes
- Add MCP client autoconfiguration with support for STDIO, WebMVC and WebFlux transports
- Auto-configuration for MCP server components
- Spring Boot starter (spring-ai-starter-mcp) with WebFlux and WebMVC support
- MCP dependency management with BOM
- Add close() method to McpToolCallback for proper resource cleanup
- Add initialize flag to control MCP client initialization
- Add comprehensive integration tests and documentation for MCP client configuration

Technical Improvements:
- Split WebMvc and WebFlux configurations into separate auto-configuration classes
- Server type configurable via 'spring.ai.mcp.server.type' property (SYNC/ASYNC)
- Comprehensive test coverage including McpServerAutoConfigurationIT
- Utility classes for converting between Spring AI tools and MCP tools
- MCP SDK version management in parent pom

Reorganize MCP tool utilities and client configuration

- Rename ToolUtils to McpToolUtils for better MCP-specific naming
- Rename McpToolCallbackProvider to SyncMcpToolCallbackProvider
- Add utility methods for handling tool callbacks in McpToolUtils
- Extract client configuration logic into new McpClientDefinitions class
- Add tool callback support to ChatClient interface and implementations
- Remove redundant integration test

Introduce MCP client customization support

- Add McpSyncClientCustomizer interface for customizing MCP sync clients
- Replace McpClientDefinitions with McpSyncClientConfigurer
- Refactor MCP client initialization to support customization
- Remove redundant close() method from McpToolCallback
- Fix conditional class dependencies in WebMvc/Flux configurations

Add MCP AOT hints

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
Christian Tzolov
2025-01-31 14:33:32 +01:00
committed by Mark Pollack
parent fc2690cdf8
commit f40945bd63
32 changed files with 2514 additions and 3 deletions

87
mcp/common/pom.xml Normal file
View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>spring-ai-mcp</artifactId>
<name>Spring AI MCP Client</name>
<description>Spring Framework integration for Model Context Protocol (MCP), providing Spring AI function calling capabilities and Spring-friendly abstractions for MCP clients and MCP servers</description>
<url>https://github.com/spring-projects/spring-ai</url>
<scm>
<url>https://github.com/spring-projects/spring-ai</url>
<connection>git://github.com/spring-projects/spring-ai.git</connection>
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-bom</artifactId>
<version>${mcp.sdk.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-spring-webflux</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-spring-webmvc</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-core</artifactId>
<version>${project.parent.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2025-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.mcp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
import org.springframework.lang.Nullable;
/**
* @author Josh Long
* @since 1.0.0
*/
@SuppressWarnings("unused")
public class McpHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
var mcs = MemberCategory.values();
for (var tr : innerClasses(McpSchema.class)) {
hints.reflection().registerType(tr, mcs);
}
}
private Set<TypeReference> innerClasses(Class<?> clazz) {
var indent = new HashSet<String>();
this.findNestedClasses(clazz, indent);
return indent.stream().map(TypeReference::of).collect(Collectors.toSet());
}
private void findNestedClasses(Class<?> clazz, Set<String> indent) {
var classes = new ArrayList<Class<?>>();
classes.addAll(Arrays.asList(clazz.getDeclaredClasses()));
classes.addAll(Arrays.asList(clazz.getClasses()));
for (var nestedClass : classes) {
this.findNestedClasses(nestedClass, indent);
}
indent.addAll(classes.stream().map(Class::getName).toList());
}
}

View File

@@ -0,0 +1,28 @@
/*
* 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.McpClient;
/**
* @author Christian Tzolov
* @since 1.0.0
*/
public interface McpSyncClientCustomizer {
void customize(String name, McpClient.SyncSpec sync);
}

View File

@@ -0,0 +1,115 @@
/*
* 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.Map;
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.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
/**
* Implementation of {@link ToolCallback} that adapts MCP tools to Spring AI's tool
* interface.
* <p>
* 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:
* <ul>
* <li>Converts MCP tool definitions to Spring AI tool definitions</li>
* <li>Handles the execution of tool calls through the MCP client</li>
* <li>Manages JSON serialization/deserialization of tool inputs and outputs</li>
* </ul>
* <p>
* Example usage: <pre>{@code
* McpSyncClient mcpClient = // obtain MCP client
* Tool mcpTool = // obtain MCP tool definition
* ToolCallback callback = new McpToolCallback(mcpClient, mcpTool);
*
* // Use the tool through Spring AI's interfaces
* ToolDefinition definition = callback.getToolDefinition();
* String result = callback.call("{\"param\": \"value\"}");
* }</pre>
*
* @author Christian Tzolov
* @see ToolCallback
* @see McpSyncClient
* @see Tool
*/
public class McpToolCallback implements ToolCallback {
private final McpSyncClient mcpClient;
private final Tool tool;
/**
* Creates a new {@code McpToolCallback} instance.
* @param mcpClient the MCP client to use for tool execution
* @param tool the MCP tool definition to adapt
*/
public McpToolCallback(McpSyncClient mcpClient, Tool tool) {
this.mcpClient = mcpClient;
this.tool = tool;
}
/**
* Returns a Spring AI tool definition adapted from the MCP tool.
* <p>
* The tool definition includes:
* <ul>
* <li>The tool's name from the MCP definition</li>
* <li>The tool's description from the MCP definition</li>
* <li>The input schema converted to JSON format</li>
* </ul>
* @return the Spring AI tool definition
*/
@Override
public ToolDefinition getToolDefinition() {
return ToolDefinition.builder()
.name(this.tool.name())
.description(this.tool.description())
.inputSchema(ModelOptionsUtils.toJsonString(this.tool.inputSchema()))
.build();
}
/**
* Executes the tool with the provided input.
* <p>
* This method:
* <ol>
* <li>Converts the JSON input string to a map of arguments</li>
* <li>Calls the tool through the MCP client</li>
* <li>Converts the tool's response content to a JSON string</li>
* </ol>
* @param functionInput the tool input as a JSON string
* @return the tool's response as a JSON string
*/
@Override
public String call(String functionInput) {
Map<String, Object> arguments = ModelOptionsUtils.jsonToMap(functionInput);
CallToolResult response = this.mcpClient
.callTool(new CallToolRequest(this.getToolDefinition().name(), arguments));
return ModelOptionsUtils.toJsonString(response.content());
}
}

View File

@@ -0,0 +1,198 @@
/*
* 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.
*
* <p>
* 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.
*
* <p>
* This helper class provides methods to:
* <ul>
* <li>Convert Spring AI's {@link ToolCallback} instances to MCP tool registrations</li>
* <li>Generate JSON schemas for tool input validation</li>
* </ul>
*
* @author Christian Tzolov
*/
public final class McpToolUtils {
private McpToolUtils() {
}
/**
* Converts a list of Spring AI tool callbacks to MCP synchronous tool registrations.
* <p>
* 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<McpServerFeatures.SyncToolRegistration> toSyncToolRegistration(
List<ToolCallback> toolCallbacks) {
return toolCallbacks.stream().map(McpToolUtils::toSyncToolRegistration).toList();
}
/**
* Convenience method to convert a variable number of tool callbacks to MCP
* synchronous tool registrations.
* <p>
* 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<McpServerFeatures.SyncToolRegistration> 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.
*
* <p>
* The conversion process:
* <ul>
* <li>Creates an MCP Tool with the function's name and input schema</li>
* <li>Wraps the function's execution in a SyncToolRegistration that handles the MCP
* protocol</li>
* <li>Provides error handling and result formatting according to MCP
* specifications</li>
* </ul>
*
* 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.
* <p>
* 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<McpServerFeatures.AsyncToolRegistration> toAsyncToolRegistration(
List<ToolCallback> toolCallbacks) {
return toolCallbacks.stream().map(McpToolUtils::toAsyncToolRegistration).toList();
}
/**
* Convenience method to convert a variable number of tool callbacks to MCP
* asynchronous tool registrations.
* <p>
* 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<McpServerFeatures.AsyncToolRegistration> toAsyncToolRegistration(ToolCallback... toolCallbacks) {
return toAsyncToolRegistration(List.of(toolCallbacks));
}
/**
* Converts a Spring AI tool callback to an MCP asynchronous tool registration.
* <p>
* 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:
* <ul>
* <li>First converts the callback to a synchronous registration</li>
* <li>Wraps the synchronous execution in a reactive Mono</li>
* <li>Configures execution on a bounded elastic scheduler for non-blocking
* operation</li>
* </ul>
* <p>
* The resulting async registration will:
* <ul>
* <li>Execute the tool without blocking the calling thread</li>
* <li>Handle errors and results asynchronously</li>
* <li>Provide backpressure through Project Reactor</li>
* </ul>
* @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);
return new AsyncToolRegistration(syncToolRegistration.tool(),
map -> Mono.fromCallable(() -> syncToolRegistration.call().apply(map))
.subscribeOn(Schedulers.boundedElastic()));
}
public static List<ToolCallback> getToolCallbacks(McpSyncClient... mcpClients) {
return getToolCallbacks(List.of(mcpClients));
}
public static List<ToolCallback> getToolCallbacks(List<McpSyncClient> mcpClients) {
if (CollectionUtils.isEmpty(mcpClients)) {
return List.of();
}
return mcpClients.stream()
.map(mcpClient -> List.of((new SyncMcpToolCallbackProvider(mcpClient).getToolCallbacks())))
.flatMap(List::stream)
.toList();
}
}

View File

@@ -0,0 +1,120 @@
/*
* 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 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.
* <p>
* 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:
* <ul>
* <li>Connects to an MCP server through a sync client</li>
* <li>Lists and retrieves available tools from the server</li>
* <li>Creates {@link McpToolCallback} instances for each discovered tool</li>
* <li>Validates tool names to prevent duplicates</li>
* </ul>
* <p>
* Example usage: <pre>{@code
* McpSyncClient mcpClient = // obtain MCP client
* ToolCallbackProvider provider = new McpToolCallbackProvider(mcpClient);
*
* // Get all available tools
* ToolCallback[] tools = provider.getToolCallbacks();
* }</pre>
*
* @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.
* <p>
* This method:
* <ol>
* <li>Retrieves the list of tools from the MCP server</li>
* <li>Creates a {@link McpToolCallback} for each tool</li>
* <li>Validates that there are no duplicate tool names</li>
* </ol>
* @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.
* <p>
* 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<String> duplicateToolNames = ToolUtils.getDuplicateToolNames(toolCallbacks);
if (!duplicateToolNames.isEmpty()) {
throw new IllegalStateException(
"Multiple tools with the same name (%s)".formatted(String.join(", ", duplicateToolNames)));
}
}
public static List<ToolCallback> syncToolCallbacks(List<McpSyncClient> mcpClients) {
if (CollectionUtils.isEmpty(mcpClients)) {
return List.of();
}
return mcpClients.stream()
.map(mcpClient -> List.of((new SyncMcpToolCallbackProvider(mcpClient).getToolCallbacks())))
.flatMap(List::stream)
.toList();
}
}

View File

@@ -0,0 +1,22 @@
/*
* 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.
*/
@NonNullApi
@NonNullFields
package org.springframework.ai.mcp;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -0,0 +1,2 @@
org.springframework.aot.hint.RuntimeHintsRegistrar=\
org.springframework.ai.mcp.McpHints

View File

@@ -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");
}
}

View File

@@ -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();
}
}

View File

@@ -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<McpToolUtils> 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<SyncToolRegistration> 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<AsyncToolRegistration> 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;
}
}

20
pom.xml
View File

@@ -127,7 +127,11 @@
<module>spring-ai-spring-boot-starters/spring-ai-starter-zhipuai</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-moonshot</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-mcp</module>
<module>spring-ai-integration-tests</module>
<module>mcp/common</module>
</modules>
<organization>
@@ -249,6 +253,9 @@
<asciidoctorj-epub.version>1.5.1</asciidoctorj-epub.version>
<spring-asciidoctor-backends.version>0.0.6</spring-asciidoctor-backends.version>
<!-- MCP-->
<mcp.sdk.version>0.7.0-SNAPSHOT</mcp.sdk.version>
<!-- plugin versions -->
<maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
<maven-surefire-plugin.version>3.1.2</maven-surefire-plugin.version>
@@ -850,11 +857,22 @@
</dependencyManagement>
<repositories>
<repository>
<name>Central Portal Snapshots</name>
<id>central-portal-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>maven-central</id>
<url>https://repo.maven.apache.org/maven2/</url>
<snapshots>
<enabled>false</enabled>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>true</enabled>

View File

@@ -42,6 +42,12 @@
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-core</artifactId>
@@ -581,6 +587,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-spring-boot-starter</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -35,6 +35,7 @@ import org.springframework.ai.chat.prompt.Prompt;
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 @@ public interface ChatClient {
ChatClientRequestSpec tools(FunctionCallback... toolCallbacks);
ChatClientRequestSpec tools(List<ToolCallback> toolCallbacks);
ChatClientRequestSpec tools(Object... toolObjects);
@Deprecated
@@ -283,6 +286,8 @@ public interface ChatClient {
Builder defaultTools(FunctionCallback... toolCallbacks);
Builder defaultTools(List<ToolCallback> toolCallbacks);
Builder defaultTools(Object... toolObjects);
/**

View File

@@ -33,6 +33,8 @@ import java.util.function.Consumer;
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 class DefaultChatClient implements ChatClient {
return this;
}
@Override
public ChatClientRequestSpec tools(List<ToolCallback> 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");

View File

@@ -35,6 +35,7 @@ import org.springframework.ai.chat.model.ChatModel;
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 class DefaultChatClientBuilder implements Builder {
return this;
}
@Override
public Builder defaultTools(List<ToolCallback> toolCallbacks) {
this.defaultRequest.tools(toolCallbacks);
return this;
}
@Override
public Builder defaultTools(Object... toolObjects) {
this.defaultRequest.tools(toolObjects);

View File

@@ -95,13 +95,13 @@
* xref:api/prompt.adoc[]
* xref:api/structured-output-converter.adoc[Structured Output]
* xref:api/tools.adoc[Tool Calling]
* xref:api/model-context-protocol.adoc[Model Context Protocol (MCP)]
* xref:api/functions.adoc[Function Calling (Deprecated)]
** xref:api/function-callback.adoc[FunctionCallback API (Deprecated)]
** xref:api/tools-migration.adoc[Migrating to ToolCallback API]
* xref:api/multimodality.adoc[Multimodality]
* xref:api/etl-pipeline.adoc[]
* xref:api/testing.adoc[AI Model Evaluation]
* xref:api/model-context-protocol.adoc[Model Context Protocol (MCP)]
* Service Connections
** xref:api/docker-compose.adoc[Docker Compose]

View File

@@ -1,5 +1,5 @@
[[upgrade-notes]]
= Upgrading Notes
= Upgrade Notes
== Upgrading to 1.0.0.M6

View File

@@ -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
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
```
## 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
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-spring-webmvc</artifactId>
<version>${mcp.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
```
### 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
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-spring-webflux</artifactId>
<version>${mcp.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
```
## 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.

View File

@@ -45,6 +45,29 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-spring-webflux</artifactId>
<version>0.7.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-spring-webmvc</artifactId>
<version>0.7.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai</artifactId>

View File

@@ -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.
* <p>
* 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.
* <p>
* 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.
* <p>
* This version is reported to clients and used for compatibility checks.
*/
private String version = "1.0.0";
/**
* The timeout duration for MCP client requests.
* <p>
* Defaults to 20 seconds.
*/
private Duration requestTimeout = Duration.ofSeconds(20);
/**
* Flag to enable/disable root change notifications.
* <p>
* 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.
* <p>
* This resource should contain a JSON configuration defining the MCP servers and
* their parameters.
*/
private Resource serversConfiguration;
/**
* Map of MCP stdio connections configurations.
* <p>
* Each entry represents a named connection with its specific configuration
* parameters.
*/
private final Map<String, McpStdioConnection> 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<String, McpStdioConnection> 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.
* <p>
* Includes the command to execute, command arguments, and environment variables.
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
public record Parameters(
/**
* The command to execute for the MCP server.
*/
@JsonProperty("command") String command,
/**
* List of command arguments.
*/
@JsonProperty("args") List<String> args,
/**
* Map of environment variables for the server process.
*/
@JsonProperty("env") Map<String, String> env) {
}
private Map<String, ServerParameters> resourceToServerParameters() {
try {
Map<String, Map<String, Parameters>> stdioConnection = new ObjectMapper().readValue(
this.serversConfiguration.getInputStream(),
new TypeReference<Map<String, Map<String, Parameters>>>() {
});
Map<String, Parameters> mcpServerJsonConfig = stdioConnection.entrySet().iterator().next().getValue();
return mcpServerJsonConfig.entrySet().stream().collect(Collectors.toMap(kv -> kv.getKey(), kv -> {
Parameters parameters = kv.getValue();
return ServerParameters.builder(parameters.command())
.args(parameters.args())
.env(parameters.env())
.build();
}));
}
catch (Exception e) {
throw new RuntimeException("Failed to read stdio connection resource", e);
}
}
public Map<String, ServerParameters> toServerParameters() {
Map<String, ServerParameters> serverParameters = new HashMap<>();
if (this.serversConfiguration != null) {
serverParameters.putAll(resourceToServerParameters());
}
for (Map.Entry<String, McpStdioConnection> entry : this.stdioConnections.entrySet()) {
serverParameters.put(entry.getKey(), entry.getValue().toServerParameters());
}
return serverParameters;
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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.util.ArrayList;
import java.util.List;
import java.util.Map;
import io.modelcontextprotocol.client.transport.ServerParameters;
public class McpStdioConnection {
private String command;
private List<String> args = new ArrayList<>();
private Map<String, String> env;
public String getCommand() {
return this.command;
}
public void setCommand(String command) {
this.command = command;
}
public List<String> getArgs() {
return this.args;
}
public void setArgs(List<String> args) {
this.args = args;
}
public Map<String, String> getEnv() {
return this.env;
}
public void setEnv(Map<String, String> env) {
this.env = env;
}
public ServerParameters toServerParameters() {
return ServerParameters.builder(this.command).args(this.args).env(this.env).build();
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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.util.List;
import io.modelcontextprotocol.client.McpClient;
import org.springframework.ai.mcp.McpSyncClientCustomizer;
public class McpSyncClientConfigurer {
private List<McpSyncClientCustomizer> customizers;
void setCustomizers(List<McpSyncClientCustomizer> customizers) {
this.customizers = customizers;
}
public McpClient.SyncSpec configure(String name, McpClient.SyncSpec spec) {
applyCustomizers(name, spec);
return spec;
}
private void applyCustomizers(String name, McpClient.SyncSpec spec) {
if (this.customizers != null) {
for (McpSyncClientCustomizer customizer : this.customizers) {
customizer.customize(name, spec);
}
}
}
}

View File

@@ -0,0 +1,137 @@
/*
* 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.util.ArrayList;
import java.util.List;
import java.util.Map;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.ServerParameters;
import io.modelcontextprotocol.client.transport.StdioClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.ai.mcp.McpSyncClientCustomizer;
import org.springframework.ai.mcp.McpToolCallback;
import org.springframework.ai.mcp.McpToolUtils;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
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;
/**
* Auto-configuration for Model Context Protocol (MCP) STDIO clients.
*
* <p>
* This configuration is responsible for setting up MCP clients that communicate with MCP
* servers through standard input/output (STDIO). It creates and configures
* {@link McpSyncClient} instances based on the provided configuration properties.
*
* <p>
* The configuration is conditionally enabled when:
* <ul>
* <li>Required classes ({@link McpSchema} and {@link McpSyncClient}) are present on the
* classpath</li>
* <li>The 'spring.ai.mcp.client.stdio.enabled' property is set to 'true'</li>
* </ul>
*
* <p>
* This auto-configuration provides:
* <ul>
* <li>A {@code List<McpSyncClient>} bean configured for STDIO communication</li>
* <li>A {@link McpSyncClientConfigurer} bean for customizing the MCP sync client
* configuration</li>
* <li>A {@code List<ToolCallback>} bean containing tool callbacks from the MCP
* clients</li>
* </ul>
*
* @author Christian Tzolov
* @since 1.0.0
* @see McpStdioClientProperties
* @see McpSyncClient
* @see McpToolCallback
*/
@AutoConfiguration
@ConditionalOnClass({ McpSchema.class, McpSyncClient.class })
@EnableConfigurationProperties(McpStdioClientProperties.class)
@ConditionalOnProperty(prefix = McpStdioClientProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true")
public class MpcStdioClientAutoConfiguration {
@Bean
public List<McpSyncClient> mcpSyncClients(McpSyncClientConfigurer mcpSyncClientConfigurer,
McpStdioClientProperties clientProperties) {
List<McpSyncClient> clients = new ArrayList<>();
for (Map.Entry<String, ServerParameters> serverParameters : clientProperties.toServerParameters().entrySet()) {
var transport = new StdioClientTransport(serverParameters.getValue());
McpSchema.Implementation clientInfo = new McpSchema.Implementation(serverParameters.getKey(),
clientProperties.getVersion());
McpClient.SyncSpec syncSpec = McpClient.sync(transport)
.clientInfo(clientInfo)
.requestTimeout(clientProperties.getRequestTimeout());
syncSpec = mcpSyncClientConfigurer.configure(serverParameters.getKey(), syncSpec);
var syncClient = syncSpec.build();
if (clientProperties.isInitialize()) {
syncClient.initialize();
}
clients.add(syncClient);
}
return clients;
}
@Bean
public List<ToolCallback> toolCallbacks(List<McpSyncClient> mcpClients) {
return McpToolUtils.getToolCallbacks(mcpClients);
}
public record ClosebleMcpSyncClients(List<McpSyncClient> clients) implements AutoCloseable {
@Override
public void close() {
this.clients.forEach(McpSyncClient::close);
}
}
@Bean
public ClosebleMcpSyncClients makeThemClosable(List<McpSyncClient> clients) {
return new ClosebleMcpSyncClients(clients);
}
@Bean
@ConditionalOnMissingBean
McpSyncClientConfigurer mcpSyncClientConfigurer(ObjectProvider<McpSyncClientCustomizer> customizerProvider) {
McpSyncClientConfigurer configurer = new McpSyncClientConfigurer();
configurer.setCustomizers(customizerProvider.orderedStream().toList());
return configurer;
}
}

View File

@@ -0,0 +1,240 @@
/*
* 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.server;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.Assert;
/**
* Configuration properties for the Model Context Protocol (MCP) server.
* <p>
* These properties control the behavior and configuration of the MCP server, including:
* <ul>
* <li>Server identification (name and version)</li>
* <li>Transport type (STDIO, WEBMVC, or WEBFLUX)</li>
* <li>Change notification settings for tools, resources, and prompts</li>
* <li>Web transport endpoint configuration</li>
* </ul>
* <p>
* All properties are prefixed with {@code spring.ai.mcp.server}.
*
* @author Christian Tzolov
* @since 1.0.0
* @see org.springframework.ai.autoconfigure.mcp.server.MpcServerAutoConfiguration
*/
@ConfigurationProperties(McpServerProperties.CONFIG_PREFIX)
public class McpServerProperties {
public static final String CONFIG_PREFIX = "spring.ai.mcp.server";
/**
* Enable/disable the MCP server.
* <p>
* When set to false, the MCP server and all its components will not be initialized.
*/
private boolean enabled = false;
/**
* The name of the MCP server instance.
* <p>
* This name is used to identify the server in logs and monitoring.
*/
private String name = "mcp-server";
/**
* The version of the MCP server instance.
* <p>
* This version is reported to clients and used for compatibility checks.
*/
private String version = "1.0.0";
/**
* Enable/disable notifications for resource changes. Only relevant for MCP servers
* with resource capabilities.
* <p>
* When enabled, the server will notify clients when resources are added, updated, or
* removed.
*/
private boolean resourceChangeNotification = true;
/**
* Enable/disable notifications for tool changes. Only relevant for MCP servers with
* tool capabilities.
* <p>
* When enabled, the server will notify clients when tools are registered or
* unregistered.
*/
private boolean toolChangeNotification = true;
/**
* Enable/disable notifications for prompt changes. Only relevant for MCP servers with
* prompt capabilities.
* <p>
* When enabled, the server will notify clients when prompt templates are modified.
*/
private boolean promptChangeNotification = true;
/**
* The transport type to use for MCP server communication.
* <p>
* Supported types are:
* <ul>
* <li>STDIO - Standard input/output transport (default)</li>
* <li>WEBMVC - Spring MVC Server-Sent Events transport</li>
* <li>WEBFLUX - Spring WebFlux Server-Sent Events transport</li>
* </ul>
*/
private Transport transport = Transport.STDIO;
/**
* The endpoint path for Server-Sent Events (SSE) when using web transports.
* <p>
* This property is only used when transport is set to WEBMVC or WEBFLUX.
*/
private String sseMessageEndpoint = "/mcp/message";
/**
* The type of server to use for MCP server communication.
* <p>
* Supported types are:
* <ul>
* <li>SYNC - Standard synchronous server (default)</li>
* <li>ASYNC - Asynchronous server</li>
* </ul>
*/
private ServerType type = ServerType.SYNC;
/**
* Transport types supported by the MCP server.
*/
public enum Transport {
/**
* Standard input/output transport, suitable for command-line tools and local
* development.
*/
STDIO,
/**
* Spring MVC Server-Sent Events transport, requires spring-boot-starter-web and
* mcp-spring-webmvc.
*/
WEBMVC,
/**
* Spring WebFlux Server-Sent Events transport, requires
* spring-boot-starter-webflux and mcp-spring-webflux.
*/
WEBFLUX
}
/**
* Server types supported by the MCP server.
*/
public enum ServerType {
/**
* Synchronous (McpSyncServer) server
*/
SYNC,
/**
* Asynchronous (McpAsyncServer) server
*/
ASYNC
}
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getName() {
return this.name;
}
public void setName(String name) {
Assert.hasText(name, "Name must not be empty");
this.name = name;
}
public String getVersion() {
return this.version;
}
public void setVersion(String version) {
Assert.hasText(version, "Version must not be empty");
this.version = version;
}
public boolean isResourceChangeNotification() {
return this.resourceChangeNotification;
}
public void setResourceChangeNotification(boolean resourceChangeNotification) {
this.resourceChangeNotification = resourceChangeNotification;
}
public boolean isToolChangeNotification() {
return this.toolChangeNotification;
}
public void setToolChangeNotification(boolean toolChangeNotification) {
this.toolChangeNotification = toolChangeNotification;
}
public boolean isPromptChangeNotification() {
return this.promptChangeNotification;
}
public void setPromptChangeNotification(boolean promptChangeNotification) {
this.promptChangeNotification = promptChangeNotification;
}
public Transport getTransport() {
return this.transport;
}
public void setTransport(Transport transport) {
Assert.notNull(transport, "Transport must not be null");
this.transport = transport;
}
public String getSseMessageEndpoint() {
return this.sseMessageEndpoint;
}
public void setSseMessageEndpoint(String sseMessageEndpoint) {
Assert.hasText(sseMessageEndpoint, "SSE message endpoint must not be empty");
this.sseMessageEndpoint = sseMessageEndpoint;
}
public ServerType getType() {
return this.type;
}
public void setType(ServerType serverType) {
Assert.notNull(serverType, "Server type must not be null");
this.type = serverType;
}
}

View File

@@ -0,0 +1,229 @@
/*
* 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.server;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import io.modelcontextprotocol.server.McpServer;
import reactor.core.publisher.Mono;
import io.modelcontextprotocol.server.McpServer.SyncSpec;
import io.modelcontextprotocol.server.McpServer.AsyncSpec;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.McpServerFeatures.SyncToolRegistration;
import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolRegistration;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.McpAsyncServer;
import io.modelcontextprotocol.server.transport.StdioServerTransport;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.Implementation;
import io.modelcontextprotocol.spec.ServerMcpTransport;
import org.springframework.ai.mcp.McpToolUtils;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
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.core.log.LogAccessor;
/**
* {@link EnableAutoConfiguration Auto-configuration} for the Model Context Protocol (MCP)
* Server.
* <p>
* This configuration class sets up the core MCP server components with support for both
* synchronous and asynchronous operation modes. The server type is controlled through the
* {@code spring.ai.mcp.server.type} property, defaulting to SYNC mode.
* <p>
* Core features and capabilities include:
* <ul>
* <li>Tools: Extensible tool registration system supporting both sync and async
* execution</li>
* <li>Resources: Static and dynamic resource management with optional change
* notifications</li>
* <li>Prompts: Configurable prompt templates with change notification support</li>
* <li>Transport: Flexible transport layer with built-in support for:
* <ul>
* <li>STDIO (default): Standard input/output based communication</li>
* <li>WebMvc: HTTP-based transport when Spring MVC is available</li>
* <li>WebFlux: Reactive transport when Spring WebFlux is available</li>
* </ul>
* </li>
* </ul>
* <p>
* The configuration is activated when:
* <ul>
* <li>The required MCP classes ({@link McpSchema} and {@link McpSyncServer}) are on the
* classpath</li>
* <li>The {@code spring.ai.mcp.server.enabled} property is true (default)</li>
* </ul>
* <p>
* Server configuration is managed through {@link McpServerProperties} with support for:
* <ul>
* <li>Server identification (name, version)</li>
* <li>Transport selection</li>
* <li>Change notification settings for tools, resources, and prompts</li>
* <li>Sync/Async operation mode selection</li>
* </ul>
* <p>
* WebMvc transport support is provided separately by
* {@link MpcWebMvcServerAutoConfiguration}.
*
* @author Christian Tzolov
* @since 1.0.0
* @see McpServerProperties
* @see MpcWebMvcServerAutoConfiguration
* @see org.springframework.ai.mcp.ToolCallback
*/
@AutoConfiguration
@ConditionalOnClass({ McpSchema.class, McpSyncServer.class })
@EnableConfigurationProperties(McpServerProperties.class)
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true")
public class MpcServerAutoConfiguration {
private static final LogAccessor logger = new LogAccessor(MpcServerAutoConfiguration.class);
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "transport", havingValue = "STDIO",
matchIfMissing = true)
public ServerMcpTransport stdioServerTransport() {
return new StdioServerTransport();
}
@Bean
@ConditionalOnMissingBean
public McpSchema.ServerCapabilities.Builder capabilitiesBuilder() {
return McpSchema.ServerCapabilities.builder();
}
@Bean
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
matchIfMissing = true)
public List<McpServerFeatures.SyncToolRegistration> syncTools(List<ToolCallback> toolCalls) {
return McpToolUtils.toSyncToolRegistration(toolCalls);
}
@Bean
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
matchIfMissing = true)
public McpSyncServer mcpSyncServer(ServerMcpTransport transport,
McpSchema.ServerCapabilities.Builder capabilitiesBuilder, McpServerProperties serverProperties,
ObjectProvider<List<SyncToolRegistration>> tools,
ObjectProvider<List<McpServerFeatures.SyncResourceRegistration>> resources,
ObjectProvider<List<McpServerFeatures.SyncPromptRegistration>> prompts,
ObjectProvider<Consumer<List<McpSchema.Root>>> rootsChangeConsumers) {
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);
tools.ifAvailable(toolsList -> {
serverBuilder.tools(toolsList);
capabilitiesBuilder.tools(serverProperties.isToolChangeNotification());
logger.info("Registered tools" + toolsList.size() + " notification: "
+ serverProperties.isToolChangeNotification());
});
resources.ifAvailable(resourceList -> {
serverBuilder.resources(resourceList);
capabilitiesBuilder.resources(false, serverProperties.isResourceChangeNotification());
logger.info("Registered resources" + resourceList.size() + " notification: "
+ serverProperties.isResourceChangeNotification());
});
prompts.ifAvailable(promptList -> {
serverBuilder.prompts(promptList);
capabilitiesBuilder.prompts(serverProperties.isPromptChangeNotification());
logger.info("Registered prompts" + promptList.size() + " notification: "
+ serverProperties.isPromptChangeNotification());
});
rootsChangeConsumers.ifAvailable(consumer -> {
serverBuilder.rootsChangeConsumer(consumer);
logger.info("Registered roots change consumer");
});
serverBuilder.capabilities(capabilitiesBuilder.build());
return serverBuilder.build();
}
@Bean
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
public List<McpServerFeatures.AsyncToolRegistration> asyncTools(List<ToolCallback> toolCalls) {
return McpToolUtils.toAsyncToolRegistration(toolCalls);
}
@Bean
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
public McpAsyncServer mcpAsyncServer(ServerMcpTransport transport,
McpSchema.ServerCapabilities.Builder capabilitiesBuilder, McpServerProperties serverProperties,
ObjectProvider<List<AsyncToolRegistration>> tools,
ObjectProvider<List<McpServerFeatures.AsyncResourceRegistration>> resources,
ObjectProvider<List<McpServerFeatures.AsyncPromptRegistration>> prompts,
ObjectProvider<Consumer<List<McpSchema.Root>>> rootsChangeConsumer) {
McpSchema.Implementation serverInfo = new Implementation(serverProperties.getName(),
serverProperties.getVersion());
// Create the server with both tool and resource capabilities
AsyncSpec serverBilder = McpServer.async(transport).serverInfo(serverInfo);
tools.ifAvailable(toolsList -> {
serverBilder.tools(toolsList);
capabilitiesBuilder.tools(serverProperties.isToolChangeNotification());
logger.info("Registered tools" + toolsList.size() + " notification: "
+ serverProperties.isToolChangeNotification());
});
resources.ifAvailable(resourceList -> {
serverBilder.resources(resourceList);
capabilitiesBuilder.resources(false, serverProperties.isResourceChangeNotification());
logger.info("Registered resources" + resourceList.size() + " notification: "
+ serverProperties.isResourceChangeNotification());
});
prompts.ifAvailable(promptList -> {
serverBilder.prompts(promptList);
capabilitiesBuilder.prompts(serverProperties.isPromptChangeNotification());
logger.info("Registered prompts" + promptList.size() + " notification: "
+ serverProperties.isPromptChangeNotification());
});
rootsChangeConsumer.ifAvailable(consumer -> {
Function<List<McpSchema.Root>, Mono<Void>> asyncConsumer = roots -> {
consumer.accept(roots);
return Mono.empty();
};
serverBilder.rootsChangeConsumer(asyncConsumer);
logger.info("Registered roots change consumer");
});
serverBilder.capabilities(capabilitiesBuilder.build());
return serverBilder.build();
}
}

View File

@@ -0,0 +1,82 @@
/*
* 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.server;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.transport.WebFluxSseServerTransport;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.web.reactive.function.server.RouterFunction;
/**
* {@link AutoConfiguration Auto-configuration} for MCP WebFlux Server Transport.
* <p>
* This configuration class sets up the WebFlux-specific transport components for the MCP
* server, providing reactive Server-Sent Events (SSE) communication through Spring
* WebFlux. It is activated when:
* <ul>
* <li>The WebFluxSseServerTransport class is on the classpath (from mcp-spring-webflux
* dependency)</li>
* <li>Spring WebFlux's RouterFunction class is available (from
* spring-boot-starter-webflux)</li>
* <li>The {@code spring.ai.mcp.server.transport} property is set to {@code WEBFLUX}</li>
* </ul>
* <p>
* The configuration provides:
* <ul>
* <li>A WebFluxSseServerTransport bean for handling reactive SSE communication</li>
* <li>A RouterFunction bean that sets up the reactive SSE endpoint</li>
* </ul>
* <p>
* Required dependencies: <pre>{@code
* <dependency>
* <groupId>io.modelcontextprotocol.sdk</groupId>
* <artifactId>mcp-spring-webflux</artifactId>
* </dependency>
* <dependency>
* <groupId>org.springframework.boot</groupId>
* <artifactId>spring-boot-starter-webflux</artifactId>
* </dependency>
* }</pre>
*
* @author Christian Tzolov
* @since 1.0.0
* @see McpServerProperties
* @see WebFluxSseServerTransport
*/
@AutoConfiguration
@ConditionalOnClass({ WebFluxSseServerTransport.class })
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "transport", havingValue = "WEBFLUX")
public class MpcWebFluxServerAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public WebFluxSseServerTransport webFluxTransport(McpServerProperties serverProperties) {
return new WebFluxSseServerTransport(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();
}
}

View File

@@ -0,0 +1,82 @@
/*
* 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.server;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.transport.WebMvcSseServerTransport;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;
/**
* {@link AutoConfiguration Auto-configuration} for MCP WebMvc Server Transport.
* <p>
* This configuration class sets up the WebMvc-specific transport components for the MCP
* server, providing Server-Sent Events (SSE) communication through Spring MVC. It is
* activated when:
* <ul>
* <li>The WebMvcSseServerTransport class is on the classpath (from mcp-spring-webmvc
* dependency)</li>
* <li>Spring MVC's RouterFunction class is available (from spring-boot-starter-web)</li>
* <li>The {@code spring.ai.mcp.server.transport} property is set to {@code WEBMVC}</li>
* </ul>
* <p>
* The configuration provides:
* <ul>
* <li>A WebMvcSseServerTransport bean for handling SSE communication</li>
* <li>A RouterFunction bean that sets up the SSE endpoint</li>
* </ul>
* <p>
* Required dependencies: <pre>{@code
* <dependency>
* <groupId>io.modelcontextprotocol.sdk</groupId>
* <artifactId>mcp-spring-webmvc</artifactId>
* </dependency>
* <dependency>
* <groupId>org.springframework.boot</groupId>
* <artifactId>spring-boot-starter-web</artifactId>
* </dependency>
* }</pre>
*
* @author Christian Tzolov
* @since 1.0.0
* @see McpServerProperties
* @see WebMvcSseServerTransport
*/
@AutoConfiguration
@ConditionalOnClass({ WebMvcSseServerTransport.class })
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "transport", havingValue = "WEBMVC")
public class MpcWebMvcServerAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public WebMvcSseServerTransport webMvcSseServerTransport(ObjectMapper objectMapper,
McpServerProperties serverProperties) {
return new WebMvcSseServerTransport(objectMapper, serverProperties.getSseMessageEndpoint());
}
@Bean
public RouterFunction<ServerResponse> mvcMcpRouterFunction(WebMvcSseServerTransport transport) {
return transport.getRouterFunction();
}
}

View File

@@ -60,3 +60,8 @@ org.springframework.ai.autoconfigure.minimax.MiniMaxAutoConfiguration
org.springframework.ai.autoconfigure.vertexai.embedding.VertexAiEmbeddingAutoConfiguration
org.springframework.ai.autoconfigure.chat.memory.cassandra.CassandraChatMemoryAutoConfiguration
org.springframework.ai.autoconfigure.vectorstore.observation.VectorStoreObservationAutoConfiguration
org.springframework.ai.autoconfigure.mcp.server.MpcServerAutoConfiguration
org.springframework.ai.autoconfigure.mcp.server.MpcWebMvcServerAutoConfiguration
org.springframework.ai.autoconfigure.mcp.server.MpcWebFluxServerAutoConfiguration
org.springframework.ai.autoconfigure.mcp.client.stdio.MpcStdioClientAutoConfiguration

View File

@@ -0,0 +1,93 @@
/*
* 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.server;
import io.modelcontextprotocol.server.McpAsyncServer;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.transport.StdioServerTransport;
import io.modelcontextprotocol.spec.ServerMcpTransport;
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 McpServerAutoConfigurationIT {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.mcp.server.enabled=true")
.withConfiguration(AutoConfigurations.of(MpcServerAutoConfiguration.class));
@Test
void defaultConfiguration() {
this.contextRunner.run(context -> {
assertThat(context).hasSingleBean(McpSyncServer.class);
assertThat(context).hasSingleBean(ServerMcpTransport.class);
assertThat(context.getBean(ServerMcpTransport.class)).isInstanceOf(StdioServerTransport.class);
McpServerProperties properties = context.getBean(McpServerProperties.class);
assertThat(properties.getName()).isEqualTo("mcp-server");
assertThat(properties.getVersion()).isEqualTo("1.0.0");
assertThat(properties.getTransport()).isEqualTo(McpServerProperties.Transport.STDIO);
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 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();
});
}
}

View File

@@ -0,0 +1,10 @@
# Test MCP STDIO client configuration
spring.ai.mcp.client.stdio.enabled=true
spring.ai.mcp.client.stdio.version=test-version
spring.ai.mcp.client.stdio.request-timeout=15s
spring.ai.mcp.client.stdio.root-change-notification=false
# Test server configuration
spring.ai.mcp.client.stdio.stdio-connections.test-server.command=echo
spring.ai.mcp.client.stdio.stdio-connections.test-server.args[0]=test
spring.ai.mcp.client.stdio.stdio-connections.test-server.env.TEST_ENV=test-value

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2023-2024 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>spring-ai-mcp-spring-boot-starter</artifactId>
<packaging>jar</packaging>
<name>Spring AI Starter - MCP</name>
<description>Spring AI MCP Auto Configuration</description>
<url>https://github.com/spring-projects/spring-ai</url>
<scm>
<url>https://github.com/spring-projects/spring-ai</url>
<connection>git://github.com/spring-projects/spring-ai.git</connection>
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-spring-boot-autoconfigure</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp</artifactId>
<version>${project.parent.version}</version>
</dependency>
</dependencies>
</project>