feat(mcp): update WebFlux and WebMvc server transport providers with URL configuration

- Update MCP SDK version to 0.9.0
- Add baseUrl and sseEndpoint properties to McpServerProperties
- Update WebFlux and WebMvc server transport providers to use new URL configuration properties
- Remove deprecated backward compatibility code and related tests
- Remove deprecated methods from McpToolUtils
- Update MCP SDK version to 0.9.0-SNAPSHOT
- Add tool filtering capability to MCP Tool Callback Providers
  Introduces a BiPredicate-based filtering mechanism for both Sync and Async
  MCP Tool Callback Providers, allowing selective tool discovery based on
  custom criteria. This enables filtering tools by name, client, or
  any combination of properties.
  * Apply filter in getToolCallbacks() methods for both providers
  * Add  tests for various filtering scenarios
- Add utility method to retrieve MCP exchange from tool context
 * Add constant TOOL_CONTEXT_MCP_EXCHANGE_KEY to replace hardcoded exchange string
 * Implement getMcpExchange utility method to safely retrieve the MCP exchange object

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
Christian Tzolov
2025-04-06 21:26:56 +02:00
parent dbc9375d72
commit 3409c1fd87
12 changed files with 258 additions and 850 deletions

View File

@@ -1,112 +0,0 @@
/*
* Copyright 2025 - 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.mcp.server.autoconfigure;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.McpSyncServerExchange;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.CollectionUtils;
/**
* @author Christian Tzolov
*/
@Deprecated
@Configuration
public class McpBackwardCompatibility {
@Bean
public List<BiConsumer<McpSyncServerExchange, List<McpSchema.Root>>> syncRootsChangeConsumerToHandler(
List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers) {
if (CollectionUtils.isEmpty(rootsChangeConsumers)) {
return List.of();
}
return rootsChangeConsumers.stream()
.map(c -> (BiConsumer<McpSyncServerExchange, List<McpSchema.Root>>) ((exchange, roots) -> c.accept(roots)))
.toList();
}
@Bean
public List<McpServerFeatures.SyncToolSpecification> syncToolsRegistrationToSpecificaiton(
ObjectProvider<List<McpServerFeatures.SyncToolRegistration>> toolRegistrations) {
return toolRegistrations.stream()
.flatMap(List::stream)
.map(McpServerFeatures.SyncToolRegistration::toSpecification)
.toList();
}
@Bean
public List<McpServerFeatures.SyncResourceSpecification> syncResourceRegistrationToSpecificaiton(
ObjectProvider<List<McpServerFeatures.SyncResourceRegistration>> resourceRegistrations) {
return resourceRegistrations.stream()
.flatMap(List::stream)
.map(McpServerFeatures.SyncResourceRegistration::toSpecification)
.toList();
}
@Bean
public List<McpServerFeatures.SyncPromptSpecification> syncPromptRegistrationToSpecificaiton(
ObjectProvider<List<McpServerFeatures.SyncPromptRegistration>> promptRegistrations) {
return promptRegistrations.stream()
.flatMap(List::stream)
.map(McpServerFeatures.SyncPromptRegistration::toSpecification)
.toList();
}
// Async
@Bean
public List<McpServerFeatures.AsyncToolSpecification> asyncToolsRegistrationToSpecificaiton(
ObjectProvider<List<McpServerFeatures.AsyncToolRegistration>> toolRegistrations) {
return toolRegistrations.stream()
.flatMap(List::stream)
.map(McpServerFeatures.AsyncToolRegistration::toSpecification)
.toList();
}
@Bean
public List<McpServerFeatures.AsyncResourceSpecification> asyncResourceRegistrationToSpecificaiton(
ObjectProvider<List<McpServerFeatures.AsyncResourceRegistration>> resourceRegistrations) {
return resourceRegistrations.stream()
.flatMap(List::stream)
.map(McpServerFeatures.AsyncResourceRegistration::toSpecification)
.toList();
}
@Bean
public List<McpServerFeatures.AsyncPromptSpecification> asyncPromptRegistrationToSpecificaiton(
ObjectProvider<List<McpServerFeatures.AsyncPromptRegistration>> promptRegistrations) {
return promptRegistrations.stream()
.flatMap(List::stream)
.map(McpServerFeatures.AsyncPromptRegistration::toSpecification)
.toList();
}
}

View File

