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
7 changes: 6 additions & 1 deletion api/envoy/extensions/filters/http/mcp/v3/mcp.proto
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,13 @@ message Mcp {
google.protobuf.UInt32Value max_request_body_size = 3 [(validate.rules).uint32 = {lte: 10485760}];
}

// McpOverride for MCP filter
// Per-route override configuration for MCP filter
message McpOverride {
// Optional per-route traffic mode override
Mcp.TrafficMode traffic_mode = 1 [(validate.rules).enum = {defined_only: true}];

// Optional per-route max request body size override.
// When set, this overrides the global max_request_body_size for this route.
// It defaults to 8KB (8192 bytes) and the maximum allowed value is 10MB (10485760 bytes).
google.protobuf.UInt32Value max_request_body_size = 2 [(validate.rules).uint32 = {lte: 10485760}];
}
15 changes: 13 additions & 2 deletions source/extensions/filters/http/mcp/mcp_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@ bool McpFilter::shouldRejectRequest() const {
return config_->shouldRejectNonMcp();
}

uint32_t McpFilter::getMaxRequestBodySize() const {
const auto* override_config =
Http::Utility::resolveMostSpecificPerFilterConfig<McpOverrideConfig>(decoder_callbacks_);

if (override_config && override_config->maxRequestBodySize().has_value()) {
return override_config->maxRequestBodySize().value();
}

return config_->maxRequestBodySize();
}

