From 7b4f5ae07cbef2c4528088aaf51492a0b8e56049 Mon Sep 17 00:00:00 2001 From: Boteng Yao Date: Wed, 26 Nov 2025 21:17:08 +0000 Subject: [PATCH 1/3] add stats Signed-off-by: Boteng Yao --- source/extensions/filters/http/mcp/config.cc | 7 +++-- .../extensions/filters/http/mcp/mcp_filter.cc | 19 +++++++++++++ .../extensions/filters/http/mcp/mcp_filter.h | 28 +++++++++++++++---- .../filters/http/mcp/mcp_filter_test.cc | 27 +++++++++--------- 4 files changed, 59 insertions(+), 22 deletions(-) diff --git a/source/extensions/filters/http/mcp/config.cc b/source/extensions/filters/http/mcp/config.cc index 545b5873644a5..50f5d97a3e11f 100644 --- a/source/extensions/filters/http/mcp/config.cc +++ b/source/extensions/filters/http/mcp/config.cc @@ -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(proto_config); + auto config = std::make_shared(proto_config, stats_prefix, + context.serverFactoryContext().scope()); return [config](Http::FilterChainFactoryCallbacks& callbacks) -> void { callbacks.addStreamFilter(std::make_shared(config)); diff --git a/source/extensions/filters/http/mcp/mcp_filter.cc b/source/extensions/filters/http/mcp/mcp_filter.cc index cf193c42ac677..5e062d7272ab0 100644 --- a/source/extensions/filters/http/mcp/mcp_filter.cc +++ b/source/extensions/filters/http/mcp/mcp_filter.cc @@ -10,6 +10,21 @@ namespace Extensions { namespace HttpFilters { namespace Mcp { +namespace { +McpFilterStats generateStats(const std::string& prefix, Stats::Scope& scope) { + const std::string final_prefix = prefix + "mcp."; + return McpFilterStats{ALL_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) { @@ -111,6 +126,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; @@ -134,6 +150,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"), @@ -155,6 +172,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, ""); @@ -173,6 +191,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"); diff --git a/source/extensions/filters/http/mcp/mcp_filter.h b/source/extensions/filters/http/mcp/mcp_filter.h index ddda51f6df4a9..c4b032d11221d 100644 --- a/source/extensions/filters/http/mcp/mcp_filter.h +++ b/source/extensions/filters/http/mcp/mcp_filter.h @@ -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" @@ -26,17 +28,28 @@ namespace McpConstants { constexpr absl::string_view JsonRpcVersion = "2.0"; } // namespace McpConstants +/** + * All MCP filter stats. @see stats_macros.h + */ +#define ALL_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 { + ALL_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_; @@ -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_; }; /** diff --git a/test/extensions/filters/http/mcp/mcp_filter_test.cc b/test/extensions/filters/http/mcp/mcp_filter_test.cc index 28897cf26b1be..e6a6a1b85e97a 100644 --- a/test/extensions/filters/http/mcp/mcp_filter_test.cc +++ b/test/extensions/filters/http/mcp/mcp_filter_test.cc @@ -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(proto_config); + config_ = std::make_shared(proto_config, "test.", factory_context_.scope()); filter_ = std::make_unique(config_); filter_->setDecoderFilterCallbacks(decoder_callbacks_); filter_->setEncoderFilterCallbacks(encoder_callbacks_); @@ -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(proto_config); + config_ = std::make_shared(proto_config, "test.", factory_context_.scope()); filter_ = std::make_unique(config_); filter_->setDecoderFilterCallbacks(decoder_callbacks_); filter_->setEncoderFilterCallbacks(encoder_callbacks_); @@ -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(proto_config); + config_ = std::make_shared(proto_config, "test.", factory_context_.scope()); filter_ = std::make_unique(config_); filter_->setDecoderFilterCallbacks(decoder_callbacks_); filter_->setEncoderFilterCallbacks(encoder_callbacks_); } protected: + NiceMock factory_context_; NiceMock decoder_callbacks_; NiceMock encoder_callbacks_; McpFilterConfigSharedPtr config_; @@ -286,7 +287,7 @@ 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(proto_config); + auto config = std::make_shared(proto_config, "test.", factory_context_.scope()); EXPECT_EQ(8192u, config->maxRequestBodySize()); } @@ -294,7 +295,7 @@ TEST_F(McpFilterTest, DefaultMaxBodySizeIsEightKB) { 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(proto_config); + auto config = std::make_shared(proto_config, "test.", factory_context_.scope()); EXPECT_EQ(16384u, config->maxRequestBodySize()); } @@ -302,7 +303,7 @@ TEST_F(McpFilterTest, CustomMaxBodySizeConfiguration) { 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(proto_config); + auto config = std::make_shared(proto_config, "test.", factory_context_.scope()); EXPECT_EQ(0u, config->maxRequestBodySize()); } @@ -310,7 +311,7 @@ TEST_F(McpFilterTest, DisabledMaxBodySizeConfiguration) { 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(proto_config); + config_ = std::make_shared(proto_config, "test.", factory_context_.scope()); filter_ = std::make_unique(config_); filter_->setDecoderFilterCallbacks(decoder_callbacks_); @@ -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(proto_config); + config_ = std::make_shared(proto_config, "test.", factory_context_.scope()); filter_ = std::make_unique(config_); filter_->setDecoderFilterCallbacks(decoder_callbacks_); @@ -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(proto_config); + config_ = std::make_shared(proto_config, "test.", factory_context_.scope()); filter_ = std::make_unique(config_); filter_->setDecoderFilterCallbacks(decoder_callbacks_); @@ -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(proto_config); + config_ = std::make_shared(proto_config, "test.", factory_context_.scope()); filter_ = std::make_unique(config_); filter_->setDecoderFilterCallbacks(decoder_callbacks_); @@ -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(proto_config); + config_ = std::make_shared(proto_config, "test.", factory_context_.scope()); filter_ = std::make_unique(config_); filter_->setDecoderFilterCallbacks(decoder_callbacks_); @@ -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(proto_config); + config_ = std::make_shared(proto_config, "test.", factory_context_.scope()); filter_ = std::make_unique(config_); filter_->setDecoderFilterCallbacks(decoder_callbacks_); @@ -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(proto_config); + config_ = std::make_shared(proto_config, "test.", factory_context_.scope()); filter_ = std::make_unique(config_); filter_->setDecoderFilterCallbacks(decoder_callbacks_); From 95029961be010ce723adf0d3b618f8b4bb391b23 Mon Sep 17 00:00:00 2001 From: Boteng Yao Date: Wed, 26 Nov 2025 21:26:24 +0000 Subject: [PATCH 2/3] minors Signed-off-by: Boteng Yao --- source/extensions/filters/http/mcp/config.cc | 2 +- source/extensions/filters/http/mcp/mcp_filter.cc | 7 ++++--- source/extensions/filters/http/mcp/mcp_filter.h | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/source/extensions/filters/http/mcp/config.cc b/source/extensions/filters/http/mcp/config.cc index 50f5d97a3e11f..662d6babd2f56 100644 --- a/source/extensions/filters/http/mcp/config.cc +++ b/source/extensions/filters/http/mcp/config.cc @@ -14,7 +14,7 @@ Http::FilterFactoryCb McpFilterConfigFactory::createFilterFactoryFromProtoTyped( const std::string& stats_prefix, Server::Configuration::FactoryContext& context) { auto config = std::make_shared(proto_config, stats_prefix, - context.serverFactoryContext().scope()); + context.serverFactoryContext().scope()); return [config](Http::FilterChainFactoryCallbacks& callbacks) -> void { callbacks.addStreamFilter(std::make_shared(config)); diff --git a/source/extensions/filters/http/mcp/mcp_filter.cc b/source/extensions/filters/http/mcp/mcp_filter.cc index 5e062d7272ab0..0ec59e8e769c4 100644 --- a/source/extensions/filters/http/mcp/mcp_filter.cc +++ b/source/extensions/filters/http/mcp/mcp_filter.cc @@ -12,14 +12,15 @@ namespace Mcp { namespace { McpFilterStats generateStats(const std::string& prefix, Stats::Scope& scope) { - const std::string final_prefix = prefix + "mcp."; - return McpFilterStats{ALL_MCP_FILTER_STATS(POOL_COUNTER_PREFIX(scope, final_prefix))}; + 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()), + : 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 diff --git a/source/extensions/filters/http/mcp/mcp_filter.h b/source/extensions/filters/http/mcp/mcp_filter.h index c4b032d11221d..37cad9be7230f 100644 --- a/source/extensions/filters/http/mcp/mcp_filter.h +++ b/source/extensions/filters/http/mcp/mcp_filter.h @@ -31,7 +31,7 @@ constexpr absl::string_view JsonRpcVersion = "2.0"; /** * All MCP filter stats. @see stats_macros.h */ -#define ALL_MCP_FILTER_STATS(COUNTER) \ +#define MCP_FILTER_STATS(COUNTER) \ COUNTER(requests_rejected) \ COUNTER(invalid_json) \ COUNTER(body_too_large) @@ -40,7 +40,7 @@ constexpr absl::string_view JsonRpcVersion = "2.0"; * Struct definition for MCP filter stats. @see stats_macros.h */ struct McpFilterStats { - ALL_MCP_FILTER_STATS(GENERATE_COUNTER_STRUCT) + MCP_FILTER_STATS(GENERATE_COUNTER_STRUCT) }; /** From fd54982687c09ebd55b50137879b78593235c7b0 Mon Sep 17 00:00:00 2001 From: Boteng Yao Date: Wed, 26 Nov 2025 22:42:36 +0000 Subject: [PATCH 3/3] fix format Signed-off-by: Boteng Yao --- source/extensions/filters/http/mcp/mcp_filter.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/extensions/filters/http/mcp/mcp_filter.h b/source/extensions/filters/http/mcp/mcp_filter.h index 37cad9be7230f..22d56c110b8ad 100644 --- a/source/extensions/filters/http/mcp/mcp_filter.h +++ b/source/extensions/filters/http/mcp/mcp_filter.h @@ -31,7 +31,7 @@ constexpr absl::string_view JsonRpcVersion = "2.0"; /** * All MCP filter stats. @see stats_macros.h */ -#define MCP_FILTER_STATS(COUNTER) \ +#define MCP_FILTER_STATS(COUNTER) \ COUNTER(requests_rejected) \ COUNTER(invalid_json) \ COUNTER(body_too_large)