Skip to content

feat(mcp): Refactor MCP server API to use Specification pattern #2508

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,8 @@
import org.springframework.ai.mcp.client.autoconfigure.configurer.McpAsyncClientConfigurer;
import org.springframework.ai.mcp.client.autoconfigure.configurer.McpSyncClientConfigurer;
import org.springframework.ai.mcp.client.autoconfigure.properties.McpClientCommonProperties;
import org.springframework.ai.mcp.AsyncMcpToolCallbackProvider;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.mcp.customizer.McpAsyncClientCustomizer;
import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
Expand Down Expand Up @@ -179,36 +175,6 @@ public List<McpSyncClient> mcpSyncClients(McpSyncClientConfigurer mcpSyncClientC
return mcpSyncClients;
}

/**
* Creates tool callbacks for all configured MCP clients.
*
* <p>
* These callbacks enable integration with Spring AI's tool execution framework,
* allowing MCP tools to be used as part of AI interactions.
* @param mcpClientsProvider provider of MCP sync clients
* @return list of tool callbacks for MCP integration
*/
@Bean
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
matchIfMissing = true)
public ToolCallbackProvider toolCallbacks(ObjectProvider<List<McpSyncClient>> mcpClientsProvider) {
List<McpSyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
return new SyncMcpToolCallbackProvider(mcpClients);
}

/**
* @deprecated replaced by {@link #toolCallbacks(ObjectProvider)} that returns a
* {@link ToolCallbackProvider} instead of a list of {@link ToolCallback}
*/
@Deprecated
@Bean
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
matchIfMissing = true)
public List<ToolCallback> toolCallbacksDeprecated(ObjectProvider<List<McpSyncClient>> mcpClientsProvider) {
List<McpSyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
return List.of(new SyncMcpToolCallbackProvider(mcpClients).getToolCallbacks());
}

/**
* Record class that implements {@link AutoCloseable} to ensure proper cleanup of MCP
* clients.
Expand Down Expand Up @@ -292,25 +258,6 @@ public List<McpAsyncClient> mcpAsyncClients(McpAsyncClientConfigurer mcpSyncClie
return mcpSyncClients;
}

/**
* @deprecated replaced by {@link #asyncToolCallbacks(ObjectProvider)} that returns a
* {@link ToolCallbackProvider} instead of a list of {@link ToolCallback}
*/
@Deprecated
@Bean
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
public List<ToolCallback> asyncToolCallbacksDeprecated(ObjectProvider<List<McpAsyncClient>> mcpClientsProvider) {
List<McpAsyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
return List.of(new AsyncMcpToolCallbackProvider(mcpClients).getToolCallbacks());
}

@Bean
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
public ToolCallbackProvider asyncToolCallbacks(ObjectProvider<List<McpAsyncClient>> mcpClientsProvider) {
List<McpAsyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
return new AsyncMcpToolCallbackProvider(mcpClients);
}

public record CloseableMcpAsyncClients(List<McpAsyncClient> clients) implements AutoCloseable {
@Override
public void close() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright 2025-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.ai.mcp.client.autoconfigure;

import java.util.List;

import io.modelcontextprotocol.client.McpAsyncClient;
import io.modelcontextprotocol.client.McpSyncClient;

import org.springframework.ai.mcp.AsyncMcpToolCallbackProvider;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.mcp.client.autoconfigure.properties.McpClientCommonProperties;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;

/**
*/
@AutoConfiguration(after = { McpClientAutoConfiguration.class })
@EnableConfigurationProperties(McpClientCommonProperties.class)
@Conditional(McpToolCallbackAutoConfiguration.McpToolCallbackAutoconfiguraitonCondition.class)
public class McpToolCallbackAutoConfiguration {

public static class McpToolCallbackAutoconfiguraitonCondition extends AllNestedConditions {

public McpToolCallbackAutoconfiguraitonCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}

@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
matchIfMissing = true)
static class McpAutoConfigEnabled {

}

@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX + ".toolcallback", name = "enabled",
havingValue = "true", matchIfMissing = false)
static class ToolCallbackProviderEnabled {

}

}

/**
* Creates tool callbacks for all configured MCP clients.
*
* <p>
* These callbacks enable integration with Spring AI's tool execution framework,
* allowing MCP tools to be used as part of AI interactions.
* @param syncMcpClients provider of MCP sync clients
* @return list of tool callbacks for MCP integration
*/
@Bean
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
matchIfMissing = true)
public ToolCallbackProvider mcpToolCallbacks(ObjectProvider<List<McpSyncClient>> syncMcpClients) {
List<McpSyncClient> mcpClients = syncMcpClients.stream().flatMap(List::stream).toList();
return new SyncMcpToolCallbackProvider(mcpClients);
}

