diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransport.java index 002009708..ed3bec46f 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransport.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransport.java @@ -35,6 +35,7 @@ import io.modelcontextprotocol.server.McpTransportContextExtractor; import io.modelcontextprotocol.server.transport.DefaultServerTransportSecurityValidator; import io.modelcontextprotocol.server.transport.HeaderAccessor; +import io.modelcontextprotocol.server.transport.HttpServletRequestUtils; import io.modelcontextprotocol.server.transport.ServerHttpHeaderValidator; import io.modelcontextprotocol.server.transport.ServerTransportSecurityException; import io.modelcontextprotocol.util.Assert; @@ -185,6 +186,11 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) response.sendError(e.getStatusCode(), e.getMessage()); return; } + if (!HttpServletRequestUtils.isJsonContentType(request.getContentType())) { + response.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, + "Unsupported Media Type: Content-Type must be application/json"); + return; + } if (request.getContentLengthLong() > this.requestMaxSize) { response.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE); return; diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletRequestUtils.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletRequestUtils.java index ddbced4eb..6ffe4ffed 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletRequestUtils.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletRequestUtils.java @@ -16,7 +16,7 @@ * * @author Daniel Garnier-Moiroux */ -final class HttpServletRequestUtils { +public final class HttpServletRequestUtils { private static final String APPLICATION_JSON = "application/json"; @@ -35,7 +35,7 @@ private HttpServletRequestUtils() { * @param contentType The {@code Content-Type} header value, may be {@code null} * @return {@code true} if the media type is {@code application/json} */ - static boolean isJsonContentType(String contentType) { + public static boolean isJsonContentType(String contentType) { if (contentType == null) { return false; } diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportIntegrationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportIntegrationTests.java index 9bd943125..41ba8b78f 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportIntegrationTests.java @@ -50,6 +50,9 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -198,6 +201,36 @@ void singleToolCallReturnsJson() throws Exception { .hasValueSatisfying(v -> assertThat(v).contains("application/json")); } + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = { "text/plain", "application/x-www-form-urlencoded", "multipart/form-data", + "application/json-rpc", "application/jsonp", "text/plain; a=application/json" }) + void postWithNonJsonContentTypeIsRejected(String contentType) throws Exception { + HttpRequest original = post("tools/call", Map.of("_meta", meta(), "name", "echo")).header("Mcp-Name", "echo") + .build(); + HttpRequest.Builder builder = HttpRequest.newBuilder(original, + (name, value) -> !name.equalsIgnoreCase("Content-Type")); + if (contentType != null) { + builder.header("Content-Type", contentType); + } + HttpResponse response = HttpClient.newHttpClient() + .send(builder.build(), HttpResponse.BodyHandlers.ofString()); + + assertThat(response.statusCode()).isEqualTo(415); + } + + @ParameterizedTest + @ValueSource(strings = { "application/json; charset=utf-8", "Application/JSON", "Application/JSON; charset=UTF-8" }) + void postWithJsonContentTypeIsAccepted(String contentType) throws Exception { + HttpRequest request = post("tools/call", Map.of("_meta", meta(), "name", "echo")).header("Mcp-Name", "echo") + .setHeader("Content-Type", contentType) + .build(); + HttpResponse response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); + + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.body()).contains("echo:echo"); + } + @Test void streamingToolCallReturnsSse() throws Exception { Map meta = meta();