Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@

public abstract class AbstractMcpClientServerIntegrationTests {

protected static final int MAX_REQUEST_SIZE = 2048;

protected ConcurrentHashMap<String, McpClient.SyncSpec> clientBuilders = new ConcurrentHashMap<>();

abstract protected void prepareClients(int port, String mcpEndpoint);
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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();

Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
Loading