Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 13 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<jackson.version>2.22.2</jackson.version>
<junit.version>5.14.4</junit.version>
<spring-boot.version>3.5.16</spring-boot.version>
<spring-ai.version>1.1.0</spring-ai.version>
</properties>

<dependencies>
Expand Down Expand Up @@ -66,6 +67,18 @@
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
<version>${spring-ai.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
<version>${spring-ai.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
*
* <p>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 {

Expand Down
Original file line number Diff line number Diff line change
@@ -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 + "!";
}
}
}
Original file line number Diff line number Diff line change
@@ -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 + "!";
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}