Skip to content

refactor: restructure MCP server auto-configuraitons, adding streamable-http support #4179

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
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 @@ -104,8 +104,6 @@
"org.springframework.ai.mcp.client.httpclient.autoconfigure.StreamableHttpHttpClientTransportAutoConfiguration",
"org.springframework.ai.mcp.client.webflux.autoconfigure.SseWebFluxTransportAutoConfiguration",
"org.springframework.ai.mcp.client.webflux.autoconfigure.StreamableHttpWebFluxTransportAutoConfiguration" })

// @AutoConfiguration
@ConditionalOnClass({ McpSchema.class })
@EnableConfigurationProperties(McpClientCommonProperties.class)
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
* <p>
* Example configuration: <pre>
* spring.ai.mcp.client.streamable-http:
* connections-http:
* connections:
* server1:
* url: http://localhost:8080/events
* server2:
Expand Down
19 changes: 19 additions & 0 deletions auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,25 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>net.javacrumbs.json-unit</groupId>
<artifactId>json-unit-assertj</artifactId>
<version>${json-unit-assertj.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-mcp-client-webflux</artifactId>
<version>${project.parent.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,26 @@
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.stream.Collectors;

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.env.Environment;
import org.springframework.core.log.LogAccessor;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.support.StandardServletEnvironment;

import io.modelcontextprotocol.server.McpAsyncServer;
import io.modelcontextprotocol.server.McpAsyncServerExchange;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpServer.AsyncSpecification;
import io.modelcontextprotocol.server.McpServer.SyncSpecification;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.McpServerFeatures.AsyncCompletionSpecification;
import io.modelcontextprotocol.server.McpServerFeatures.AsyncPromptSpecification;
import io.modelcontextprotocol.server.McpServerFeatures.AsyncResourceSpecification;
Expand All @@ -44,23 +56,6 @@
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import reactor.core.publisher.Mono;

import org.springframework.ai.mcp.McpToolUtils;
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.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.env.Environment;
import org.springframework.core.log.LogAccessor;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MimeType;
import org.springframework.web.context.support.StandardServletEnvironment;

/**
* {@link EnableAutoConfiguration Auto-configuration} for the Model Context Protocol (MCP)
* Server.
Expand Down Expand Up @@ -110,7 +105,8 @@
* @see McpWebFluxServerAutoConfiguration
* @see ToolCallback
*/
@AutoConfiguration(after = { McpWebMvcServerAutoConfiguration.class, McpWebFluxServerAutoConfiguration.class })
@AutoConfiguration(after = { ToolCallbackConverterAutoConfiguration.class, McpWebMvcServerAutoConfiguration.class,
McpWebFluxServerAutoConfiguration.class })
@ConditionalOnClass({ McpSchema.class, McpSyncServer.class })
@EnableConfigurationProperties(McpServerProperties.class)
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
Expand All @@ -131,44 +127,6 @@ public McpSchema.ServerCapabilities.Builder capabilitiesBuilder() {
return McpSchema.ServerCapabilities.builder();
}

@Bean
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
matchIfMissing = true)
public List<McpServerFeatures.SyncToolSpecification> syncTools(ObjectProvider<List<ToolCallback>> toolCalls,
List<ToolCallback> toolCallbacksList, McpServerProperties serverProperties) {

List<ToolCallback> tools = new ArrayList<>(toolCalls.stream().flatMap(List::stream).toList());

if (!CollectionUtils.isEmpty(toolCallbacksList)) {
tools.addAll(toolCallbacksList);
}

return this.toSyncToolSpecifications(tools, serverProperties);
}

private List<McpServerFeatures.SyncToolSpecification> toSyncToolSpecifications(List<ToolCallback> tools,
McpServerProperties serverProperties) {

// De-duplicate tools by their name, keeping the first occurrence of each tool
// name
return tools.stream() // Key: tool name
.collect(Collectors.toMap(tool -> tool.getToolDefinition().name(), tool -> tool, // Value:
// the
// tool
// itself
(existing, replacement) -> existing)) // On duplicate key, keep the
// existing tool
.values()
.stream()
.map(tool -> {
String toolName = tool.getToolDefinition().name();
MimeType mimeType = (serverProperties.getToolResponseMimeType().containsKey(toolName))
? MimeType.valueOf(serverProperties.getToolResponseMimeType().get(toolName)) : null;
return McpToolUtils.toSyncToolSpecification(tool, mimeType);
})
.toList();
}

@Bean
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
matchIfMissing = true)
Expand All @@ -179,7 +137,7 @@ public McpSyncServer mcpSyncServer(McpServerTransportProvider transportProvider,
ObjectProvider<List<SyncPromptSpecification>> prompts,
ObjectProvider<List<SyncCompletionSpecification>> completions,
ObjectProvider<BiConsumer<McpSyncServerExchange, List<McpSchema.Root>>> rootsChangeConsumers,
List<ToolCallbackProvider> toolCallbackProvider, Environment environment) {
Environment environment) {

McpSchema.Implementation serverInfo = new Implementation(serverProperties.getName(),
serverProperties.getVersion());
Expand All @@ -195,15 +153,6 @@ public McpSyncServer mcpSyncServer(McpServerTransportProvider transportProvider,
List<SyncToolSpecification> toolSpecifications = new ArrayList<>(
tools.stream().flatMap(List::stream).toList());

List<ToolCallback> providerToolCallbacks = toolCallbackProvider.stream()
.map(pr -> List.of(pr.getToolCallbacks()))
.flatMap(List::stream)
.filter(fc -> fc instanceof ToolCallback)
.map(fc -> (ToolCallback) fc)
.toList();

toolSpecifications.addAll(this.toSyncToolSpecifications(providerToolCallbacks, serverProperties));

if (!CollectionUtils.isEmpty(toolSpecifications)) {
serverBuilder.tools(toolSpecifications);
logger.info("Registered tools: " + toolSpecifications.size());
Expand Down Expand Up @@ -268,41 +217,6 @@ public McpSyncServer mcpSyncServer(McpServerTransportProvider transportProvider,
return serverBuilder.build();
}

@Bean
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
public List<McpServerFeatures.AsyncToolSpecification> asyncTools(ObjectProvider<List<ToolCallback>> toolCalls,
List<ToolCallback> toolCallbackList, McpServerProperties serverProperties) {

List<ToolCallback> tools = new ArrayList<>(toolCalls.stream().flatMap(List::stream).toList());
if (!CollectionUtils.isEmpty(toolCallbackList)) {
tools.addAll(toolCallbackList);
}

return this.toAsyncToolSpecification(tools, serverProperties);
}

private List<McpServerFeatures.AsyncToolSpecification> toAsyncToolSpecification(List<ToolCallback> tools,
McpServerProperties serverProperties) {
// De-duplicate tools by their name, keeping the first occurrence of each tool
// name
return tools.stream() // Key: tool name
.collect(Collectors.toMap(tool -> tool.getToolDefinition().name(), tool -> tool, // Value:
// the
// tool
// itself
(existing, replacement) -> existing)) // On duplicate key, keep the
// existing tool
.values()
.stream()
.map(tool -> {
String toolName = tool.getToolDefinition().name();
MimeType mimeType = (serverProperties.getToolResponseMimeType().containsKey(toolName))
? MimeType.valueOf(serverProperties.getToolResponseMimeType().get(toolName)) : null;
return McpToolUtils.toAsyncToolSpecification(tool, mimeType);
})
.toList();
}

@Bean
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
public McpAsyncServer mcpAsyncServer(McpServerTransportProvider transportProvider,
Expand All @@ -311,8 +225,7 @@ public McpAsyncServer mcpAsyncServer(McpServerTransportProvider transportProvide
ObjectProvider<List<AsyncResourceSpecification>> resources,
ObjectProvider<List<AsyncPromptSpecification>> prompts,
ObjectProvider<List<AsyncCompletionSpecification>> completions,
ObjectProvider<BiConsumer<McpAsyncServerExchange, List<McpSchema.Root>>> rootsChangeConsumer,
List<ToolCallbackProvider> toolCallbackProvider) {
ObjectProvider<BiConsumer<McpAsyncServerExchange, List<McpSchema.Root>>> rootsChangeConsumer) {

McpSchema.Implementation serverInfo = new Implementation(serverProperties.getName(),
serverProperties.getVersion());
Expand All @@ -324,14 +237,6 @@ public McpAsyncServer mcpAsyncServer(McpServerTransportProvider transportProvide
if (serverProperties.getCapabilities().isTool()) {
List<AsyncToolSpecification> toolSpecifications = new ArrayList<>(
tools.stream().flatMap(List::stream).toList());
List<ToolCallback> providerToolCallbacks = toolCallbackProvider.stream()
.map(pr -> List.of(pr.getToolCallbacks()))
.flatMap(List::stream)
.filter(fc -> fc instanceof ToolCallback)
.map(fc -> (ToolCallback) fc)
.toList();

toolSpecifications.addAll(this.toAsyncToolSpecification(providerToolCallbacks, serverProperties));

logger.info("Enable tools capabilities, notification: " + serverProperties.isToolChangeNotification());
capabilitiesBuilder.tools(serverProperties.isToolChangeNotification());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ public class McpServerProperties {
*/
private Duration requestTimeout = Duration.ofSeconds(20);

/**
* The duration to keep the connection alive. Disabled by default.
*/
private Duration keepAliveInterval;

public Duration getRequestTimeout() {
return this.requestTimeout;
}
Expand Down Expand Up @@ -281,6 +286,14 @@ public Map<String, String> getToolResponseMimeType() {
return this.toolResponseMimeType;
}

public void setKeepAliveInterval(Duration keepAliveInterval) {
this.keepAliveInterval = keepAliveInterval;
}

public Duration getKeepAliveInterval() {
return this.keepAliveInterval;
}

public static class Capabilities {

private boolean resource = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@
* <li>A RouterFunction bean that sets up the reactive SSE endpoint</li>
* </ul>
* <p>
* Required dependencies: <pre>{@code
* Required dependencies:
*
* <pre>{@code
* <dependency>
* <groupId>io.modelcontextprotocol.sdk</groupId>
* <artifactId>mcp-spring-webflux</artifactId>
Expand All @@ -76,12 +78,20 @@ public class McpWebFluxServerAutoConfiguration {
@ConditionalOnMissingBean
public WebFluxSseServerTransportProvider webFluxTransport(ObjectProvider<ObjectMapper> objectMapperProvider,
McpServerProperties serverProperties) {

ObjectMapper objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new);
return new WebFluxSseServerTransportProvider(objectMapper, serverProperties.getBaseUrl(),
serverProperties.getSseMessageEndpoint(), serverProperties.getSseEndpoint());

return WebFluxSseServerTransportProvider.builder()
.objectMapper(objectMapper)
.basePath(serverProperties.getBaseUrl())
.messageEndpoint(serverProperties.getSseMessageEndpoint())
.sseEndpoint(serverProperties.getSseEndpoint())
.keepAliveInterval(serverProperties.getKeepAliveInterval())
.build();
}

// Router function for SSE transport used by Spring WebFlux to start an HTTP server.
// Router function for SSE transport used by Spring WebFlux to start an HTTP
// server.
@Bean
public RouterFunction<?> webfluxMcpRouterFunction(WebFluxSseServerTransportProvider webFluxProvider) {
return webFluxProvider.getRouterFunction();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,16 @@ public class McpWebMvcServerAutoConfiguration {
@ConditionalOnMissingBean
public WebMvcSseServerTransportProvider webMvcSseServerTransportProvider(
ObjectProvider<ObjectMapper> objectMapperProvider, McpServerProperties serverProperties) {

ObjectMapper objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new);
return new WebMvcSseServerTransportProvider(objectMapper, serverProperties.getBaseUrl(),
serverProperties.getSseMessageEndpoint(), serverProperties.getSseEndpoint());

return WebMvcSseServerTransportProvider.builder()
.objectMapper(objectMapper)
.baseUrl(serverProperties.getBaseUrl())
.sseEndpoint(serverProperties.getSseEndpoint())
.messageEndpoint(serverProperties.getSseMessageEndpoint())
.keepAliveInterval(serverProperties.getKeepAliveInterval())
.build();
}

@Bean
Expand Down
Loading