Skip to content

Commit d5e98ec

Browse files
committed
test: add integration tests for Content-Type and protocol version validation
Covers the validation added in the previous commit: - HttpTransportValidationTests: verifies POST with non-JSON or missing Content-Type returns 415 for both streamable and stateless transports, and that application/json with charset parameter is accepted. - StreamableTransportProtocolVersionTests: verifies that a matching MCP-Protocol-Version header passes, an absent header passes, and a mismatched header returns 400. Signed-off-by: Gorre Surya <suryateja.g13@gmail.com>
1 parent 580cdf3 commit d5e98ec

2 files changed

Lines changed: 308 additions & 0 deletions

File tree

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
* Copyright 2026-2026 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.server.transport;
6+
7+
import java.net.URI;
8+
import java.net.http.HttpClient;
9+
import java.net.http.HttpRequest;
10+
import java.net.http.HttpResponse;
11+
import java.time.Duration;
12+
import java.util.stream.Stream;
13+
14+
import io.modelcontextprotocol.server.McpServer;
15+
import io.modelcontextprotocol.spec.McpSchema;
16+
import io.modelcontextprotocol.spec.ProtocolVersions;
17+
import jakarta.servlet.http.HttpServlet;
18+
import org.apache.catalina.LifecycleException;
19+
import org.apache.catalina.LifecycleState;
20+
import org.apache.catalina.startup.Tomcat;
21+
import org.junit.jupiter.api.AfterAll;
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.params.BeforeParameterizedClassInvocation;
24+
import org.junit.jupiter.params.Parameter;
25+
import org.junit.jupiter.params.ParameterizedClass;
26+
import org.junit.jupiter.params.provider.Arguments;
27+
import org.junit.jupiter.params.provider.MethodSource;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
import static org.junit.jupiter.api.Named.named;
31+
import static org.junit.jupiter.params.provider.Arguments.arguments;
32+
33+
/**
34+
* Validates Content-Type and protocol version enforcement in HTTP servlet transports.
35+
*
36+
* @author Gorre Surya
37+
*/
38+
@ParameterizedClass
39+
@MethodSource("transports")
40+
class HttpTransportValidationTests {
41+
42+
private static final String ACCEPT_HEADER = "application/json, text/event-stream";
43+
44+
private static final String INITIALIZE_BODY = """
45+
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"%s","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}
46+
"""
47+
.formatted(ProtocolVersions.MCP_2025_11_25)
48+
.strip();
49+
50+
@Parameter
51+
private static TransportServer transportServer;
52+
53+
private static Tomcat tomcat;
54+
55+
private static String baseUrl;
56+
57+
private static HttpClient httpClient;
58+
59+
@BeforeParameterizedClassInvocation
60+
static void setUp(TransportServer transport) {
61+
transportServer = transport;
62+
var port = TomcatTestUtil.findAvailablePort();
63+
baseUrl = "http://localhost:" + port;
64+
tomcat = TomcatTestUtil.createTomcatServer("", port, transportServer.servlet());
65+
try {
66+
tomcat.start();
67+
assertThat(tomcat.getServer().getState()).isEqualTo(LifecycleState.STARTED);
68+
}
69+
catch (Exception e) {
70+
throw new RuntimeException("Failed to start Tomcat", e);
71+
}
72+
httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
73+
}
74+
75+
@AfterAll
76+
static void tearDown() {
77+
if (tomcat != null) {
78+
try {
79+
tomcat.stop();
80+
tomcat.destroy();
81+
}
82+
catch (LifecycleException e) {
83+
throw new RuntimeException("Failed to stop Tomcat", e);
84+
}
85+
}
86+
}
87+
88+
@Test
89+
void postWithNonJsonContentTypeReturns415() throws Exception {
90+
var request = HttpRequest.newBuilder()
91+
.uri(URI.create(baseUrl + "/mcp"))
92+
.header("Content-Type", "text/plain")
93+
.header("Accept", ACCEPT_HEADER)
94+
.POST(HttpRequest.BodyPublishers.ofString(INITIALIZE_BODY))
95+
.build();
96+
97+
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
98+
99+
assertThat(response.statusCode()).isEqualTo(415);
100+
}
101+
102+
@Test
103+
void postWithMissingContentTypeReturns415() throws Exception {
104+
var request = HttpRequest.newBuilder()
105+
.uri(URI.create(baseUrl + "/mcp"))
106+
.header("Accept", ACCEPT_HEADER)
107+
.POST(HttpRequest.BodyPublishers.ofString(INITIALIZE_BODY))
108+
.build();
109+
110+
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
111+
112+
assertThat(response.statusCode()).isEqualTo(415);
113+
}
114+
115+
@Test
116+
void postWithJsonContentTypeIncludingCharsetSucceeds() throws Exception {
117+
var request = HttpRequest.newBuilder()
118+
.uri(URI.create(baseUrl + "/mcp"))
119+
.header("Content-Type", "application/json; charset=utf-8")
120+
.header("Accept", ACCEPT_HEADER)
121+
.POST(HttpRequest.BodyPublishers.ofString(INITIALIZE_BODY))
122+
.build();
123+
124+
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
125+
126+
assertThat(response.statusCode()).isIn(200, 202);
127+
}
128+
129+
static Stream<Arguments> transports() {
130+
return Stream.of(arguments(named("Streamable HTTP", new StreamableHttpTransportServer())),
131+
arguments(named("Stateless", new StatelessTransportServer())));
132+
}
133+
134+
interface TransportServer {
135+
136+
HttpServlet servlet();
137+
138+
}
139+
140+
static class StreamableHttpTransportServer implements TransportServer {
141+
142+
private final HttpServletStreamableServerTransportProvider transport;
143+
144+
StreamableHttpTransportServer() {
145+
transport = HttpServletStreamableServerTransportProvider.builder().build();
146+
McpServer.sync(transport)
147+
.serverInfo("test-server", "1.0.0")
148+
.capabilities(McpSchema.ServerCapabilities.builder().tools(true).build())
149+
.build();
150+
}
151+
152+
@Override
153+
public HttpServlet servlet() {
154+
return transport;
155+
}
156+
157+
}
158+
159+
static class StatelessTransportServer implements TransportServer {
160+
161+
private final HttpServletStatelessServerTransport transport;
162+
163+
StatelessTransportServer() {
164+
transport = HttpServletStatelessServerTransport.builder().build();
165+
McpServer.sync(transport)
166+
.serverInfo("test-server", "1.0.0")
167+
.capabilities(McpSchema.ServerCapabilities.builder().tools(true).build())
168+
.build();
169+
}
170+
171+
@Override
172+
public HttpServlet servlet() {
173+
return transport;
174+
}
175+
176+
}
177+
178+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright 2026-2026 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.server.transport;
6+
7+
import java.net.URI;
8+
import java.net.http.HttpClient;
9+
import java.net.http.HttpRequest;
10+
import java.net.http.HttpResponse;
11+
import java.time.Duration;
12+
13+
import io.modelcontextprotocol.server.McpServer;
14+
import io.modelcontextprotocol.spec.HttpHeaders;
15+
import io.modelcontextprotocol.spec.McpSchema;
16+
import io.modelcontextprotocol.spec.ProtocolVersions;
17+
import org.apache.catalina.LifecycleException;
18+
import org.apache.catalina.LifecycleState;
19+
import org.apache.catalina.startup.Tomcat;
20+
import org.junit.jupiter.api.AfterAll;
21+
import org.junit.jupiter.api.BeforeAll;
22+
import org.junit.jupiter.api.Test;
23+
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
26+
/**
27+
* Validates MCP-Protocol-Version header consistency enforcement in
28+
* {@link HttpServletStreamableServerTransportProvider}.
29+
*
30+
* @author Gorre Surya
31+
*/
32+
class StreamableTransportProtocolVersionTests {
33+
34+
private static final String ACCEPT_HEADER = "application/json, text/event-stream";
35+
36+
private static final String CONTENT_TYPE = "application/json";
37+
38+
private static Tomcat tomcat;
39+
40+
private static String baseUrl;
41+
42+
private static HttpClient httpClient;
43+
44+
@BeforeAll
45+
static void setUp() throws Exception {
46+
var port = TomcatTestUtil.findAvailablePort();
47+
baseUrl = "http://localhost:" + port;
48+
49+
var transport = HttpServletStreamableServerTransportProvider.builder().build();
50+
McpServer.sync(transport)
51+
.serverInfo("test-server", "1.0.0")
52+
.capabilities(McpSchema.ServerCapabilities.builder().tools(true).build())
53+
.build();
54+
55+
tomcat = TomcatTestUtil.createTomcatServer("", port, transport);
56+
tomcat.start();
57+
assertThat(tomcat.getServer().getState()).isEqualTo(LifecycleState.STARTED);
58+
59+
httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
60+
}
61+
62+
@AfterAll
63+
static void tearDown() {
64+
if (tomcat != null) {
65+
try {
66+
tomcat.stop();
67+
tomcat.destroy();
68+
}
69+
catch (LifecycleException e) {
70+
throw new RuntimeException("Failed to stop Tomcat", e);
71+
}
72+
}
73+
}
74+
75+
@Test
76+
void initializeWithMatchingProtocolVersionHeaderSucceeds() throws Exception {
77+
var body = initializeBody(ProtocolVersions.MCP_2025_11_25);
78+
var request = HttpRequest.newBuilder()
79+
.uri(URI.create(baseUrl + "/mcp"))
80+
.header("Content-Type", CONTENT_TYPE)
81+
.header("Accept", ACCEPT_HEADER)
82+
.header(HttpHeaders.PROTOCOL_VERSION, ProtocolVersions.MCP_2025_11_25)
83+
.POST(HttpRequest.BodyPublishers.ofString(body))
84+
.build();
85+
86+
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
87+
88+
assertThat(response.statusCode()).isIn(200, 202);
89+
}
90+
91+
@Test
92+
void initializeWithAbsentProtocolVersionHeaderSucceeds() throws Exception {
93+
var body = initializeBody(ProtocolVersions.MCP_2025_11_25);
94+
var request = HttpRequest.newBuilder()
95+
.uri(URI.create(baseUrl + "/mcp"))
96+
.header("Content-Type", CONTENT_TYPE)
97+
.header("Accept", ACCEPT_HEADER)
98+
.POST(HttpRequest.BodyPublishers.ofString(body))
99+
.build();
100+
101+
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
102+
103+
assertThat(response.statusCode()).isIn(200, 202);
104+
}
105+
106+
@Test
107+
void initializeWithMismatchedProtocolVersionHeaderReturns400() throws Exception {
108+
var body = initializeBody(ProtocolVersions.MCP_2025_11_25);
109+
var request = HttpRequest.newBuilder()
110+
.uri(URI.create(baseUrl + "/mcp"))
111+
.header("Content-Type", CONTENT_TYPE)
112+
.header("Accept", ACCEPT_HEADER)
113+
.header(HttpHeaders.PROTOCOL_VERSION, ProtocolVersions.MCP_2024_11_05)
114+
.POST(HttpRequest.BodyPublishers.ofString(body))
115+
.build();
116+
117+
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
118+
119+
assertThat(response.statusCode()).isEqualTo(400);
120+
}
121+
122+
private static String initializeBody(String protocolVersion) {
123+
return """
124+
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"%s","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}
125+
"""
126+
.formatted(protocolVersion)
127+
.strip();
128+
}
129+
130+
}

0 commit comments

Comments
 (0)