Skip to content

Commit 1b3705f

Browse files
YunKuiLutzolov
authored andcommitted
fix: stateless mcp server registration (spring-projects#4396)
- Fix StatelessServerSpecificationFactoryAutoConfiguration.toolSpecs method signature to use McpStatelessServerFeatures.SyncToolSpecification - Add StatelessServerSpecificationFactoryAutoConfiguration to spring imports - update McpStatelessServerAutoConfigurationIT and McpServerAutoConfigurationIT.java Signed-off-by: YunKui Lu <[email protected]> - Ensure the McpServerSpecificationFactoryAutoConfiguration is initialized before McpServerAutoConfiguration - Ensure the StatelessServerSpecificationFactoryAutoConfiguration is initialized before McpServerStatelessAutoConfiguration Signed-off-by: Christian Tzolov <[email protected]>
1 parent 1799f90 commit 1b3705f

File tree

6 files changed

+318
-9
lines changed

6 files changed

+318
-9
lines changed

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,13 @@
7373
* @since 1.0.0
7474
* @see McpServerProperties
7575
*/
76-
@AutoConfiguration(
77-
afterName = { "org.springframework.ai.mcp.server.common.autoconfigure.ToolCallbackConverterAutoConfiguration",
78-
"org.springframework.ai.mcp.server.autoconfigure.McpServerSseWebFluxAutoConfiguration",
79-
"org.springframework.ai.mcp.server.autoconfigure.McpServerSseWebMvcAutoConfiguration",
80-
"org.springframework.ai.mcp.server.autoconfigure.McpServerStreamableHttpWebMvcAutoConfiguration",
81-
"org.springframework.ai.mcp.server.autoconfigure.McpServerStreamableHttpWebFluxAutoConfiguration" })
76+
@AutoConfiguration(afterName = {
77+
"org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerSpecificationFactoryAutoConfiguration",
78+
"org.springframework.ai.mcp.server.common.autoconfigure.ToolCallbackConverterAutoConfiguration",
79+
"org.springframework.ai.mcp.server.autoconfigure.McpServerSseWebFluxAutoConfiguration",
80+
"org.springframework.ai.mcp.server.autoconfigure.McpServerSseWebMvcAutoConfiguration",
81+
"org.springframework.ai.mcp.server.autoconfigure.McpServerStreamableHttpWebMvcAutoConfiguration",
82+
"org.springframework.ai.mcp.server.autoconfigure.McpServerStreamableHttpWebFluxAutoConfiguration" })
8283
@ConditionalOnClass({ McpSchema.class })
8384
@EnableConfigurationProperties({ McpServerProperties.class, McpServerChangeNotificationProperties.class })
8485
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
* @author Christian Tzolov
5656
*/
5757
@AutoConfiguration(afterName = {
58+
"org.springframework.ai.mcp.server.common.autoconfigure.annotations.StatelessServerSpecificationFactoryAutoConfiguration",
5859
"org.springframework.ai.mcp.server.common.autoconfigure.StatelessToolCallbackConverterAutoConfiguration",
5960
"org.springframework.ai.mcp.server.autoconfigure.McpServerStatelessWebFluxAutoConfiguration",
6061
"org.springframework.ai.mcp.server.autoconfigure.McpServerStatelessWebMvcAutoConfiguration" })

auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/annotations/StatelessServerSpecificationFactoryAutoConfiguration.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import java.util.List;
2020

21-
import io.modelcontextprotocol.server.McpServerFeatures;
2221
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
2322
import org.springaicommunity.mcp.annotation.McpComplete;
2423
import org.springaicommunity.mcp.annotation.McpPrompt;
@@ -76,10 +75,10 @@ public List<McpStatelessServerFeatures.SyncCompletionSpecification> completionSp
7675
}
7776

7877
@Bean
79-
public List<McpServerFeatures.SyncToolSpecification> toolSpecs(
78+
public List<McpStatelessServerFeatures.SyncToolSpecification> toolSpecs(
8079
ServerMcpAnnotatedBeans beansWithMcpMethodAnnotations) {
8180
return SyncMcpAnnotationProviders
82-
.toolSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpTool.class));
81+
.statelessToolSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpTool.class));
8382
}
8483

8584
}

auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ org.springframework.ai.mcp.server.common.autoconfigure.ToolCallbackConverterAuto
1818
org.springframework.ai.mcp.server.common.autoconfigure.McpServerStatelessAutoConfiguration
1919
org.springframework.ai.mcp.server.common.autoconfigure.StatelessToolCallbackConverterAutoConfiguration
2020
org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerSpecificationFactoryAutoConfiguration
21+
org.springframework.ai.mcp.server.common.autoconfigure.annotations.StatelessServerSpecificationFactoryAutoConfiguration
2122
org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerAnnotationScannerAutoConfiguration

auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfigurationIT.java

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,20 @@
1717
package org.springframework.ai.mcp.server.common.autoconfigure;
1818

