Skip to content

Commit 0b96f4c

Browse files
tzolovmarkpollack
authored andcommitted
refactor: restructure MCP server auto-configuraitons, adding streamable-http support
- Extract MCP server functionality into specialized modules: * STDIO/SSE servers (existing functionality) * Streamable-HTTP servers with change notifications * Stateless servers for simplified deployments - Add new auto-configuration modules: * spring-ai-autoconfigure-mcp-streamable-server-common * spring-ai-autoconfigure-mcp-streamable-server-webflux/webmvc * spring-ai-autoconfigure-mcp-stateless-server-common * spring-ai-autoconfigure-mcp-stateless-server-webflux/webmvc * Add server-specific property prefixes for different server types - Extract ToolCallbackConverter into separate auto-configuration with conditional enablement via tool-callback-converter property - Enhance transport providers with builder patterns and new features: * Keep-alive interval support for connection health * Add keep-alive-interval and other transport-specific options - Add comprehensive integration tests for all server types - Update documentation to reflect new architecture and server options Signed-off-by: Christian Tzolov <[email protected]>
1 parent b9cc239 commit 0b96f4c

File tree

60 files changed

+7586
-490
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+7586
-490
lines changed

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/McpClientAutoConfiguration.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,6 @@
104104
"org.springframework.ai.mcp.client.httpclient.autoconfigure.StreamableHttpHttpClientTransportAutoConfiguration",
105105
"org.springframework.ai.mcp.client.webflux.autoconfigure.SseWebFluxTransportAutoConfiguration",
106106
"org.springframework.ai.mcp.client.webflux.autoconfigure.StreamableHttpWebFluxTransportAutoConfiguration" })
107-
108-
// @AutoConfiguration
109107
@ConditionalOnClass({ McpSchema.class })
110108
@EnableConfigurationProperties(McpClientCommonProperties.class)
111109
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpStreamableHttpClientProperties.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
* <p>
3232
* Example configuration: <pre>
3333
* spring.ai.mcp.client.streamable-http:
34-
* connections-http:
34+
* connections:
3535
* server1:
3636
* url: http://localhost:8080/events
3737
* server2:

auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/pom.xml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,25 @@
6868
<scope>test</scope>
6969
</dependency>
7070

71+
<dependency>
72+
<groupId>net.javacrumbs.json-unit</groupId>
73+
<artifactId>json-unit-assertj</artifactId>
74+
<version>${json-unit-assertj.version}</version>
75+
<scope>test</scope>
76+
</dependency>
77+
78+
<dependency>
79+
<groupId>org.springframework.ai</groupId>
80+
<artifactId>spring-ai-autoconfigure-mcp-client-webflux</artifactId>
81+
<version>${project.parent.version}</version>
82+
<scope>test</scope>
83+
</dependency>
84+
85+
<dependency>
86+
<groupId>org.springframework.boot</groupId>
87+
<artifactId>spring-boot-starter-webflux</artifactId>
88+
<scope>test</scope>
89+
</dependency>
7190

7291
</dependencies>
7392

auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfiguration.java

Lines changed: 18 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,26 @@
2020
import java.util.List;
2121
import java.util.function.BiConsumer;
2222
import java.util.function.BiFunction;
23-
import java.util.stream.Collectors;
23+
24+
import org.springframework.ai.tool.ToolCallback;
25+
import org.springframework.beans.factory.ObjectProvider;
26+
import org.springframework.boot.autoconfigure.AutoConfiguration;
27+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
28+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
29+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
30+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
31+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
32+
import org.springframework.context.annotation.Bean;
33+
import org.springframework.core.env.Environment;
34+
import org.springframework.core.log.LogAccessor;
35+
import org.springframework.util.CollectionUtils;
36+
import org.springframework.web.context.support.StandardServletEnvironment;
2437

2538
import io.modelcontextprotocol.server.McpAsyncServer;
2639
import io.modelcontextprotocol.server.McpAsyncServerExchange;
2740
import io.modelcontextprotocol.server.McpServer;
2841
import io.modelcontextprotocol.server.McpServer.AsyncSpecification;
2942
import io.modelcontextprotocol.server.McpServer.SyncSpecification;
30-
import io.modelcontextprotocol.server.McpServerFeatures;
3143
import io.modelcontextprotocol.server.McpServerFeatures.AsyncCompletionSpecification;
3244
import io.modelcontextprotocol.server.McpServerFeatures.AsyncPromptSpecification;
3345
import io.modelcontextprotocol.server.McpServerFeatures.AsyncResourceSpecification;
@@ -44,23 +56,6 @@
4456
import io.modelcontextprotocol.spec.McpServerTransportProvider;
4557
import reactor.core.publisher.Mono;
4658

