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
7 changes: 4 additions & 3 deletions source/extensions/filters/http/mcp/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ namespace HttpFilters {
namespace Mcp {

Http::FilterFactoryCb McpFilterConfigFactory::createFilterFactoryFromProtoTyped(
const envoy::extensions::filters::http::mcp::v3::Mcp& proto_config, const std::string&,
Server::Configuration::FactoryContext&) {
const envoy::extensions::filters::http::mcp::v3::Mcp& proto_config,
const std::string& stats_prefix, Server::Configuration::FactoryContext& context) {

auto config = std::make_shared<McpFilterConfig>(proto_config);
auto config = std::make_shared<McpFilterConfig>(proto_config, stats_prefix,
context.serverFactoryContext().scope());

return [config](Http::FilterChainFactoryCallbacks& callbacks) -> void {
callbacks.addStreamFilter(std::make_shared<McpFilter>(config));
Expand Down
20 changes: 20 additions & 0 deletions source/extensions/filters/http/mcp/mcp_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ namespace Extensions {
namespace HttpFilters {
namespace Mcp {

namespace {
McpFilterStats generateStats(const std::string& prefix, Stats::Scope& scope) {
const std::string final_prefix = absl::StrCat(prefix, "mcp.");
return McpFilterStats{MCP_FILTER_STATS(POOL_COUNTER_PREFIX(scope, final_prefix))};
}
} // namespace

McpFilterConfig::McpFilterConfig(const envoy::extensions::filters::http::mcp::v3::Mcp& proto_config,
const std::string& stats_prefix, Stats::Scope& scope)
: traffic_mode_(proto_config.traffic_mode()),
clear_route_cache_(proto_config.clear_route_cache()),
max_request_body_size_(proto_config.has_max_request_body_size()
? proto_config.max_request_body_size().value()
: 8192), // Default: 8KB
stats_(generateStats(stats_prefix, scope)) {}

bool McpFilter::isValidMcpSseRequest(const Http::RequestHeaderMap& headers) const {
// Check if this is a GET request for SSE stream
if (headers.getMethodValue() != Http::Headers::get().MethodValues.Get) {
Expand Down Expand Up @@ -111,6 +127,7 @@ Http::FilterHeadersStatus McpFilter::decodeHeaders(Http::RequestHeaderMap& heade

if (!is_mcp_request_ && shouldRejectRequest()) {
ENVOY_LOG(debug, "rejecting non-MCP traffic");
config_->stats().requests_rejected_.inc();
decoder_callbacks_->sendLocalReply(Http::Code::BadRequest, "Only MCP traffic is allowed",
nullptr, absl::nullopt, "mcp_filter_reject_no_mcp");
return Http::FilterHeadersStatus::StopIteration;
Expand All @@ -134,6 +151,7 @@ Http::FilterDataStatus McpFilter::decodeData(Buffer::Instance& data, bool end_st

if (total_size > max_size) {
ENVOY_LOG(debug, "request body size {} exceeds maximum {}", total_size, max_size);
config_->stats().body_too_large_.inc();
decoder_callbacks_->sendLocalReply(
Http::Code::PayloadTooLarge,
absl::StrCat("Request body size exceeds maximum allowed size of ", max_size, " bytes"),
Expand All @@ -155,6 +173,7 @@ Http::FilterDataStatus McpFilter::decodeData(Buffer::Instance& data, bool end_st
if (!status.ok()) {
is_mcp_request_ = false;
ENVOY_LOG(debug, "failed to parse the JSON");
config_->stats().invalid_json_.inc();
decoder_callbacks_->sendLocalReply(Envoy::Http::Code::BadRequest,
"Request body is not a valid JSON.", nullptr,
absl::nullopt, "");
Expand All @@ -173,6 +192,7 @@ Http::FilterDataStatus McpFilter::decodeData(Buffer::Instance& data, bool end_st
is_mcp_request_ = false;
ENVOY_LOG(debug, "non-JSON-RPC 2.0 request is detected");
if (shouldRejectRequest()) {
config_->stats().requests_rejected_.inc();
decoder_callbacks_->sendLocalReply(Http::Code::BadRequest,
"request must be a valid JSON-RPC 2.0 message for MCP",
nullptr, absl::nullopt, "mcp_filter_not_jsonrpc");
Expand Down
28 changes: 22 additions & 6 deletions source/extensions/filters/http/mcp/mcp_filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#include "envoy/extensions/filters/http/mcp/v3/mcp.pb.h"
#include "envoy/http/filter.h"
#include "envoy/server/filter_config.h"
#include "envoy/stats/scope.h"
#include "envoy/stats/stats_macros.h"

#include "source/common/common/logger.h"
#include "source/common/protobuf/protobuf.h"
Expand All @@ -26,17 +28,28 @@ namespace McpConstants {
constexpr absl::string_view JsonRpcVersion = "2.0";
} // namespace McpConstants

/**
* All MCP filter stats. @see stats_macros.h
*/
#define MCP_FILTER_STATS(COUNTER) \
COUNTER(requests_rejected) \
COUNTER(invalid_json) \
COUNTER(body_too_large)

/**
* Struct definition for MCP filter stats. @see stats_macros.h
*/
struct McpFilterStats {
MCP_FILTER_STATS(GENERATE_COUNTER_STRUCT)
};

/**
* Configuration for the MCP filter.
*/
class McpFilterConfig {
public:
explicit McpFilterConfig(const envoy::extensions::filters::http::mcp::v3::Mcp& proto_config)
: traffic_mode_(proto_config.traffic_mode()),
clear_route_cache_(proto_config.clear_route_cache()),
max_request_body_size_(proto_config.has_max_request_body_size()
? proto_config.max_request_body_size().value()
: 8192) {} // Default: 8KB
McpFilterConfig(const envoy::extensions::filters::http::mcp::v3::Mcp& proto_config,
const std::string& stats_prefix, Stats::Scope& scope);

envoy::extensions::filters::http::mcp::v3::Mcp::TrafficMode trafficMode() const {
return traffic_mode_;
Expand All @@ -50,10 +63,13 @@ class McpFilterConfig {

uint32_t maxRequestBodySize() const { return max_request_body_size_; }

McpFilterStats& stats() { return stats_; }

private:
const envoy::extensions::filters::http::mcp::v3::Mcp::TrafficMode traffic_mode_;
const bool clear_route_cache_;
const uint32_t max_request_body_size_;
McpFilterStats stats_;
};

/**
Expand Down
27 changes: 14 additions & 13 deletions test/extensions/filters/http/mcp/mcp_filter_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class McpFilterTest : public testing::Test {
// Default config with PASS_THROUGH mode
envoy::extensions::filters::http::mcp::v3::Mcp proto_config;
proto_config.set_traffic_mode(envoy::extensions::filters::http::mcp::v3::Mcp::PASS_THROUGH);
config_ = std::make_shared<McpFilterConfig>(proto_config);
config_ = std::make_shared<McpFilterConfig>(proto_config, "test.", factory_context_.scope());
filter_ = std::make_unique<McpFilter>(config_);
filter_->setDecoderFilterCallbacks(decoder_callbacks_);
filter_->setEncoderFilterCallbacks(encoder_callbacks_);
Expand All @@ -32,7 +32,7 @@ class McpFilterTest : public testing::Test {
void setupRejectMode() {
envoy::extensions::filters::http::mcp::v3::Mcp proto_config;
proto_config.set_traffic_mode(envoy::extensions::filters::http::mcp::v3::Mcp::REJECT_NO_MCP);
config_ = std::make_shared<McpFilterConfig>(proto_config);
config_ = std::make_shared<McpFilterConfig>(proto_config, "test.", factory_context_.scope());
filter_ = std::make_unique<McpFilter>(config_);
filter_->setDecoderFilterCallbacks(decoder_callbacks_);
filter_->setEncoderFilterCallbacks(encoder_callbacks_);
Expand All @@ -42,13 +42,14 @@ class McpFilterTest : public testing::Test {
envoy::extensions::filters::http::mcp::v3::Mcp proto_config;
proto_config.set_traffic_mode(envoy::extensions::filters::http::mcp::v3::Mcp::PASS_THROUGH);
proto_config.set_clear_route_cache(clear_route_cache);
config_ = std::make_shared<McpFilterConfig>(proto_config);
config_ = std::make_shared<McpFilterConfig>(proto_config, "test.", factory_context_.scope());
filter_ = std::make_unique<McpFilter>(config_);
filter_->setDecoderFilterCallbacks(decoder_callbacks_);
filter_->setEncoderFilterCallbacks(encoder_callbacks_);
}

protected:
NiceMock<Server::Configuration::MockFactoryContext> factory_context_;
NiceMock<Http::MockStreamDecoderFilterCallbacks> decoder_callbacks_;
NiceMock<Http::MockStreamEncoderFilterCallbacks> encoder_callbacks_;
McpFilterConfigSharedPtr config_;
Expand Down Expand Up @@ -286,31 +287,31 @@ TEST_F(McpFilterTest, PostWithWrongContentType) {
TEST_F(McpFilterTest, DefaultMaxBodySizeIsEightKB) {
envoy::extensions::filters::http::mcp::v3::Mcp proto_config;
// Don't set max_request_body_size, should default to 8KB
auto config = std::make_shared<McpFilterConfig>(proto_config);
auto config = std::make_shared<McpFilterConfig>(proto_config, "test.", factory_context_.scope());
EXPECT_EQ(8192u, config->maxRequestBodySize());
}

// Test custom max body size configuration
TEST_F(McpFilterTest, CustomMaxBodySizeConfiguration) {
envoy::extensions::filters::http::mcp::v3::Mcp proto_config;
proto_config.mutable_max_request_body_size()->set_value(16384);
auto config = std::make_shared<McpFilterConfig>(proto_config);
auto config = std::make_shared<McpFilterConfig>(proto_config, "test.", factory_context_.scope());
EXPECT_EQ(16384u, config->maxRequestBodySize());
}

// Test disabled max body size (0 = no limit)
TEST_F(McpFilterTest, DisabledMaxBodySizeConfiguration) {
envoy::extensions::filters::http::mcp::v3::Mcp proto_config;
proto_config.mutable_max_request_body_size()->set_value(0);
auto config = std::make_shared<McpFilterConfig>(proto_config);
auto config = std::make_shared<McpFilterConfig>(proto_config, "test.", factory_context_.scope());
EXPECT_EQ(0u, config->maxRequestBodySize());
}

// Test request body under the limit succeeds
TEST_F(McpFilterTest, RequestBodyUnderLimitSucceeds) {
envoy::extensions::filters::http::mcp::v3::Mcp proto_config;
proto_config.mutable_max_request_body_size()->set_value(1024); // 1KB limit
config_ = std::make_shared<McpFilterConfig>(proto_config);
config_ = std::make_shared<McpFilterConfig>(proto_config, "test.", factory_context_.scope());
filter_ = std::make_unique<McpFilter>(config_);
filter_->setDecoderFilterCallbacks(decoder_callbacks_);

Expand Down Expand Up @@ -339,7 +340,7 @@ TEST_F(McpFilterTest, RequestBodyUnderLimitSucceeds) {
TEST_F(McpFilterTest, RequestBodyExceedingLimitRejected) {
envoy::extensions::filters::http::mcp::v3::Mcp proto_config;
proto_config.mutable_max_request_body_size()->set_value(100); // Very small limit
config_ = std::make_shared<McpFilterConfig>(proto_config);
config_ = std::make_shared<McpFilterConfig>(proto_config, "test.", factory_context_.scope());
filter_ = std::make_unique<McpFilter>(config_);
filter_->setDecoderFilterCallbacks(decoder_callbacks_);

Expand Down Expand Up @@ -373,7 +374,7 @@ TEST_F(McpFilterTest, RequestBodyExceedingLimitRejected) {
TEST_F(McpFilterTest, RequestBodyWithDisabledLimitAllowsLargeBodies) {
envoy::extensions::filters::http::mcp::v3::Mcp proto_config;
proto_config.mutable_max_request_body_size()->set_value(0); // Disable limit
config_ = std::make_shared<McpFilterConfig>(proto_config);
config_ = std::make_shared<McpFilterConfig>(proto_config, "test.", factory_context_.scope());
filter_ = std::make_unique<McpFilter>(config_);
filter_->setDecoderFilterCallbacks(decoder_callbacks_);

Expand Down Expand Up @@ -406,7 +407,7 @@ TEST_F(McpFilterTest, RequestBodyWithDisabledLimitAllowsLargeBodies) {
TEST_F(McpFilterTest, RequestBodyExactlyAtLimitSucceeds) {
envoy::extensions::filters::http::mcp::v3::Mcp proto_config;
proto_config.mutable_max_request_body_size()->set_value(100); // 100 byte limit
config_ = std::make_shared<McpFilterConfig>(proto_config);
config_ = std::make_shared<McpFilterConfig>(proto_config, "test.", factory_context_.scope());
filter_ = std::make_unique<McpFilter>(config_);
filter_->setDecoderFilterCallbacks(decoder_callbacks_);

Expand Down Expand Up @@ -446,7 +447,7 @@ TEST_F(McpFilterTest, RequestBodyExactlyAtLimitSucceeds) {
TEST_F(McpFilterTest, BufferLimitSetForValidMcpPostRequest) {
envoy::extensions::filters::http::mcp::v3::Mcp proto_config;
proto_config.mutable_max_request_body_size()->set_value(8192);
config_ = std::make_shared<McpFilterConfig>(proto_config);
config_ = std::make_shared<McpFilterConfig>(proto_config, "test.", factory_context_.scope());
filter_ = std::make_unique<McpFilter>(config_);
filter_->setDecoderFilterCallbacks(decoder_callbacks_);

Expand All @@ -463,7 +464,7 @@ TEST_F(McpFilterTest, BufferLimitSetForValidMcpPostRequest) {
TEST_F(McpFilterTest, BufferLimitNotSetWhenDisabled) {
envoy::extensions::filters::http::mcp::v3::Mcp proto_config;
proto_config.mutable_max_request_body_size()->set_value(0); // Disabled
config_ = std::make_shared<McpFilterConfig>(proto_config);
config_ = std::make_shared<McpFilterConfig>(proto_config, "test.", factory_context_.scope());
filter_ = std::make_unique<McpFilter>(config_);
filter_->setDecoderFilterCallbacks(decoder_callbacks_);

Expand All @@ -481,7 +482,7 @@ TEST_F(McpFilterTest, BodySizeLimitInPassThroughMode) {
envoy::extensions::filters::http::mcp::v3::Mcp proto_config;
proto_config.set_traffic_mode(envoy::extensions::filters::http::mcp::v3::Mcp::PASS_THROUGH);
proto_config.mutable_max_request_body_size()->set_value(50); // Small limit
config_ = std::make_shared<McpFilterConfig>(proto_config);
config_ = std::make_shared<McpFilterConfig>(proto_config, "test.", factory_context_.scope());
filter_ = std::make_unique<McpFilter>(config_);
filter_->setDecoderFilterCallbacks(decoder_callbacks_);

Expand Down