@@ -53,7 +53,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.core.log.LogAccessor;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MimeType;
@@ -110,7 +109,6 @@ import org.springframework.util.MimeType;
@AutoConfiguration(after = { McpWebMvcServerAutoConfiguration.class, McpWebFluxServerAutoConfiguration.class })
@ConditionalOnClass({ McpSchema.class, McpSyncServer.class })
@EnableConfigurationProperties(McpServerProperties.class)
@Import(McpBackwardCompatibility.class)
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
matchIfMissing = true)
public class McpServerAutoConfiguration {

View File

@@ -98,6 +98,14 @@ public class McpServerProperties {
*/
private boolean promptChangeNotification = true;
/**
*/
private String baseUrl = "";
/**
*/
private String sseEndpoint = "/sse";
/**
* The endpoint path for Server-Sent Events (SSE) when using web transports.
* <p>
@@ -196,6 +204,24 @@ public class McpServerProperties {
this.promptChangeNotification = promptChangeNotification;
}
public String getBaseUrl() {
return this.baseUrl;
}
public void setBaseUrl(String baseUrl) {
Assert.notNull(baseUrl, "Base URL must not be null");
this.baseUrl = baseUrl;
}
public String getSseEndpoint() {
return this.sseEndpoint;
}
public void setSseEndpoint(String sseEndpoint) {
Assert.hasText(sseEndpoint, "SSE endpoint must not be empty");
this.sseEndpoint = sseEndpoint;
}
public String getSseMessageEndpoint() {
return this.sseMessageEndpoint;
}

View File

@@ -1,319 +0,0 @@
/*
* Copyright 2025-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.mcp.server.autoconfigure;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import com.fasterxml.jackson.core.type.TypeReference;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.server.McpAsyncServer;
import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification;
import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptRegistration;
import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceRegistration;
import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.transport.StdioServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpServerTransport;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.spec.ServerMcpTransport;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import reactor.core.publisher.Mono;
import org.springframework.ai.mcp.SyncMcpToolCallback;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@Disabled
public class McpServerAutoConfigurationBackwardCompatibilityIT {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(McpServerAutoConfiguration.class));
@Test
void defaultConfiguration() {
this.contextRunner.run(context -> {
assertThat(context).hasSingleBean(McpSyncServer.class);
assertThat(context).hasSingleBean(McpServerTransportProvider.class);
assertThat(context.getBean(McpServerTransportProvider.class))
.isInstanceOf(StdioServerTransportProvider.class);
McpServerProperties properties = context.getBean(McpServerProperties.class);
assertThat(properties.getName()).isEqualTo("mcp-server");
assertThat(properties.getVersion()).isEqualTo("1.0.0");
assertThat(properties.getType()).isEqualTo(McpServerProperties.ServerType.SYNC);
assertThat(properties.isToolChangeNotification()).isTrue();
assertThat(properties.isResourceChangeNotification()).isTrue();
assertThat(properties.isPromptChangeNotification()).isTrue();
});
}
@Test
void asyncConfiguration() {
this.contextRunner
.withPropertyValues("spring.ai.mcp.server.type=ASYNC", "spring.ai.mcp.server.name=test-server",
"spring.ai.mcp.server.version=2.0.0")
.run(context -> {
assertThat(context).hasSingleBean(McpAsyncServer.class);
assertThat(context).doesNotHaveBean(McpSyncServer.class);
McpServerProperties properties = context.getBean(McpServerProperties.class);
assertThat(properties.getName()).isEqualTo("test-server");
assertThat(properties.getVersion()).isEqualTo("2.0.0");
assertThat(properties.getType()).isEqualTo(McpServerProperties.ServerType.ASYNC);
});
}
@Test
void transportConfiguration() {
this.contextRunner.withUserConfiguration(CustomTransportConfiguration.class).run(context -> {
assertThat(context).hasSingleBean(McpServerTransport.class);
assertThat(context.getBean(McpServerTransport.class)).isInstanceOf(CustomServerTransport.class);
});
}
@Test
void serverNotificationConfiguration() {
this.contextRunner
.withPropertyValues("spring.ai.mcp.server.tool-change-notification=false",
"spring.ai.mcp.server.resource-change-notification=false")
.run(context -> {
McpServerProperties properties = context.getBean(McpServerProperties.class);
assertThat(properties.isToolChangeNotification()).isFalse();
assertThat(properties.isResourceChangeNotification()).isFalse();
});
}
// @Test
void invalidConfigurationThrowsException() {
this.contextRunner.withPropertyValues("spring.ai.mcp.server.version=invalid-version").run(context -> {
assertThat(context).hasFailed();
assertThat(context).getFailure()
.hasRootCauseInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Invalid version format");
});
}
@Test
void disabledConfiguration() {
this.contextRunner.withPropertyValues("spring.ai.mcp.server.enabled=false").run(context -> {
assertThat(context).doesNotHaveBean(McpSyncServer.class);
assertThat(context).doesNotHaveBean(McpAsyncServer.class);
assertThat(context).doesNotHaveBean(ServerMcpTransport.class);
});
}
@Test
void notificationConfiguration() {
this.contextRunner
.withPropertyValues("spring.ai.mcp.server.tool-change-notification=false",
"spring.ai.mcp.server.resource-change-notification=false",
"spring.ai.mcp.server.prompt-change-notification=false")
.run(context -> {
McpServerProperties properties = context.getBean(McpServerProperties.class);
assertThat(properties.isToolChangeNotification()).isFalse();
assertThat(properties.isResourceChangeNotification()).isFalse();
assertThat(properties.isPromptChangeNotification()).isFalse();
});
}
@Test
void stdioConfiguration() {
this.contextRunner.withPropertyValues("spring.ai.mcp.server.stdio=true").run(context -> {
McpServerProperties properties = context.getBean(McpServerProperties.class);
assertThat(properties.isStdio()).isTrue();
});
}
@Test
void serverCapabilitiesConfiguration() {
this.contextRunner.run(context -> {
assertThat(context).hasSingleBean(McpSchema.ServerCapabilities.Builder.class);
McpSchema.ServerCapabilities.Builder builder = context.getBean(McpSchema.ServerCapabilities.Builder.class);
assertThat(builder).isNotNull();
});
}
@Test
void toolRegistrationConfiguration() {
this.contextRunner.withUserConfiguration(TestToolConfiguration.class).run(context -> {
List<SyncToolSpecification> tools = context.getBean("syncTools", List.class);
assertThat(tools).hasSize(2);
});
}
@Test
void resourceRegistrationConfiguration() {
this.contextRunner.withUserConfiguration(TestResourceConfiguration.class).run(context -> {
McpSyncServer server = context.getBean(McpSyncServer.class);
assertThat(server).isNotNull();
});
}
@Test
void promptRegistrationConfiguration() {
this.contextRunner.withUserConfiguration(TestPromptConfiguration.class).run(context -> {
McpSyncServer server = context.getBean(McpSyncServer.class);
assertThat(server).isNotNull();
});
}
@Test
void asyncToolRegistrationConfiguration() {
this.contextRunner.withPropertyValues("spring.ai.mcp.server.type=ASYNC")
.withUserConfiguration(TestToolConfiguration.class)
.run(context -> {
List<AsyncToolSpecification> tools = context.getBean("asyncTools", List.class);
assertThat(tools).hasSize(2);
});
}
@Test
void customCapabilitiesBuilder() {
this.contextRunner.withUserConfiguration(CustomCapabilitiesConfiguration.class).run(context -> {
assertThat(context).hasSingleBean(McpSchema.ServerCapabilities.Builder.class);
assertThat(context.getBean(McpSchema.ServerCapabilities.Builder.class))
.isInstanceOf(CustomCapabilitiesBuilder.class);
});
}
@Test
void rootsChangeConsumerConfiguration() {
this.contextRunner.withUserConfiguration(TestRootsChangeConfiguration.class).run(context -> {
McpSyncServer server = context.getBean(McpSyncServer.class);
assertThat(server).isNotNull();
});
}
@Configuration
static class TestResourceConfiguration {
@Bean
List<SyncResourceRegistration> testResources() {
return List.of();
}
}
@Configuration
static class TestPromptConfiguration {
@Bean
List<SyncPromptRegistration> testPrompts() {
return List.of();
}
}
@Configuration
static class CustomCapabilitiesConfiguration {
@Bean
McpSchema.ServerCapabilities.Builder customCapabilitiesBuilder() {
return new CustomCapabilitiesBuilder();
}
}
static class CustomCapabilitiesBuilder extends McpSchema.ServerCapabilities.Builder {
// Custom implementation for testing
}
@Configuration
static class TestToolConfiguration {
@Bean
List<ToolCallback> testTool() {
McpSyncClient mockClient = Mockito.mock(McpSyncClient.class);
McpSchema.Tool mockTool = Mockito.mock(McpSchema.Tool.class);
McpSchema.CallToolResult mockResult = Mockito.mock(McpSchema.CallToolResult.class);
Mockito.when(mockTool.name()).thenReturn("test-tool");
Mockito.when(mockTool.description()).thenReturn("Test Tool");
Mockito.when(mockClient.callTool(Mockito.any(McpSchema.CallToolRequest.class))).thenReturn(mockResult);
when(mockClient.getClientInfo()).thenReturn(new McpSchema.Implementation("testClient", "1.0.0"));
return List.of(new SyncMcpToolCallback(mockClient, mockTool));
}
}
@Configuration
static class TestRootsChangeConfiguration {
@Bean
Consumer<List<McpSchema.Root>> rootsChangeConsumer() {
return roots -> {
// Test implementation
};
}
}
static class CustomServerTransport implements McpServerTransport {
@Override
public Mono<Void> connect(
Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> messageHandler) {
return Mono.empty(); // Test implementation
}
@Override
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
return Mono.empty(); // Test implementation
}
@Override
public <T> T unmarshalFrom(Object value, TypeReference<T> type) {
return null; // Test implementation
}
@Override
public void close() {
// Test implementation
}
@Override
public Mono<Void> closeGracefully() {
return Mono.empty(); // Test implementation
}
}
@Configuration
static class CustomTransportConfiguration {
@Bean
McpServerTransport customTransport() {
return new CustomServerTransport();
}
}
}

View File

@@ -18,7 +18,6 @@ package org.springframework.ai.mcp.server.autoconfigure;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Function;
import com.fasterxml.jackson.core.type.TypeReference;
import io.modelcontextprotocol.client.McpSyncClient;
@@ -33,7 +32,6 @@ import io.modelcontextprotocol.server.transport.StdioServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpServerTransport;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.spec.ServerMcpTransport;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import reactor.core.publisher.Mono;
@@ -122,7 +120,7 @@ public class McpServerAutoConfigurationIT {
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);
assertThat(context).doesNotHaveBean(McpServerTransport.class);
});
}
@@ -277,12 +275,6 @@ public class McpServerAutoConfigurationIT {
static class CustomServerTransport implements McpServerTransport {
@Override
public Mono<Void> connect(
Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> messageHandler) {
return Mono.empty(); // Test implementation
}
@Override
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
return Mono.empty(); // Test implementation

View File

@@ -17,8 +17,10 @@ package org.springframework.ai.mcp;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiPredicate;
import io.modelcontextprotocol.client.McpAsyncClient;
import io.modelcontextprotocol.spec.McpSchema.Tool;
import io.modelcontextprotocol.util.Assert;
import reactor.core.publisher.Flux;
@@ -73,6 +75,21 @@ public class AsyncMcpToolCallbackProvider implements ToolCallbackProvider {
private final List<McpAsyncClient> mcpClients;
private final BiPredicate<McpAsyncClient, Tool> toolFilter;
/**
* Creates a new {@code AsyncMcpToolCallbackProvider} instance with a list of MCP
* clients.
* @param mcpClients the list of MCP clients to use for discovering tools
* @param toolFilter a filter to apply to each discovered tool
*/
public AsyncMcpToolCallbackProvider(BiPredicate<McpAsyncClient, Tool> toolFilter, List<McpAsyncClient> mcpClients) {
Assert.notNull(mcpClients, "MCP clients must not be null");
Assert.notNull(toolFilter, "Tool filter must not be null");
this.mcpClients = mcpClients;
this.toolFilter = toolFilter;
}
/**
* Creates a new {@code AsyncMcpToolCallbackProvider} instance with a list of MCP
* clients.
@@ -82,13 +99,26 @@ public class AsyncMcpToolCallbackProvider implements ToolCallbackProvider {
* @throws IllegalArgumentException if mcpClients is null
*/
public AsyncMcpToolCallbackProvider(List<McpAsyncClient> mcpClients) {
Assert.notNull(mcpClients, "McpClients must not be null");
this.mcpClients = mcpClients;
this((mcpClient, tool) -> true, mcpClients);
}
/**
* Creates a new {@code AsyncMcpToolCallbackProvider} instance with one or more MCP
* clients.
* @param mcpClients the MCP clients to use for discovering tools
* @param toolFilter a filter to apply to each discovered tool
*/
public AsyncMcpToolCallbackProvider(BiPredicate<McpAsyncClient, Tool> toolFilter, McpAsyncClient... mcpClients) {
this(toolFilter, List.of(mcpClients));
}
/**
* Creates a new {@code AsyncMcpToolCallbackProvider} instance with one or more MCP
* clients.
* @param mcpClients the MCP clients to use for discovering tools
*/
public AsyncMcpToolCallbackProvider(McpAsyncClient... mcpClients) {
Assert.notNull(mcpClients, "McpClients must not be null");
this.mcpClients = List.of(mcpClients);
this(List.of(mcpClients));
}
/**
@@ -116,6 +146,7 @@ public class AsyncMcpToolCallbackProvider implements ToolCallbackProvider {
ToolCallback[] toolCallbacks = mcpClient.listTools()
.map(response -> response.tools()
.stream()
.filter(tool -> toolFilter.test(mcpClient, tool))
.map(tool -> new AsyncMcpToolCallback(mcpClient, tool))
.toArray(ToolCallback[]::new))
.block();

View File

@@ -17,6 +17,7 @@ package org.springframework.ai.mcp;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@@ -24,7 +25,6 @@ import io.micrometer.common.util.StringUtils;
import io.modelcontextprotocol.client.McpAsyncClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolRegistration;
import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification;
import io.modelcontextprotocol.server.McpSyncServerExchange;
import io.modelcontextprotocol.spec.McpSchema;
@@ -62,6 +62,11 @@ import org.springframework.util.MimeType;
*/
public final class McpToolUtils {
/**
* The name of tool context key used to store the MCP exchange object.
*/
public static final String TOOL_CONTEXT_MCP_EXCHANGE_KEY = "exchange";
private McpToolUtils() {
}
@@ -87,23 +92,6 @@ public final class McpToolUtils {
return formatted;
}
/**
* 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)
* @deprecated Use {@link #toSyncToolSpecification(List)} instead.
*/
@Deprecated
public static List<McpServerFeatures.SyncToolRegistration> toSyncToolRegistration(
List<ToolCallback> toolCallbacks) {
return toolCallbacks.stream().map(McpToolUtils::toSyncToolRegistration).toList();
}
/**
* Converts a list of Spring AI tool callbacks to MCP synchronous tool specificaiton.
* <p>
@@ -118,27 +106,6 @@ public final class McpToolUtils {
return toolCallbacks.stream().map(McpToolUtils::toSyncToolSpecification).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)
* @deprecated Use {@link #toSyncToolSpecification(ToolCallback...)} instead.
*/
@Deprecated
public static List<McpServerFeatures.SyncToolRegistration> toSyncToolRegistrations(ToolCallback... toolCallbacks) {
return toSyncToolRegistration(List.of(toolCallbacks));
}
@Deprecated
public static List<McpServerFeatures.SyncToolRegistration> toSyncToolRegistration(ToolCallback... toolCallbacks) {
return toSyncToolRegistration(List.of(toolCallbacks));
}
/**
* Convenience method to convert a variable number of tool callbacks to MCP
* synchronous tool specification.
@@ -153,33 +120,6 @@ public final class McpToolUtils {
return toSyncToolSpecification(List.of(toolCallbacks));
}
/**
* Converts a Spring AI ToolCallback to an MCP SyncToolRegistration. This enables
* Spring AI functions to be exposed as MCP tools that can be discovered and invoked
* by language models.
*
* <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 ToolCallback builder to create a new instance of ToolCallback using
* either java.util.function.Function or Method reference.
* @param toolCallback the Spring AI function callback to convert
* @return an MCP SyncToolRegistration that wraps the function callback
* @throws RuntimeException if there's an error during the function execution
* @deprecated Use {@link #toSyncToolSpecification(ToolCallback)} instead.
*/
@Deprecated
public static McpServerFeatures.SyncToolRegistration toSyncToolRegistration(ToolCallback toolCallback) {
return toSyncToolRegistration(toolCallback, null);
}
/**
* Converts a Spring AI ToolCallback to an MCP SyncToolSpecification. This enables
* Spring AI functions to be exposed as MCP tools that can be discovered and invoked
@@ -205,65 +145,6 @@ public final class McpToolUtils {
return toSyncToolSpecification(toolCallback, null);
}
/**
* Converts a Spring AI ToolCallback to an MCP SyncToolRegistration. This enables
* Spring AI functions to be exposed as MCP tools that can be discovered and invoked
* by language models.
*
* <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 ToolCallback builder to create a new instance of ToolCallback using
* either java.util.function.Function or Method reference.
* @param toolCallback the Spring AI function callback to convert
* @param mimeType the MIME type of the output content
* @return an MCP SyncToolRegistration that wraps the function callback
* @throws RuntimeException if there's an error during the function execution
* @deprecated Use {@link #toSyncToolSpecification(ToolCallback, MimeType)} instead.
*/
@Deprecated
public static McpServerFeatures.SyncToolRegistration toSyncToolRegistration(ToolCallback toolCallback,
MimeType mimeType) {
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));
String imgData = callResult;
if (mimeType != null && "image".equals(mimeType.getType())) {
String imgType = mimeType.toString();
if (callResult.startsWith("{") && callResult.endsWith("}")) {
// This is most likely a JSON structure:
// let's try to parse it as a base64 wrapper.
var b64Struct = JsonParser.fromJson(callResult, Base64Wrapper.class);
if (b64Struct.mimeType() != null && b64Struct.data() != null
&& b64Struct.mimeType.getType().equals("image")) {
// Get the base64 encoded image as is.
imgType = b64Struct.mimeType().toString();
imgData = b64Struct.data();
}
}
return new McpSchema.CallToolResult(
List.of(new McpSchema.ImageContent(List.of(Role.ASSISTANT), null, imgData, imgType)),
false);
}
return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(callResult)), false);
}
catch (Exception e) {
return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(e.getMessage())), true);
}
});
}
/**
* Converts a Spring AI ToolCallback to an MCP SyncToolSpecification. This enables
* Spring AI functions to be exposed as MCP tools that can be discovered and invoked
@@ -292,7 +173,7 @@ public final class McpToolUtils {
return new McpServerFeatures.SyncToolSpecification(tool, (exchange, request) -> {
try {
String callResult = toolCallback.call(ModelOptionsUtils.toJsonString(request),
new ToolContext(Map.of("exchange", exchange)));
new ToolContext(Map.of(TOOL_CONTEXT_MCP_EXCHANGE_KEY, exchange)));
if (mimeType != null && mimeType.toString().startsWith("image")) {
return new McpSchema.CallToolResult(List
.of(new McpSchema.ImageContent(List.of(Role.ASSISTANT), null, callResult, mimeType.toString())),
@@ -307,21 +188,16 @@ public final class McpToolUtils {
}
/**
* 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)
* @deprecated Use {@link #toAsyncToolSpecification(List)} instead.
* Retrieves the MCP exchange object from the provided tool context if it exists.
* @param toolContext the tool context from which to retrieve the MCP exchange
* @return the MCP exchange object, or null if not present in the context
*/
@Deprecated
public static List<McpServerFeatures.AsyncToolRegistration> toAsyncToolRegistration(
List<ToolCallback> toolCallbacks) {
return toolCallbacks.stream().map(McpToolUtils::toAsyncToolRegistration).toList();
public static Optional<McpSyncServerExchange> getMcpExchange(ToolContext toolContext) {
if (toolContext != null && toolContext.getContext().containsKey(TOOL_CONTEXT_MCP_EXCHANGE_KEY)) {
return Optional
.ofNullable((McpSyncServerExchange) toolContext.getContext().get(TOOL_CONTEXT_MCP_EXCHANGE_KEY));
}
return Optional.empty();
}
/**
@@ -339,20 +215,6 @@ public final class McpToolUtils {
return toolCallbacks.stream().map(McpToolUtils::toAsyncToolSpecification).toList();
}
/**
* Convenience method to convert a variable number of tool callbacks to MCP
* asynchronous tool specifications.
* <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));
}
/**
* Convenience method to convert a variable number of tool callbacks to MCP
* asynchronous tool specificaiton.
@@ -368,36 +230,6 @@ public final class McpToolUtils {
return toAsyncToolSpecifications(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()
* @deprecated Use {@link #toAsyncToolSpecification(ToolCallback)} instead.
*/
@Deprecated
public static McpServerFeatures.AsyncToolRegistration toAsyncToolRegistration(ToolCallback toolCallback) {
return toAsyncToolRegistration(toolCallback, null);
}
/**
* Converts a Spring AI tool callback to an MCP asynchronous tool registration.
* <p>
@@ -426,43 +258,6 @@ public final class McpToolUtils {
return toAsyncToolSpecification(toolCallback, null);
}
/**
* 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
* @param mimeType the MIME type of the output content
* @return an MCP asynchronous tool registration that wraps the tool callback
* @see McpServerFeatures.AsyncToolRegistration
* @see Mono
* @see Schedulers#boundedElastic()
* @deprecated Use {@link #toAsyncToolSpecification(ToolCallback, MimeType)} instead.
*/
@Deprecated
public static McpServerFeatures.AsyncToolRegistration toAsyncToolRegistration(ToolCallback toolCallback,
MimeType mimeType) {
McpServerFeatures.SyncToolRegistration syncToolRegistration = toSyncToolRegistration(toolCallback, mimeType);
return new AsyncToolRegistration(syncToolRegistration.tool(),
map -> Mono.fromCallable(() -> syncToolRegistration.call().apply(map))
.subscribeOn(Schedulers.boundedElastic()));
}
/**
* Converts a Spring AI tool callback to an MCP asynchronous tool specification.
* <p>

View File

@@ -17,12 +17,15 @@ package org.springframework.ai.mcp;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiPredicate;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.McpSchema.Tool;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.util.ToolUtils;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
@@ -69,17 +72,47 @@ public class SyncMcpToolCallbackProvider implements ToolCallbackProvider {
private final List<McpSyncClient> mcpClients;
private final BiPredicate<McpSyncClient, Tool> toolFilter;
/**
* Creates a new {@code SyncMcpToolCallbackProvider} instance with a list of MCP
* clients.
* @param mcpClients the list of MCP clients to use for discovering tools
* @param toolFilter a filter to apply to each discovered tool
*/
public SyncMcpToolCallbackProvider(BiPredicate<McpSyncClient, Tool> toolFilter, List<McpSyncClient> mcpClients) {
Assert.notNull(mcpClients, "MCP clients must not be null");
Assert.notNull(toolFilter, "Tool filter must not be null");
this.mcpClients = mcpClients;
this.toolFilter = toolFilter;
}
/**
* Creates a new {@code SyncMcpToolCallbackProvider} instance with a list of MCP
* clients.
* @param mcpClients the list of MCP clients to use for discovering tools
*/
public SyncMcpToolCallbackProvider(List<McpSyncClient> mcpClients) {
this.mcpClients = mcpClients;
this((mcpClient, tool) -> true, mcpClients);
}
/**
* Creates a new {@code SyncMcpToolCallbackProvider} instance with one or more MCP
* clients.
* @param mcpClients the MCP clients to use for discovering tools
* @param toolFilter a filter to apply to each discovered tool
*/
public SyncMcpToolCallbackProvider(BiPredicate<McpSyncClient, Tool> toolFilter, McpSyncClient... mcpClients) {
this(toolFilter, List.of(mcpClients));
}
/**
* Creates a new {@code SyncMcpToolCallbackProvider} instance with one or more MCP
* clients.
* @param mcpClients the MCP clients to use for discovering tools
*/
public SyncMcpToolCallbackProvider(McpSyncClient... mcpClients) {
this.mcpClients = List.of(mcpClients);
this(List.of(mcpClients));
}
/**
@@ -99,10 +132,11 @@ public class SyncMcpToolCallbackProvider implements ToolCallbackProvider {
var toolCallbacks = new ArrayList<>();
mcpClients.stream().forEach(mcpClient -> {
this.mcpClients.stream().forEach(mcpClient -> {
toolCallbacks.addAll(mcpClient.listTools()
.tools()
.stream()
.filter(tool -> toolFilter.test(mcpClient, tool))
.map(tool -> new SyncMcpToolCallback(mcpClient, tool))
.toList());
});

View File

@@ -22,6 +22,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.List;
import java.util.function.BiPredicate;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -129,4 +130,142 @@ class SyncMcpToolCallbackProviderTests {
assertThat(callbacks).hasSize(2);
}
@Test
void toolFilterShouldAcceptAllToolsByDefault() {
var clientInfo = new Implementation("testClient", "1.0.0");
when(mcpClient.getClientInfo()).thenReturn(clientInfo);
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);
// Using the constructor without explicit filter (should use default filter that
// accepts all)
SyncMcpToolCallbackProvider provider = new SyncMcpToolCallbackProvider(mcpClient);
var callbacks = provider.getToolCallbacks();
assertThat(callbacks).hasSize(2);
}
@Test
void toolFilterShouldRejectAllToolsWhenConfigured() {
Tool tool1 = mock(Tool.class);
Tool tool2 = mock(Tool.class);
ListToolsResult listToolsResult = mock(ListToolsResult.class);
when(listToolsResult.tools()).thenReturn(List.of(tool1, tool2));
when(mcpClient.listTools()).thenReturn(listToolsResult);
// Create a filter that rejects all tools
BiPredicate<McpSyncClient, Tool> rejectAllFilter = (client, tool) -> false;
SyncMcpToolCallbackProvider provider = new SyncMcpToolCallbackProvider(rejectAllFilter, mcpClient);
var callbacks = provider.getToolCallbacks();
assertThat(callbacks).isEmpty();
}
@Test
void toolFilterShouldFilterToolsByNameWhenConfigured() {
var clientInfo = new Implementation("testClient", "1.0.0");
when(mcpClient.getClientInfo()).thenReturn(clientInfo);
Tool tool1 = mock(Tool.class);
when(tool1.name()).thenReturn("tool1");
Tool tool2 = mock(Tool.class);
when(tool2.name()).thenReturn("tool2");
Tool tool3 = mock(Tool.class);
when(tool3.name()).thenReturn("tool3");
ListToolsResult listToolsResult = mock(ListToolsResult.class);
when(listToolsResult.tools()).thenReturn(List.of(tool1, tool2, tool3));
when(mcpClient.listTools()).thenReturn(listToolsResult);
// Create a filter that only accepts tools with names containing "2" or "3"
BiPredicate<McpSyncClient, Tool> nameFilter = (client, tool) -> tool.name().contains("2")
|| tool.name().contains("3");
SyncMcpToolCallbackProvider provider = new SyncMcpToolCallbackProvider(nameFilter, mcpClient);
var callbacks = provider.getToolCallbacks();
assertThat(callbacks).hasSize(2);
assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("testClient_tool2");
assertThat(callbacks[1].getToolDefinition().name()).isEqualTo("testClient_tool3");
}
@Test
void toolFilterShouldFilterToolsByClientWhenConfigured() {
Tool tool1 = mock(Tool.class);
when(tool1.name()).thenReturn("tool1");
Tool tool2 = mock(Tool.class);
McpSyncClient mcpClient1 = mock(McpSyncClient.class);
ListToolsResult listToolsResult1 = mock(ListToolsResult.class);
when(listToolsResult1.tools()).thenReturn(List.of(tool1));
when(mcpClient1.listTools()).thenReturn(listToolsResult1);
var clientInfo1 = new Implementation("testClient1", "1.0.0");
when(mcpClient1.getClientInfo()).thenReturn(clientInfo1);
McpSyncClient mcpClient2 = mock(McpSyncClient.class);
ListToolsResult listToolsResult2 = mock(ListToolsResult.class);
when(listToolsResult2.tools()).thenReturn(List.of(tool2));
when(mcpClient2.listTools()).thenReturn(listToolsResult2);
var clientInfo2 = new Implementation("testClient2", "1.0.0");
when(mcpClient2.getClientInfo()).thenReturn(clientInfo2);
// Create a filter that only accepts tools from client1
BiPredicate<McpSyncClient, Tool> clientFilter = (client,
tool) -> client.getClientInfo().name().equals("testClient1");
SyncMcpToolCallbackProvider provider = new SyncMcpToolCallbackProvider(clientFilter, mcpClient1, mcpClient2);
var callbacks = provider.getToolCallbacks();
assertThat(callbacks).hasSize(1);
assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("testClient1_tool1");
}
@Test
void toolFilterShouldCombineClientAndToolCriteriaWhenConfigured() {
Tool tool1 = mock(Tool.class);
when(tool1.name()).thenReturn("weather");
Tool tool2 = mock(Tool.class);
when(tool2.name()).thenReturn("calculator");
McpSyncClient weatherClient = mock(McpSyncClient.class);
ListToolsResult weatherResult = mock(ListToolsResult.class);
when(weatherResult.tools()).thenReturn(List.of(tool1, tool2));
when(weatherClient.listTools()).thenReturn(weatherResult);
var weatherClientInfo = new Implementation("weather-service", "1.0.0");
when(weatherClient.getClientInfo()).thenReturn(weatherClientInfo);
// Create a filter that only accepts weather tools from the weather service
BiPredicate<McpSyncClient, Tool> complexFilter = (client,
tool) -> client.getClientInfo().name().equals("weather-service") && tool.name().equals("weather");
SyncMcpToolCallbackProvider provider = new SyncMcpToolCallbackProvider(complexFilter, weatherClient);
var callbacks = provider.getToolCallbacks();
assertThat(callbacks).hasSize(1);
assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("weather_service_weather");
}
}

View File

@@ -1,178 +0,0 @@
/*
* Copyright 2025-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.mcp;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Map;
import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolRegistration;
import io.modelcontextprotocol.server.McpServerFeatures.SyncToolRegistration;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @deprecated used to test backward compatbility. Replaced by the {@link ToolUtilsTests}
* instead
*/
@Deprecated
class ToolUtilsDeprecatedTests {
@Test
void constructorShouldBePrivate() throws Exception {
Constructor<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.toSyncToolRegistrations(callback1, callback2);
// Assert
assertThat(registrations).hasSize(2);
assertThat(registrations.get(0).tool().name()).isEqualTo("test1");
assertThat(registrations.get(1).tool().name()).isEqualTo("test2");
}
@Test
void toAsyncToolRegistrationShouldConvertSingleCallback() {
// Arrange
ToolCallback callback = createMockToolCallback("test", "success");
// Act
AsyncToolRegistration registration = McpToolUtils.toAsyncToolRegistration(callback);
// Assert
assertThat(registration).isNotNull();
assertThat(registration.tool().name()).isEqualTo("test");
StepVerifier.create(registration.call().apply(Map.of())).assertNext(result -> {
TextContent content = (TextContent) result.content().get(0);
assertThat(content.text()).isEqualTo("success");
assertThat(result.isError()).isFalse();
}).verifyComplete();
}
@Test
void toAsyncToolRegistrationShouldHandleError() {
// Arrange
ToolCallback callback = createMockToolCallback("test", new RuntimeException("error"));
// Act
AsyncToolRegistration registration = McpToolUtils.toAsyncToolRegistration(callback);
// Assert
assertThat(registration).isNotNull();
StepVerifier.create(registration.call().apply(Map.of())).assertNext(result -> {
TextContent content = (TextContent) result.content().get(0);
assertThat(content.text()).isEqualTo("error");
assertThat(result.isError()).isTrue();
}).verifyComplete();
}
@Test
void toAsyncToolRegistrationShouldConvertMultipleCallbacks() {
// Arrange
ToolCallback callback1 = createMockToolCallback("test1", "success1");
ToolCallback callback2 = createMockToolCallback("test2", "success2");
// Act
List<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;
}
}

View File

@@ -306,7 +306,7 @@
<okhttp3.version>4.12.0</okhttp3.version>
<!-- MCP-->
<mcp.sdk.version>0.8.1</mcp.sdk.version>
<mcp.sdk.version>0.9.0</mcp.sdk.version>
<!-- plugin versions -->
<antlr.version>4.13.1</antlr.version>

View File

@@ -96,7 +96,9 @@ All properties are prefixed with `spring.ai.mcp.server`:
|`prompt-change-notification` |Enable prompt change notifications |`true`
|`tool-change-notification` |Enable tool change notifications |`true`
|`tool-response-mime-type` |(optional) response MIME type per tool name. For example `spring.ai.mcp.server.tool-response-mime-type.generateImage=image/png` will associate the `image/png` mime type with the `generateImage()` tool name |`-`
|`sse-message-endpoint` |SSE endpoint path for web transport |`/mcp/message`
|`sse-message-endpoint` | Custom SSE Message endpoint path for web transport to be used by the client to send messages|`/mcp/message`
|`sse-endpoint` |Custom SSE endpoint path for web transport |`/sse`
|`base-url` | Optional URL prefix. For example `base-url=/api/v1` means that the client should access the sse endpont at `/api/v1` + `sse-endpoint` and the message endpoint is `/api/v1` + `sse-message-endpoint` | -
|===
== Sync/Async Server Types