@Bean
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
public ToolCallbackProvider mcpAsyncToolCallbacks(ObjectProvider<List<McpAsyncClient>> mcpClientsProvider) {
List<McpAsyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
return new AsyncMcpToolCallbackProvider(mcpClients);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/
package org.springframework.ai.mcp.client.autoconfigure;

import io.modelcontextprotocol.spec.ClientMcpTransport;
import io.modelcontextprotocol.spec.McpClientTransport;

/**
* A named MCP client transport. Usually created by the transport auto-configurations, but
Expand All @@ -26,6 +26,6 @@
* @author Christian Tzolov
* @since 1.0.0
*/
public record NamedClientMcpTransport(String name, ClientMcpTransport transport) {
public record NamedClientMcpTransport(String name, McpClientTransport transport) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ org.springframework.ai.mcp.client.autoconfigure.StdioTransportAutoConfiguration
org.springframework.ai.mcp.client.autoconfigure.SseWebFluxTransportAutoConfiguration
org.springframework.ai.mcp.client.autoconfigure.SseHttpClientTransportAutoConfiguration
org.springframework.ai.mcp.client.autoconfigure.McpClientAutoConfiguration
org.springframework.ai.mcp.client.autoconfigure.McpToolCallbackAutoConfiguration


Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import com.fasterxml.jackson.core.type.TypeReference;
import io.modelcontextprotocol.client.McpAsyncClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.ClientMcpTransport;
import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
Expand All @@ -44,8 +44,8 @@
@Disabled
public class McpClientAutoConfigurationIT {

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(McpClientAutoConfiguration.class));
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(
AutoConfigurations.of(McpToolCallbackAutoConfiguration.class, McpClientAutoConfiguration.class));

@Test
void defaultConfiguration() {
Expand Down Expand Up @@ -131,7 +131,7 @@ static class TestTransportConfiguration {

@Bean
List<NamedClientMcpTransport> testTransports() {
return List.of(new NamedClientMcpTransport("test", Mockito.mock(ClientMcpTransport.class)));
return List.of(new NamedClientMcpTransport("test", Mockito.mock(McpClientTransport.class)));
}

}
Expand All @@ -157,7 +157,7 @@ McpSyncClientCustomizer testCustomizer() {

}

static class CustomClientTransport implements ClientMcpTransport {
static class CustomClientTransport implements McpClientTransport {

@Override
public void close() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2025-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.ai.mcp.client.autoconfigure;

import org.junit.jupiter.api.Test;

import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

import static org.assertj.core.api.Assertions.assertThat;

public class McpToolCallbackAutoConfigurationTests {

private final ApplicationContextRunner applicationContext = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(McpToolCallbackAutoConfiguration.class));

@Test
void disabledByDeafault() {

this.applicationContext.run((context) -> {
assertThat(context).doesNotHaveBean("mcpToolCallbacks");
assertThat(context).doesNotHaveBean("mcpAsyncToolCallbacks");
});

this.applicationContext
.withPropertyValues("spring.ai.mcp.client.enabled=true", "spring.ai.mcp.client.type=SYNC")
.run((context) -> {
assertThat(context).doesNotHaveBean("mcpToolCallbacks");
assertThat(context).doesNotHaveBean("mcpAsyncToolCallbacks");
});

this.applicationContext
.withPropertyValues("spring.ai.mcp.client.enabled=true", "spring.ai.mcp.client.type=ASYNC")
.run((context) -> {
assertThat(context).doesNotHaveBean("mcpToolCallbacks");
assertThat(context).doesNotHaveBean("mcpAsyncToolCallbacks");
});
}

@Test
void enabledMcpToolCallbackAutoconfiguration() {

// sync
this.applicationContext.withPropertyValues("spring.ai.mcp.client.toolcallback.enabled=true").run((context) -> {
assertThat(context).hasBean("mcpToolCallbacks");
assertThat(context).doesNotHaveBean("mcpAsyncToolCallbacks");
});

this.applicationContext
.withPropertyValues("spring.ai.mcp.client.enabled=true", "spring.ai.mcp.client.toolcallback.enabled=true",
"spring.ai.mcp.client.type=SYNC")
.run((context) -> {
assertThat(context).hasBean("mcpToolCallbacks");
assertThat(context).doesNotHaveBean("mcpAsyncToolCallbacks");
});

// Async
this.applicationContext
.withPropertyValues("spring.ai.mcp.client.toolcallback.enabled=true", "spring.ai.mcp.client.type=ASYNC")
.run((context) -> {
assertThat(context).doesNotHaveBean("mcpToolCallbacks");
assertThat(context).hasBean("mcpAsyncToolCallbacks");
});

this.applicationContext
.withPropertyValues("spring.ai.mcp.client.enabled=true", "spring.ai.mcp.client.toolcallback.enabled=true",
"spring.ai.mcp.client.type=ASYNC")
.run((context) -> {
assertThat(context).doesNotHaveBean("mcpToolCallbacks");
assertThat(context).hasBean("mcpAsyncToolCallbacks");
});
}

}
Loading