Uses {@code @SpringBootConfiguration} + {@code @EnableAutoConfiguration} instead of + * {@code @SpringBootApplication} so this context does not component-scan the {@code e2e} + * package and pick up the Spring AI sample apps. */ -@SpringBootApplication +@SpringBootConfiguration +@EnableAutoConfiguration @RestController public class SampleSpringMcpApp { diff --git a/src/test/java/io/github/senor14/mcptestkit/e2e/SampleWebFluxSpringAiMcpApp.java b/src/test/java/io/github/senor14/mcptestkit/e2e/SampleWebFluxSpringAiMcpApp.java new file mode 100644 index 0000000..c733469 --- /dev/null +++ b/src/test/java/io/github/senor14/mcptestkit/e2e/SampleWebFluxSpringAiMcpApp.java @@ -0,0 +1,31 @@ +package io.github.senor14.mcptestkit.e2e; + +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.method.MethodToolCallbackProvider; +import org.springframework.ai.mcp.server.autoconfigure.McpServerStreamableHttpWebMvcAutoConfiguration; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Bean; + +/** + * Minimal Spring AI WebFlux MCP server used to verify the {@code spring:} URL scheme against + * the starter's Netty-backed Streamable HTTP transport. + */ +@SpringBootConfiguration +@EnableAutoConfiguration(exclude = McpServerStreamableHttpWebMvcAutoConfiguration.class) +public class SampleWebFluxSpringAiMcpApp { + + @Bean + ToolCallbackProvider sampleTools() { + return MethodToolCallbackProvider.builder().toolObjects(new SampleTools()).build(); + } + + private static final class SampleTools { + + @Tool(description = "Returns a greeting for the supplied name.") + String greet(String name) { + return "Hello, " + name + "!"; + } + } +} diff --git a/src/test/java/io/github/senor14/mcptestkit/e2e/SampleWebMvcSpringAiMcpApp.java b/src/test/java/io/github/senor14/mcptestkit/e2e/SampleWebMvcSpringAiMcpApp.java new file mode 100644 index 0000000..97884b6 --- /dev/null +++ b/src/test/java/io/github/senor14/mcptestkit/e2e/SampleWebMvcSpringAiMcpApp.java @@ -0,0 +1,30 @@ +package io.github.senor14.mcptestkit.e2e; + +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.method.MethodToolCallbackProvider; +import org.springframework.ai.mcp.server.autoconfigure.McpServerStreamableHttpWebFluxAutoConfiguration; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Bean; + +/** + * Minimal Spring AI WebMVC MCP server used to retain servlet-stack regression coverage. + */ +@SpringBootConfiguration +@EnableAutoConfiguration(exclude = McpServerStreamableHttpWebFluxAutoConfiguration.class) +public class SampleWebMvcSpringAiMcpApp { + + @Bean + ToolCallbackProvider sampleTools() { + return MethodToolCallbackProvider.builder().toolObjects(new SampleTools()).build(); + } + + private static final class SampleTools { + + @Tool(description = "Returns a greeting for the supplied name.") + String greet(String name) { + return "Hello, " + name + "!"; + } + } +} diff --git a/src/test/java/io/github/senor14/mcptestkit/e2e/WebFluxSpringAiMcpEndToEndTest.java b/src/test/java/io/github/senor14/mcptestkit/e2e/WebFluxSpringAiMcpEndToEndTest.java new file mode 100644 index 0000000..8bce446 --- /dev/null +++ b/src/test/java/io/github/senor14/mcptestkit/e2e/WebFluxSpringAiMcpEndToEndTest.java @@ -0,0 +1,35 @@ +package io.github.senor14.mcptestkit.e2e; + +import io.github.senor14.mcptestkit.McpAssertions; +import io.github.senor14.mcptestkit.McpServerTest; +import io.github.senor14.mcptestkit.McpTestClient; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * Verifies that {@code spring:} discovers a random WebFlux/Netty server port and reaches a real + * Spring AI Streamable HTTP MCP endpoint. + */ +@SpringBootTest( + classes = SampleWebFluxSpringAiMcpApp.class, + webEnvironment = RANDOM_PORT, + properties = { + "spring.main.web-application-type=reactive", + "spring.ai.mcp.server.protocol=STREAMABLE", + "spring.ai.mcp.server.name=sample-webflux-mcp-server", + "spring.ai.mcp.server.version=1.0.0" + }) +@McpServerTest(url = "spring:/mcp") +class WebFluxSpringAiMcpEndToEndTest { + + @Test + void conformancePassesAgainstSpringAiWebFluxServer(McpTestClient client) { + McpAssertions.assertThat(client) + .initializesSuccessfully() + .hasTools() + .toolsHaveDescriptions() + .toolSchemasAreValid(); + } +} diff --git a/src/test/java/io/github/senor14/mcptestkit/e2e/WebMvcSpringAiMcpEndToEndTest.java b/src/test/java/io/github/senor14/mcptestkit/e2e/WebMvcSpringAiMcpEndToEndTest.java new file mode 100644 index 0000000..45f34cc --- /dev/null +++ b/src/test/java/io/github/senor14/mcptestkit/e2e/WebMvcSpringAiMcpEndToEndTest.java @@ -0,0 +1,34 @@ +package io.github.senor14.mcptestkit.e2e; + +import io.github.senor14.mcptestkit.McpAssertions; +import io.github.senor14.mcptestkit.McpServerTest; +import io.github.senor14.mcptestkit.McpTestClient; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * Verifies that {@code spring:} discovers a random Spring AI WebMVC server port. + */ +@SpringBootTest( + classes = SampleWebMvcSpringAiMcpApp.class, + webEnvironment = RANDOM_PORT, + properties = { + "spring.main.web-application-type=servlet", + "spring.ai.mcp.server.protocol=STREAMABLE", + "spring.ai.mcp.server.name=sample-webmvc-mcp-server", + "spring.ai.mcp.server.version=1.0.0" + }) +@McpServerTest(url = "spring:/mcp") +class WebMvcSpringAiMcpEndToEndTest { + + @Test + void conformancePassesAgainstSpringAiWebMvcServer(McpTestClient client) { + McpAssertions.assertThat(client) + .initializesSuccessfully() + .hasTools() + .toolsHaveDescriptions() + .toolSchemasAreValid(); + } +}