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:
committed by
Mark Pollack
parent
fc2690cdf8
commit
f40945bd63
87
mcp/common/pom.xml
Normal file
87
mcp/common/pom.xml
Normal 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>
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.aot.hint.RuntimeHintsRegistrar=\
|
||||
org.springframework.ai.mcp.McpHints
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
20
pom.xml
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[[upgrade-notes]]
|
||||
= Upgrading Notes
|
||||
= Upgrade Notes
|
||||
|
||||
== Upgrading to 1.0.0.M6
|
||||
|
||||
|
||||
202
spring-ai-docs/src/main/asciidoc/mcp.md
Normal file
202
spring-ai-docs/src/main/asciidoc/mcp.md
Normal 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.
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
58
spring-ai-spring-boot-starters/spring-ai-starter-mcp/pom.xml
Normal file
58
spring-ai-spring-boot-starters/spring-ai-starter-mcp/pom.xml
Normal 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>
|
||||
Reference in New Issue
Block a user