diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java index bc22b0ace..dd4d9f702 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java @@ -72,8 +72,6 @@ public abstract class AbstractMcpClientServerIntegrationTests { - protected static final int MAX_REQUEST_SIZE = 2048; - protected ConcurrentHashMap clientBuilders = new ConcurrentHashMap<>(); abstract protected void prepareClients(int port, String mcpEndpoint); @@ -2430,51 +2428,6 @@ void testResourceSubscription_afterUnsubscribe_noNotification(String clientType) } } - // Bounded read - @ParameterizedTest(name = "{0} : {displayName} ") - @MethodSource("clientsForTesting") - void testRejectsWhenContentLengthHeaderExceedsLimit(String clientType) throws Exception { - var clientBuilder = clientBuilders.get(clientType); - - String inputSchema = """ - { - "type": "object", - "properties": { - "message": { "type": "string" } - }, - "required": ["message"] - } - """; - - McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder() - .tool(Tool.builder("tool1", McpJsonDefaults.getMapper(), inputSchema) - .description("tool1 description") - .build()) - .callHandler((exchange, request) -> CallToolResult.builder() - .addContent(TextContent.builder(request.arguments().get("message").toString()).build()) - .build()) - .build(); - - var mcpServer = prepareSyncServerBuilder().capabilities(ServerCapabilities.builder().tools(false).build()) - .tools(tool1) - .build(); - - try (var mcpClient = clientBuilder.build()) { - String oversizedBody = "a".repeat(MAX_REQUEST_SIZE + 1); - - mcpClient.initialize(); - assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); - - assertThatThrownBy(() -> mcpClient.callTool( - McpSchema.CallToolRequest.builder("tool1").arguments(Map.of("message", oversizedBody)).build())) - .isInstanceOf(RuntimeException.class) - .hasMessageContaining("413"); - } - finally { - mcpServer.closeGracefully(); - } - } - private double evaluateExpression(String expression) { // Simple expression evaluator for testing return switch (expression) { diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java index 72c54bccd..6f6517adc 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java @@ -24,6 +24,7 @@ import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import io.modelcontextprotocol.common.McpTransportContext; +import io.modelcontextprotocol.json.McpJsonDefaults; import io.modelcontextprotocol.server.McpServer.AsyncSpecification; import io.modelcontextprotocol.server.McpServer.SyncSpecification; import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; @@ -42,11 +43,13 @@ import org.junit.jupiter.api.Timeout; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import reactor.core.publisher.Mono; import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; @Timeout(15) class HttpServletSseIntegrationTests extends AbstractMcpClientServerIntegrationTests { @@ -57,6 +60,8 @@ class HttpServletSseIntegrationTests extends AbstractMcpClientServerIntegrationT private static final String CUSTOM_MESSAGE_ENDPOINT = "/otherPath/mcp/message"; + protected static final int MAX_REQUEST_SIZE = 2048; + // Tomcat is started once for the whole class; each test swaps in its own transport private static final TomcatTestUtil.DelegatingServlet MCP_SERVLET = new TomcatTestUtil.DelegatingServlet(); @@ -132,6 +137,51 @@ public void after() { protected void prepareClients(int port, String mcpEndpoint) { } + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testRejectsWhenContentLengthHeaderExceedsLimit(String clientType) { + var clientBuilder = clientBuilders.get(clientType); + + String inputSchema = """ + { + "type": "object", + "properties": { + "message": { "type": "string" } + }, + "required": ["message"] + } + """; + + McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder("tool1", McpJsonDefaults.getMapper(), inputSchema) + .description("tool1 description") + .build()) + .callHandler((exchange, request) -> McpSchema.CallToolResult.builder() + .addContent(McpSchema.TextContent.builder(request.arguments().get("message").toString()).build()) + .build()) + .build(); + + var mcpServer = prepareSyncServerBuilder() + .capabilities(McpSchema.ServerCapabilities.builder().tools(false).build()) + .tools(tool1) + .build(); + + try (var mcpClient = clientBuilder.build()) { + String oversizedBody = "a".repeat(MAX_REQUEST_SIZE + 1); + + mcpClient.initialize(); + assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); + + assertThatThrownBy(() -> mcpClient.callTool( + McpSchema.CallToolRequest.builder("tool1").arguments(Map.of("message", oversizedBody)).build())) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("413"); + } + finally { + mcpServer.closeGracefully(); + } + } + @Test void rejectsWhenBodyBytesExceedLimitWithoutContentLengthHeader() throws Exception { var httpClient = HttpClient.newHttpClient(); diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java index 46227fe35..99f693156 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java @@ -30,6 +30,7 @@ import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; import io.modelcontextprotocol.common.McpTransportContext; +import io.modelcontextprotocol.json.McpJsonDefaults; import io.modelcontextprotocol.server.McpServer.AsyncSpecification; import io.modelcontextprotocol.server.McpServer.SyncSpecification; import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; @@ -50,6 +51,7 @@ import org.junit.jupiter.api.Timeout; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; @@ -61,6 +63,7 @@ import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA; import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; /** @@ -90,6 +93,8 @@ class HttpServletStreamableIntegrationTests extends AbstractMcpClientServerInteg // result in a session sweep private final Duration SESSION_SWEEP_INTERVAL = KEEP_ALIVE_INTERVAL.multipliedBy(2); + protected static final int MAX_REQUEST_SIZE = 2048; + private final HttpClient httpClient = HttpClient.newHttpClient(); @Override @@ -253,6 +258,51 @@ public void cancel() { assertThat(response.statusCode()).isEqualTo(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE); } + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testRejectsWhenContentLengthHeaderExceedsLimit(String clientType) { + var clientBuilder = clientBuilders.get(clientType); + + String inputSchema = """ + { + "type": "object", + "properties": { + "message": { "type": "string" } + }, + "required": ["message"] + } + """; + + McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder("tool1", McpJsonDefaults.getMapper(), inputSchema) + .description("tool1 description") + .build()) + .callHandler((exchange, request) -> McpSchema.CallToolResult.builder() + .addContent(McpSchema.TextContent.builder(request.arguments().get("message").toString()).build()) + .build()) + .build(); + + var mcpServer = prepareSyncServerBuilder() + .capabilities(McpSchema.ServerCapabilities.builder().tools(false).build()) + .tools(tool1) + .build(); + + try (var mcpClient = clientBuilder.build()) { + String oversizedBody = "a".repeat(MAX_REQUEST_SIZE + 1); + + mcpClient.initialize(); + assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); + + assertThatThrownBy(() -> mcpClient.callTool( + McpSchema.CallToolRequest.builder("tool1").arguments(Map.of("message", oversizedBody)).build())) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("413"); + } + finally { + mcpServer.closeGracefully(); + } + } + @ParameterizedTest @ValueSource(strings = { "text/plain;charset=UTF-8", "application/x-www-form-urlencoded", "multipart/form-data" }) void rejectsInitializeWithNonJsonContentType(String contentType) throws Exception {