1919
import java.util.List;
20+
import java.util.concurrent.ConcurrentHashMap;
21+
import java.util.concurrent.CopyOnWriteArrayList;
2022
import java.util.function.BiConsumer;
2123
import java.util.function.BiFunction;
24+
import java.util.stream.Stream;
2225

2326
import io.modelcontextprotocol.client.McpSyncClient;
2427
import io.modelcontextprotocol.json.TypeRef;
2528
import io.modelcontextprotocol.server.McpAsyncServer;
2629
import io.modelcontextprotocol.server.McpAsyncServerExchange;
2730
import io.modelcontextprotocol.server.McpServerFeatures;
2831
import io.modelcontextprotocol.server.McpServerFeatures.AsyncCompletionSpecification;
32+
import io.modelcontextprotocol.server.McpServerFeatures.AsyncPromptSpecification;
33+
import io.modelcontextprotocol.server.McpServerFeatures.AsyncResourceSpecification;
2934
import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification;
3035
import io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification;
3136
import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptSpecification;
@@ -39,9 +44,17 @@
3944
import io.modelcontextprotocol.spec.McpServerTransportProvider;
4045
import org.junit.jupiter.api.Test;
4146
import org.mockito.Mockito;
47+
import org.springaicommunity.mcp.annotation.McpArg;
48+
import org.springaicommunity.mcp.annotation.McpComplete;
49+
import org.springaicommunity.mcp.annotation.McpPrompt;
50+
import org.springaicommunity.mcp.annotation.McpResource;
51+
import org.springaicommunity.mcp.annotation.McpTool;
52+
import org.springaicommunity.mcp.annotation.McpToolParam;
4253
import reactor.core.publisher.Mono;
4354

4455
import org.springframework.ai.mcp.SyncMcpToolCallback;
56+
import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerAnnotationScannerAutoConfiguration;
57+
import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerSpecificationFactoryAutoConfiguration;
4558
import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerChangeNotificationProperties;
4659
import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerProperties;
4760
import org.springframework.ai.tool.ToolCallback;
@@ -50,6 +63,8 @@
5063
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
5164
import org.springframework.context.annotation.Bean;
5265
import org.springframework.context.annotation.Configuration;
66+
import org.springframework.stereotype.Component;
67+
import org.springframework.test.util.ReflectionTestUtils;
5368

5469
import static org.assertj.core.api.Assertions.assertThat;
5570
import static org.mockito.Mockito.when;
@@ -345,6 +360,72 @@ void toolCallbackProviderConfiguration() {
345360
.run(context -> assertThat(context).hasSingleBean(ToolCallbackProvider.class));
346361
}
347362

