diff --git a/source/extensions/filters/http/mcp/config.cc b/source/extensions/filters/http/mcp/config.cc index 545b5873644a5..662d6babd2f56 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..0ec59e8e769c4 100644 --- a/source/extensions/filters/http/mcp/mcp_filter.cc +++ b/source/extensions/filters/http/mcp/mcp_filter.cc @@ -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) { @@ -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; @@ -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"), @@ -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, ""); @@ -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"); diff --git a/source/extensions/filters/http/mcp/mcp_filter.h b/source/extensions/filters/http/mcp/mcp_filter.h index ddda51f6df4a9..22d56c110b8ad 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 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_; @@ -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_);