Http::FilterHeadersStatus McpFilter::decodeHeaders(Http::RequestHeaderMap& headers,
bool end_stream) {

Expand All @@ -99,7 +110,7 @@ Http::FilterHeadersStatus McpFilter::decodeHeaders(Http::RequestHeaderMap& heade
is_mcp_request_ = true;

// Set the buffer limit - Envoy will automatically send 413 if exceeded
const uint32_t max_size = config_->maxRequestBodySize();
const uint32_t max_size = getMaxRequestBodySize();
if (max_size > 0) {
decoder_callbacks_->setDecoderBufferLimit(max_size);
ENVOY_LOG(debug, "set decoder buffer limit to {} bytes", max_size);
Expand Down Expand Up @@ -127,7 +138,7 @@ Http::FilterDataStatus McpFilter::decodeData(Buffer::Instance& data, bool end_st

if (end_stream) {
// Check if the complete request body exceeds the limit
const uint32_t max_size = config_->maxRequestBodySize();
const uint32_t max_size = getMaxRequestBodySize();
if (max_size > 0) {
decoder_callbacks_->addDecodedData(data, false);
const uint64_t total_size = decoder_callbacks_->decodingBuffer()->length();
Expand Down
10 changes: 9 additions & 1 deletion source/extensions/filters/http/mcp/mcp_filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,21 @@ class McpOverrideConfig : public Router::RouteSpecificFilterConfig {
public:
explicit McpOverrideConfig(
const envoy::extensions::filters::http::mcp::v3::McpOverride& proto_config)
: traffic_mode_(proto_config.traffic_mode()) {}
: traffic_mode_(proto_config.traffic_mode()),
max_request_body_size_(
proto_config.has_max_request_body_size()
? absl::optional<uint32_t>(proto_config.max_request_body_size().value())
: absl::nullopt) {}

envoy::extensions::filters::http::mcp::v3::Mcp::TrafficMode trafficMode() const {
return traffic_mode_;
}

absl::optional<uint32_t> maxRequestBodySize() const { return max_request_body_size_; }

private:
const envoy::extensions::filters::http::mcp::v3::Mcp::TrafficMode traffic_mode_;
const absl::optional<uint32_t> max_request_body_size_;
};

using McpFilterConfigSharedPtr = std::shared_ptr<McpFilterConfig>;
Expand Down Expand Up @@ -103,6 +110,7 @@ class McpFilter : public Http::PassThroughFilter, public Logger::Loggable<Logger
bool isValidMcpSseRequest(const Http::RequestHeaderMap& headers) const;
bool isValidMcpPostRequest(const Http::RequestHeaderMap& headers) const;
bool shouldRejectRequest() const;
uint32_t getMaxRequestBodySize() const;

void finalizeDynamicMetadata();
McpFilterConfigSharedPtr config_;
Expand Down
126 changes: 126 additions & 0 deletions test/extensions/filters/http/mcp/mcp_filter_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,132 @@ TEST_F(McpFilterTest, ClearRouteCacheConfigGetter) {
EXPECT_TRUE(config_->clearRouteCache());
}

// Test per-route max body size override with smaller limit
TEST_F(McpFilterTest, PerRouteMaxBodySizeSmallerLimit) {
// Global config with 1024 bytes
envoy::extensions::filters::http::mcp::v3::Mcp proto_config;
proto_config.mutable_max_request_body_size()->set_value(1024);
config_ = std::make_shared<McpFilterConfig>(proto_config);
filter_ = std::make_unique<McpFilter>(config_);
filter_->setDecoderFilterCallbacks(decoder_callbacks_);

// Per-route config with smaller limit (100 bytes)
envoy::extensions::filters::http::mcp::v3::McpOverride override_config;
override_config.mutable_max_request_body_size()->set_value(100);
auto route_config = std::make_shared<McpOverrideConfig>(override_config);

EXPECT_CALL(decoder_callbacks_, mostSpecificPerFilterConfig())
.WillRepeatedly(Return(route_config.get()));

Http::TestRequestHeaderMapImpl headers{{":method", "POST"},
{"content-type", "application/json"},
{"accept", "application/json"},
{"accept", "text/event-stream"}};

// Should use per-route limit of 100 bytes, not global 1024
EXPECT_CALL(decoder_callbacks_, setDecoderBufferLimit(100));
filter_->decodeHeaders(headers, false);

// Create a body that exceeds per-route limit (100) but under global (1024)
std::string json =
R"({"jsonrpc": "2.0", "method": "test", "params": {"key": "value", "extra": "data to exceed 100 bytes"}, "id": 1})";
Buffer::OwnedImpl buffer(json);
Buffer::OwnedImpl decoding_buffer;

EXPECT_CALL(decoder_callbacks_, addDecodedData(_, false))
.WillOnce([&decoding_buffer](Buffer::Instance& data, bool) { decoding_buffer.move(data); });
EXPECT_CALL(decoder_callbacks_, decodingBuffer()).WillRepeatedly(Return(&decoding_buffer));

// Should be rejected based on per-route limit
EXPECT_CALL(decoder_callbacks_,
sendLocalReply(Http::Code::PayloadTooLarge,
testing::HasSubstr("Request body size exceeds maximum allowed size"),
_, _, "mcp_filter_body_too_large"));

EXPECT_EQ(Http::FilterDataStatus::StopIterationNoBuffer, filter_->decodeData(buffer, true));
}

// Test per-route max body size override with larger limit
TEST_F(McpFilterTest, PerRouteMaxBodySizeLargerLimit) {
// Global config with 100 bytes
envoy::extensions::filters::http::mcp::v3::Mcp proto_config;
proto_config.mutable_max_request_body_size()->set_value(100);
config_ = std::make_shared<McpFilterConfig>(proto_config);
filter_ = std::make_unique<McpFilter>(config_);
filter_->setDecoderFilterCallbacks(decoder_callbacks_);

// Per-route config with larger limit (2048 bytes)
envoy::extensions::filters::http::mcp::v3::McpOverride override_config;
override_config.mutable_max_request_body_size()->set_value(2048);
auto route_config = std::make_shared<McpOverrideConfig>(override_config);

EXPECT_CALL(decoder_callbacks_, mostSpecificPerFilterConfig())
.WillRepeatedly(Return(route_config.get()));

Http::TestRequestHeaderMapImpl headers{{":method", "POST"},
{"content-type", "application/json"},
{"accept", "application/json"},
{"accept", "text/event-stream"}};

// Should use per-route limit of 2048 bytes, not global 100
EXPECT_CALL(decoder_callbacks_, setDecoderBufferLimit(2048));
filter_->decodeHeaders(headers, false);

// Create a body that exceeds global limit (100) but under per-route (2048)
std::string json =
R"({"jsonrpc": "2.0", "method": "test", "params": {"key": "value", "extra": "data to exceed 100 bytes limit"}, "id": 1})";
Buffer::OwnedImpl buffer(json);
Buffer::OwnedImpl decoding_buffer;

EXPECT_CALL(decoder_callbacks_, addDecodedData(_, false))
.WillOnce([&decoding_buffer](Buffer::Instance& data, bool) { decoding_buffer.move(data); });
EXPECT_CALL(decoder_callbacks_, decodingBuffer()).WillRepeatedly(Return(&decoding_buffer));
EXPECT_CALL(decoder_callbacks_.stream_info_, setDynamicMetadata("mcp_proxy", _));

// Should succeed based on per-route limit
EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(buffer, true));
}

// Test fallback to global max body size when no per-route override
TEST_F(McpFilterTest, PerRouteMaxBodySizeFallbackToGlobal) {
// Global config with 512 bytes
envoy::extensions::filters::http::mcp::v3::Mcp proto_config;
proto_config.mutable_max_request_body_size()->set_value(512);
config_ = std::make_shared<McpFilterConfig>(proto_config);
filter_ = std::make_unique<McpFilter>(config_);
filter_->setDecoderFilterCallbacks(decoder_callbacks_);

// Per-route config WITHOUT max_request_body_size override (only traffic mode)
envoy::extensions::filters::http::mcp::v3::McpOverride override_config;
override_config.set_traffic_mode(envoy::extensions::filters::http::mcp::v3::Mcp::PASS_THROUGH);
auto route_config = std::make_shared<McpOverrideConfig>(override_config);

EXPECT_CALL(decoder_callbacks_, mostSpecificPerFilterConfig())
.WillRepeatedly(Return(route_config.get()));

Http::TestRequestHeaderMapImpl headers{{":method", "POST"},
{"content-type", "application/json"},
{"accept", "application/json"},
{"accept", "text/event-stream"}};

// Should fallback to global limit of 512 bytes
EXPECT_CALL(decoder_callbacks_, setDecoderBufferLimit(512));
filter_->decodeHeaders(headers, false);

// Create a valid JSON-RPC body under 512 bytes
std::string json = R"({"jsonrpc": "2.0", "method": "test", "params": {"key": "value"}, "id": 1})";
Buffer::OwnedImpl buffer(json);
Buffer::OwnedImpl decoding_buffer;

EXPECT_CALL(decoder_callbacks_, addDecodedData(_, false))
.WillOnce([&decoding_buffer](Buffer::Instance& data, bool) { decoding_buffer.move(data); });
EXPECT_CALL(decoder_callbacks_, decodingBuffer()).WillRepeatedly(Return(&decoding_buffer));
EXPECT_CALL(decoder_callbacks_.stream_info_, setDynamicMetadata("mcp_proxy", _));

// Should succeed based on global limit
EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(buffer, true));
}

} // namespace
} // namespace Mcp
} // namespace HttpFilters
Expand Down