From 5c6be3fc1d3ba7770d04afa1e77cf1bd64a07357 Mon Sep 17 00:00:00 2001 From: Boteng Yao Date: Sun, 15 Feb 2026 06:50:56 +0000 Subject: [PATCH 1/3] relax json requirement Signed-off-by: Boteng Yao --- changelogs/current.yaml | 5 +++++ source/extensions/filters/http/mcp/mcp_filter.cc | 2 +- .../filters/http/mcp/mcp_filter_test.cc | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/changelogs/current.yaml b/changelogs/current.yaml index d1b1e4761d874..d0dd44e04d7e8 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -55,6 +55,11 @@ minor_behavior_changes: change: | OpenTelemetry :ref:`SinkConfig ` stopped reporting empty delta counters and histograms. +- area: mcp + change: | + Relaxed the MCP filter POST Content-Type check from an exact match on ``application/json`` to a + prefix match, so that ``application/json; charset=utf-8`` and similar media-type parameters are + accepted. bug_fixes: # *Changes expected to improve the state of the world and are unlikely to have negative effects* diff --git a/source/extensions/filters/http/mcp/mcp_filter.cc b/source/extensions/filters/http/mcp/mcp_filter.cc index cb6d2f49ba118..4d3f25fd745b0 100644 --- a/source/extensions/filters/http/mcp/mcp_filter.cc +++ b/source/extensions/filters/http/mcp/mcp_filter.cc @@ -62,7 +62,7 @@ bool McpFilter::isValidMcpPostRequest(const Http::RequestHeaderMap& headers) con // Check if this is a POST request with JSON content bool is_post_request = headers.getMethodValue() == Http::Headers::get().MethodValues.Post && - headers.getContentTypeValue() == Http::Headers::get().ContentTypeValues.Json; + absl::StartsWith(headers.getContentTypeValue(), Http::Headers::get().ContentTypeValues.Json); if (!is_post_request) { return false; diff --git a/test/extensions/filters/http/mcp/mcp_filter_test.cc b/test/extensions/filters/http/mcp/mcp_filter_test.cc index 9e7b406f515cd..51689abc91772 100644 --- a/test/extensions/filters/http/mcp/mcp_filter_test.cc +++ b/test/extensions/filters/http/mcp/mcp_filter_test.cc @@ -1244,6 +1244,22 @@ TEST_F(McpFilterTest, BothStorageModeSetsBothTargets) { EXPECT_EQ(filter_state_obj->method().value(), "tools/call"); } +// Test that POST with Content-Type "application/json; charset=utf-8" is accepted +TEST_F(McpFilterTest, PostWithJsonCharsetContentTypeAccepted) { + Http::TestRequestHeaderMapImpl headers{{":method", "POST"}, + {"content-type", "application/json; charset=utf-8"}, + {"accept", "application/json"}, + {"accept", "text/event-stream"}}; + + filter_->decodeHeaders(headers, false); + + std::string json = R"({"jsonrpc": "2.0", "method": "test", "params": {"key": "value"}, "id": 1})"; + Buffer::OwnedImpl buffer(json); + + EXPECT_CALL(decoder_callbacks_.stream_info_, setDynamicMetadata("envoy.filters.http.mcp", _)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(buffer, true)); +} + } // namespace } // namespace Mcp } // namespace HttpFilters From ccecee510f6585ae815a3e2041a5a912c6267b04 Mon Sep 17 00:00:00 2001 From: Boteng Yao Date: Tue, 17 Feb 2026 17:22:51 +0000 Subject: [PATCH 2/3] address feedback Signed-off-by: Boteng Yao --- .../extensions/filters/http/mcp/mcp_filter.cc | 14 ++++-- .../filters/http/mcp/mcp_filter_test.cc | 48 +++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/source/extensions/filters/http/mcp/mcp_filter.cc b/source/extensions/filters/http/mcp/mcp_filter.cc index 47c58eff0b491..af4bfb7b4af75 100644 --- a/source/extensions/filters/http/mcp/mcp_filter.cc +++ b/source/extensions/filters/http/mcp/mcp_filter.cc @@ -72,10 +72,18 @@ bool McpFilter::isValidMcpSseRequest(const Http::RequestHeaderMap& headers) cons } bool McpFilter::isValidMcpPostRequest(const Http::RequestHeaderMap& headers) const { - // Check if this is a POST request with JSON content + // Check if this is a POST request with JSON content. + // Content-Type is JSON if it is exactly "application/json" or starts with + // "application/json" followed by ';' or ' ' (for parameters like charset). + // This rejects related but distinct types like application/json-patch+json. + const absl::string_view content_type = headers.getContentTypeValue(); + const auto& json_ct = Http::Headers::get().ContentTypeValues.Json; + bool is_json_content_type = + absl::StartsWith(content_type, json_ct) && + (content_type.size() == json_ct.size() || content_type[json_ct.size()] == ';' || + content_type[json_ct.size()] == ' '); bool is_post_request = - headers.getMethodValue() == Http::Headers::get().MethodValues.Post && - absl::StartsWith(headers.getContentTypeValue(), Http::Headers::get().ContentTypeValues.Json); + headers.getMethodValue() == Http::Headers::get().MethodValues.Post && is_json_content_type; if (!is_post_request) { return false; diff --git a/test/extensions/filters/http/mcp/mcp_filter_test.cc b/test/extensions/filters/http/mcp/mcp_filter_test.cc index 2ddc99bc877dc..03edbb4a3c8ff 100644 --- a/test/extensions/filters/http/mcp/mcp_filter_test.cc +++ b/test/extensions/filters/http/mcp/mcp_filter_test.cc @@ -1258,6 +1258,54 @@ TEST_F(McpFilterTest, PostWithJsonCharsetContentTypeAccepted) { EXPECT_CALL(decoder_callbacks_.stream_info_, setDynamicMetadata("envoy.filters.http.mcp", _)); EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(buffer, true)); +} + +// Test that POST with Content-Type "application/json;charset=utf-8" (no space) is accepted +TEST_F(McpFilterTest, PostWithJsonCharsetNoSpaceContentTypeAccepted) { + Http::TestRequestHeaderMapImpl headers{{":method", "POST"}, + {"content-type", "application/json;charset=utf-8"}, + {"accept", "application/json"}, + {"accept", "text/event-stream"}}; + + filter_->decodeHeaders(headers, false); + + std::string json = R"({"jsonrpc": "2.0", "method": "test", "params": {"key": "value"}, "id": 1})"; + Buffer::OwnedImpl buffer(json); + + EXPECT_CALL(decoder_callbacks_.stream_info_, setDynamicMetadata("envoy.filters.http.mcp", _)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(buffer, true)); +} + +// Test that POST with Content-Type "application/json-patch+json" (RFC 6902) is rejected in +// REJECT_NO_MCP mode because it is not plain application/json. +TEST_F(McpFilterTest, PostWithJsonPatchContentTypeRejectedInRejectMode) { + setupRejectMode(); + + Http::TestRequestHeaderMapImpl headers{{":method", "POST"}, + {"content-type", "application/json-patch+json"}, + {"accept", "application/json"}, + {"accept", "text/event-stream"}}; + + EXPECT_CALL(decoder_callbacks_, + sendLocalReply(Http::Code::BadRequest, "Only MCP traffic is allowed", _, _, _)); + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, filter_->decodeHeaders(headers, true)); +} + +// Test that POST with Content-Type "application/jsonl" is rejected in REJECT_NO_MCP mode +// because it is not plain application/json. +TEST_F(McpFilterTest, PostWithJsonlContentTypeRejectedInRejectMode) { + setupRejectMode(); + + Http::TestRequestHeaderMapImpl headers{{":method", "POST"}, + {"content-type", "application/jsonl"}, + {"accept", "application/json"}, + {"accept", "text/event-stream"}}; + + EXPECT_CALL(decoder_callbacks_, + sendLocalReply(Http::Code::BadRequest, "Only MCP traffic is allowed", _, _, _)); + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, filter_->decodeHeaders(headers, true)); +} + // Test REJECT_NO_MCP mode - allow DELETE with MCP-Session-Id (session termination) TEST_F(McpFilterTest, RejectModeAllowsDeleteWithSessionId) { setupRejectMode(); From 314ef98a24d34a07c600cb04cc7cda868b6abb74 Mon Sep 17 00:00:00 2001 From: Boteng Yao Date: Tue, 17 Feb 2026 17:43:49 +0000 Subject: [PATCH 3/3] fix ci Signed-off-by: Boteng Yao --- tools/spelling/spelling_dictionary.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/spelling/spelling_dictionary.txt b/tools/spelling/spelling_dictionary.txt index 9650e9fba7002..5456db22937cc 100644 --- a/tools/spelling/spelling_dictionary.txt +++ b/tools/spelling/spelling_dictionary.txt @@ -1663,6 +1663,7 @@ effective_cpus cpus ja3 ja4 +jsonl HBONE waypoint ztunnel