47-
import org.springframework.ai.mcp.McpToolUtils;
48-
import org.springframework.ai.tool.ToolCallback;
49-
import org.springframework.ai.tool.ToolCallbackProvider;
50-
import org.springframework.beans.factory.ObjectProvider;
51-
import org.springframework.boot.autoconfigure.AutoConfiguration;
52-
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
53-
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
54-
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
55-
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
56-
import org.springframework.boot.context.properties.EnableConfigurationProperties;
57-
import org.springframework.context.annotation.Bean;
58-
import org.springframework.core.env.Environment;
59-
import org.springframework.core.log.LogAccessor;
60-
import org.springframework.util.CollectionUtils;
61-
import org.springframework.util.MimeType;
62-
import org.springframework.web.context.support.StandardServletEnvironment;
63-
6459
/**
6560
* {@link EnableAutoConfiguration Auto-configuration} for the Model Context Protocol (MCP)
6661
* Server.
@@ -110,7 +105,8 @@
110105
* @see McpWebFluxServerAutoConfiguration
111106
* @see ToolCallback
112107
*/
113-
@AutoConfiguration(after = { McpWebMvcServerAutoConfiguration.class, McpWebFluxServerAutoConfiguration.class })
108+
@AutoConfiguration(after = { ToolCallbackConverterAutoConfiguration.class, McpWebMvcServerAutoConfiguration.class,
109+
McpWebFluxServerAutoConfiguration.class })
114110
@ConditionalOnClass({ McpSchema.class, McpSyncServer.class })
115111
@EnableConfigurationProperties(McpServerProperties.class)
116112
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
@@ -131,44 +127,6 @@ public McpSchema.ServerCapabilities.Builder capabilitiesBuilder() {
131127
return McpSchema.ServerCapabilities.builder();
132128
}
133129

