diff --git a/README.md b/README.md index 9fd96aa..1bd3c5a 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ class MySpringServerTest { } ``` -No Spring dependency is pulled in — the port lookup is reflective and only activates when you use `spring:`. It is exercised against a real Spring AI MCP server (the `spring-ai-starter-mcp-server-webmvc` starter) in [maven-tools-mcp](https://github.com/arvindand/maven-tools-mcp)'s CI, as well as against the sample server in this repo. The HTTP client speaks the 2025-11-25 Streamable HTTP transport: it echoes the negotiated revision on every post-handshake request via `MCP-Protocol-Version`, captures and echoes `Mcp-Session-Id` when a server issues one, and handles both plain JSON and SSE response modes. +No Spring dependency is pulled in — the port lookup is reflective and only activates when you use `spring:`. It is exercised against real Spring AI MCP servers using both the `spring-ai-starter-mcp-server-webmvc` starter in [maven-tools-mcp](https://github.com/arvindand/maven-tools-mcp)'s CI and the `spring-ai-starter-mcp-server-webflux` starter in this repository, as well as against the sample server in this repo. The HTTP client speaks the 2025-11-25 Streamable HTTP transport: it echoes the negotiated revision on every post-handshake request via `MCP-Protocol-Version`, captures and echoes `Mcp-Session-Id` when a server issues one, and handles both plain JSON and SSE response modes. ## Relationship to official tooling diff --git a/pom.xml b/pom.xml index 0090ec1..3b75d8d 100644 --- a/pom.xml +++ b/pom.xml @@ -38,6 +38,7 @@ 2.22.2 5.14.4 3.5.16 + 1.1.0 @@ -66,6 +67,18 @@ ${spring-boot.version} test + + org.springframework.ai + spring-ai-starter-mcp-server-webmvc + ${spring-ai.version} + test + + + org.springframework.ai + spring-ai-starter-mcp-server-webflux + ${spring-ai.version} + test + org.springframework.boot spring-boot-starter-test diff --git a/src/test/java/io/github/senor14/mcptestkit/e2e/SampleSpringMcpApp.java b/src/test/java/io/github/senor14/mcptestkit/e2e/SampleSpringMcpApp.java index 448f158..ac910d0 100644 --- a/src/test/java/io/github/senor14/mcptestkit/e2e/SampleSpringMcpApp.java +++ b/src/test/java/io/github/senor14/mcptestkit/e2e/SampleSpringMcpApp.java @@ -3,7 +3,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -16,8 +17,13 @@ * Minimal Spring Boot app exposing the sample MCP server ({@link SampleMcpLogic}) at * {@code POST /mcp}, used to test the {@code spring:} URL scheme end-to-end without * depending on any MCP SDK. + * + *

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(); + } +}