363+
@SuppressWarnings("unchecked")
364+
@Test
365+
void syncServerSpecificationConfiguration() {
366+
this.contextRunner
367+
.withUserConfiguration(McpServerAnnotationScannerAutoConfiguration.class,
368+
McpServerSpecificationFactoryAutoConfiguration.class)
369+
.withBean(SyncTestMcpSpecsComponent.class)
370+
.run(context -> {
371+
McpSyncServer syncServer = context.getBean(McpSyncServer.class);
372+
McpAsyncServer asyncServer = (McpAsyncServer) ReflectionTestUtils.getField(syncServer, "asyncServer");
373+
374+
CopyOnWriteArrayList<AsyncToolSpecification> tools = (CopyOnWriteArrayList<AsyncToolSpecification>) ReflectionTestUtils
375+
.getField(asyncServer, "tools");
376+
assertThat(tools).hasSize(1);
377+
assertThat(tools.get(0).tool().name()).isEqualTo("add");
378+
379+
ConcurrentHashMap<String, AsyncResourceSpecification> resources = (ConcurrentHashMap<String, AsyncResourceSpecification>) ReflectionTestUtils
380+
.getField(asyncServer, "resources");
381+
assertThat(resources).hasSize(1);
382+
assertThat(resources.get("config://{key}")).isNotNull();
383+
384+
ConcurrentHashMap<String, AsyncPromptSpecification> prompts = (ConcurrentHashMap<String, AsyncPromptSpecification>) ReflectionTestUtils
385+
.getField(asyncServer, "prompts");
386+
assertThat(prompts).hasSize(1);
387+
assertThat(prompts.get("greeting")).isNotNull();
388+
389+
ConcurrentHashMap<McpSchema.CompleteReference, AsyncCompletionSpecification> completions = (ConcurrentHashMap<McpSchema.CompleteReference, AsyncCompletionSpecification>) ReflectionTestUtils
390+
.getField(asyncServer, "completions");
391+
assertThat(completions).hasSize(1);
392+
assertThat(completions.keySet().iterator().next()).isInstanceOf(McpSchema.CompleteReference.class);
393+
});
394+
}
395+
396+
@SuppressWarnings("unchecked")
397+
@Test
398+
void asyncServerSpecificationConfiguration() {
399+
this.contextRunner
400+
.withUserConfiguration(McpServerAnnotationScannerAutoConfiguration.class,
401+
McpServerSpecificationFactoryAutoConfiguration.class)
402+
.withBean(AsyncTestMcpSpecsComponent.class)
403+
.withPropertyValues("spring.ai.mcp.server.type=async")
404+
.run(context -> {
405+
McpAsyncServer asyncServer = context.getBean(McpAsyncServer.class);
406+
407+
CopyOnWriteArrayList<AsyncToolSpecification> tools = (CopyOnWriteArrayList<AsyncToolSpecification>) ReflectionTestUtils
408+
.getField(asyncServer, "tools");
409+
assertThat(tools).hasSize(1);
410+
assertThat(tools.get(0).tool().name()).isEqualTo("add");
411+
412+
ConcurrentHashMap<String, AsyncResourceSpecification> resources = (ConcurrentHashMap<String, AsyncResourceSpecification>) ReflectionTestUtils
413+
.getField(asyncServer, "resources");
414+
assertThat(resources).hasSize(1);
415+
assertThat(resources.get("config://{key}")).isNotNull();
416+
417+
ConcurrentHashMap<String, AsyncPromptSpecification> prompts = (ConcurrentHashMap<String, AsyncPromptSpecification>) ReflectionTestUtils
418+
.getField(asyncServer, "prompts");
419+
assertThat(prompts).hasSize(1);
420+
assertThat(prompts.get("greeting")).isNotNull();
421+
422+
ConcurrentHashMap<McpSchema.CompleteReference, AsyncCompletionSpecification> completions = (ConcurrentHashMap<McpSchema.CompleteReference, AsyncCompletionSpecification>) ReflectionTestUtils
423+
.getField(asyncServer, "completions");
424+
assertThat(completions).hasSize(1);
425+
assertThat(completions.keySet().iterator().next()).isInstanceOf(McpSchema.CompleteReference.class);
426+
});
427+
}
428+
348429
@Configuration
349430
static class TestResourceConfiguration {
350431

@@ -516,4 +597,76 @@ McpServerTransport customTransport() {
516597

517598
}
518599

600+
@Component
601+
static class SyncTestMcpSpecsComponent {
602+
603+
@McpTool(name = "add", description = "Add two numbers together", title = "Add Two Numbers Together",
604+
annotations = @McpTool.McpAnnotations(title = "Rectangle Area Calculator", readOnlyHint = true,
605+
destructiveHint = false, idempotentHint = true))
606+
public int add(@McpToolParam(description = "First number", required = true) int a,
607+
@McpToolParam(description = "Second number", required = true) int b) {
608+
return a + b;
609+
}
610+
611+
@McpResource(uri = "config://{key}", name = "Configuration", description = "Provides configuration data")
612+
public String getConfig(String key) {
613+
return "config value";
614+
}
615+
616+
@McpPrompt(name = "greeting", description = "Generate a greeting message")
617+
public McpSchema.GetPromptResult greeting(
618+
@McpArg(name = "name", description = "User's name", required = true) String name) {
619+
620+
String message = "Hello, " + name + "! How can I help you today?";
621+
622+
return new McpSchema.GetPromptResult("Greeting",
623+
List.of(new McpSchema.PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent(message))));
624+
}
625+
626+
@McpComplete(prompt = "city-search")
627+
public List<String> completeCityName(String prefix) {
628+
return Stream.of("New York", "Los Angeles", "Chicago", "Houston", "Phoenix")
629+
.filter(city -> city.toLowerCase().startsWith(prefix.toLowerCase()))
630+
.limit(10)
631+
.toList();
632+
}
633+
634+
}
635+
636+
@Component
637+
static class AsyncTestMcpSpecsComponent {
638+
639+
@McpTool(name = "add", description = "Add two numbers together", title = "Add Two Numbers Together",
640+
annotations = @McpTool.McpAnnotations(title = "Rectangle Area Calculator", readOnlyHint = true,
641+
destructiveHint = false, idempotentHint = true))
642+
public Mono<Integer> add(@McpToolParam(description = "First number", required = true) int a,
643+
@McpToolParam(description = "Second number", required = true) int b) {
644+
return Mono.just(a + b);
645+
}
646+
647+
@McpResource(uri = "config://{key}", name = "Configuration", description = "Provides configuration data")
648+
public Mono<String> getConfig(String key) {
649+
return Mono.just("config value");
650+
}
651+
652+
@McpPrompt(name = "greeting", description = "Generate a greeting message")
653+
public Mono<McpSchema.GetPromptResult> greeting(
654+
@McpArg(name = "name", description = "User's name", required = true) String name) {
655+
656+
String message = "Hello, " + name + "! How can I help you today?";
657+
658+
return Mono.just(new McpSchema.GetPromptResult("Greeting", List
659+
.of(new McpSchema.PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent(message)))));
660+
}
661+
662+
@McpComplete(prompt = "city-search")
663+
public Mono<List<String>> completeCityName(String prefix) {
664+
return Mono.just(Stream.of("New York", "Los Angeles", "Chicago", "Houston", "Phoenix")
665+
.filter(city -> city.toLowerCase().startsWith(prefix.toLowerCase()))
666+
.limit(10)
667+
.toList());
668+
}
669+
670+
}
671+
519672
}

0 commit comments

Comments
 (0)