134-
@Bean
135-
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
136-
matchIfMissing = true)
137-
public List<McpServerFeatures.SyncToolSpecification> syncTools(ObjectProvider<List<ToolCallback>> toolCalls,
138-
List<ToolCallback> toolCallbacksList, McpServerProperties serverProperties) {
139-
140-
List<ToolCallback> tools = new ArrayList<>(toolCalls.stream().flatMap(List::stream).toList());
141-
142-
if (!CollectionUtils.isEmpty(toolCallbacksList)) {
143-
tools.addAll(toolCallbacksList);
144-
}
145-
146-
return this.toSyncToolSpecifications(tools, serverProperties);
147-
}
148-
149-
private List<McpServerFeatures.SyncToolSpecification> toSyncToolSpecifications(List<ToolCallback> tools,
150-
McpServerProperties serverProperties) {
151-
152-
// De-duplicate tools by their name, keeping the first occurrence of each tool
153-
// name
154-
return tools.stream() // Key: tool name
155-
.collect(Collectors.toMap(tool -> tool.getToolDefinition().name(), tool -> tool, // Value:
156-
// the
157-
// tool
158-
// itself
159-
(existing, replacement) -> existing)) // On duplicate key, keep the
160-
// existing tool
161-
.values()
162-
.stream()
163-
.map(tool -> {
164-
String toolName = tool.getToolDefinition().name();
165-
MimeType mimeType = (serverProperties.getToolResponseMimeType().containsKey(toolName))
166-
? MimeType.valueOf(serverProperties.getToolResponseMimeType().get(toolName)) : null;
167-
return McpToolUtils.toSyncToolSpecification(tool, mimeType);
168-
})
169-
.toList();
170-
}
171-
172130
@Bean
173131
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
174132
matchIfMissing = true)
@@ -179,7 +137,7 @@ public McpSyncServer mcpSyncServer(McpServerTransportProvider transportProvider,
179137
ObjectProvider<List<SyncPromptSpecification>> prompts,
180138
ObjectProvider<List<SyncCompletionSpecification>> completions,
181139
ObjectProvider<BiConsumer<McpSyncServerExchange, List<McpSchema.Root>>> rootsChangeConsumers,
182-
List<ToolCallbackProvider> toolCallbackProvider, Environment environment) {
140+
Environment environment) {
183141

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

198-
List<ToolCallback> providerToolCallbacks = toolCallbackProvider.stream()
199-
.map(pr -> List.of(pr.getToolCallbacks()))
200-
.flatMap(List::stream)
201-
.filter(fc -> fc instanceof ToolCallback)
202-
.map(fc -> (ToolCallback) fc)
203-
.toList();
204-
205-
toolSpecifications.addAll(this.toSyncToolSpecifications(providerToolCallbacks, serverProperties));
206-
207156
if (!CollectionUtils.isEmpty(toolSpecifications)) {
208157
serverBuilder.tools(toolSpecifications);
209158
logger.info("Registered tools: " + toolSpecifications.size());
@@ -268,41 +217,6 @@ public McpSyncServer mcpSyncServer(McpServerTransportProvider transportProvider,
268217
return serverBuilder.build();
269218
}
270219

271-
@Bean
272-
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
273-
public List<McpServerFeatures.AsyncToolSpecification> asyncTools(ObjectProvider<List<ToolCallback>> toolCalls,
274-
List<ToolCallback> toolCallbackList, McpServerProperties serverProperties) {
275-
276-
List<ToolCallback> tools = new ArrayList<>(toolCalls.stream().flatMap(List::stream).toList());
277-
if (!CollectionUtils.isEmpty(toolCallbackList)) {
278-
tools.addAll(toolCallbackList);
279-
}
280-
281-
return this.toAsyncToolSpecification(tools, serverProperties);
282-
}
283-
284-
private List<McpServerFeatures.AsyncToolSpecification> toAsyncToolSpecification(List<ToolCallback> tools,
285-
McpServerProperties serverProperties) {
286-
// De-duplicate tools by their name, keeping the first occurrence of each tool
287-
// name
288-
return tools.stream() // Key: tool name
289-
.collect(Collectors.toMap(tool -> tool.getToolDefinition().name(), tool -> tool, // Value:
290-
// the
291-
// tool
292-
// itself
293-
(existing, replacement) -> existing)) // On duplicate key, keep the
294-
// existing tool
295-
.values()
296-
.stream()
297-
.map(tool -> {
298-
String toolName = tool.getToolDefinition().name();
299-
MimeType mimeType = (serverProperties.getToolResponseMimeType().containsKey(toolName))
300-
? MimeType.valueOf(serverProperties.getToolResponseMimeType().get(toolName)) : null;
301-
return McpToolUtils.toAsyncToolSpecification(tool, mimeType);
302-
})
303-
.toList();
304-
}
305-
306220
@Bean
307221
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
308222
public McpAsyncServer mcpAsyncServer(McpServerTransportProvider transportProvider,
@@ -311,8 +225,7 @@ public McpAsyncServer mcpAsyncServer(McpServerTransportProvider transportProvide
311225
ObjectProvider<List<AsyncResourceSpecification>> resources,
312226
ObjectProvider<List<AsyncPromptSpecification>> prompts,
313227
ObjectProvider<List<AsyncCompletionSpecification>> completions,
314-
ObjectProvider<BiConsumer<McpAsyncServerExchange, List<McpSchema.Root>>> rootsChangeConsumer,
315-
List<ToolCallbackProvider> toolCallbackProvider) {
228+
ObjectProvider<BiConsumer<McpAsyncServerExchange, List<McpSchema.Root>>> rootsChangeConsumer) {
316229

317230
McpSchema.Implementation serverInfo = new Implementation(serverProperties.getName(),
318231
serverProperties.getVersion());
@@ -324,14 +237,6 @@ public McpAsyncServer mcpAsyncServer(McpServerTransportProvider transportProvide
324237
if (serverProperties.getCapabilities().isTool()) {
325238
List<AsyncToolSpecification> toolSpecifications = new ArrayList<>(
326239
tools.stream().flatMap(List::stream).toList());
327-
List<ToolCallback> providerToolCallbacks = toolCallbackProvider.stream()
328-
.map(pr -> List.of(pr.getToolCallbacks()))
329-
.flatMap(List::stream)
330-
.filter(fc -> fc instanceof ToolCallback)
331-
.map(fc -> (ToolCallback) fc)
332-
.toList();
333-
334-
toolSpecifications.addAll(this.toAsyncToolSpecification(providerToolCallbacks, serverProperties));
335240

336241
logger.info("Enable tools capabilities, notification: " + serverProperties.isToolChangeNotification());
337242
capabilitiesBuilder.tools(serverProperties.isToolChangeNotification());

auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerProperties.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ public class McpServerProperties {
140140
*/
141141
private Duration requestTimeout = Duration.ofSeconds(20);
142142

143+
/**
144+
* The duration to keep the connection alive. Disabled by default.
145+
*/
146+
private Duration keepAliveInterval;
147+
143148
public Duration getRequestTimeout() {
144149
return this.requestTimeout;
145150
}
@@ -281,6 +286,14 @@ public Map<String, String> getToolResponseMimeType() {
281286
return this.toolResponseMimeType;
282287
}
283288

289+
public void setKeepAliveInterval(Duration keepAliveInterval) {
290+
this.keepAliveInterval = keepAliveInterval;
291+
}
292+
293+
public Duration getKeepAliveInterval() {
294+
return this.keepAliveInterval;
295+
}
296+
284297
public static class Capabilities {
285298

286299
private boolean resource = true;

auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpWebFluxServerAutoConfiguration.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@
4949
* <li>A RouterFunction bean that sets up the reactive SSE endpoint</li>
5050
* </ul>
5151
* <p>
52-
* Required dependencies: <pre>{@code
52+
* Required dependencies:
53+
*
54+
* <pre>{@code
5355
* <dependency>
5456
* <groupId>io.modelcontextprotocol.sdk</groupId>
5557
* <artifactId>mcp-spring-webflux</artifactId>
@@ -76,12 +78,20 @@ public class McpWebFluxServerAutoConfiguration {
7678
@ConditionalOnMissingBean
7779
public WebFluxSseServerTransportProvider webFluxTransport(ObjectProvider<ObjectMapper> objectMapperProvider,
7880
McpServerProperties serverProperties) {
81+
7982
ObjectMapper objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new);
80-
return new WebFluxSseServerTransportProvider(objectMapper, serverProperties.getBaseUrl(),
81-
serverProperties.getSseMessageEndpoint(), serverProperties.getSseEndpoint());
83+
84+
return WebFluxSseServerTransportProvider.builder()
85+
.objectMapper(objectMapper)
86+
.basePath(serverProperties.getBaseUrl())
87+
.messageEndpoint(serverProperties.getSseMessageEndpoint())
88+
.sseEndpoint(serverProperties.getSseEndpoint())
89+
.keepAliveInterval(serverProperties.getKeepAliveInterval())
90+
.build();
8291
}
8392

84-
// Router function for SSE transport used by Spring WebFlux to start an HTTP server.
93+
// Router function for SSE transport used by Spring WebFlux to start an HTTP
94+
// server.
8595
@Bean
8696
public RouterFunction<?> webfluxMcpRouterFunction(WebFluxSseServerTransportProvider webFluxProvider) {
8797
return webFluxProvider.getRouterFunction();

auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpWebMvcServerAutoConfiguration.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,16 @@ public class McpWebMvcServerAutoConfiguration {
7171
@ConditionalOnMissingBean
7272
public WebMvcSseServerTransportProvider webMvcSseServerTransportProvider(
7373
ObjectProvider<ObjectMapper> objectMapperProvider, McpServerProperties serverProperties) {
74+
7475
ObjectMapper objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new);
75-
return new WebMvcSseServerTransportProvider(objectMapper, serverProperties.getBaseUrl(),
76-
serverProperties.getSseMessageEndpoint(), serverProperties.getSseEndpoint());
76+
77+
return WebMvcSseServerTransportProvider.builder()
78+
.objectMapper(objectMapper)
79+
.baseUrl(serverProperties.getBaseUrl())
80+
.sseEndpoint(serverProperties.getSseEndpoint())
81+
.messageEndpoint(serverProperties.getSseMessageEndpoint())
82+
.keepAliveInterval(serverProperties.getKeepAliveInterval())
83+
.build();
7784
}
7885

7986
@Bean

0 commit comments

Comments
 (0)