From e9c0ea26ef6e06eded78dff14f78fc874b866959 Mon Sep 17 00:00:00 2001 From: HarshPopat23 Date: Fri, 16 Oct 2026 10:12:24 +0530 Subject: [PATCH] Reject empty metadata keys in MCP validation MCP metadata keys without a prefix must begin and end with an alphanumeric character. Previously, metadata_key allowed an empty string when prefix_required was false because name.empty() evaluated to true. Hold unprefixed keys to having at least one valid character, rejecting empty keys. Signed-off-by: HarshPopat23 --- src/core/mcp/validation.h | 2 +- test/mcp/mcp_headers_test.cc | 9 +++++++++ test/mcp/mcp_notifications_test.cc | 13 +++++++++++++ test/mcp/mcp_results_test.cc | 18 ++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/core/mcp/validation.h b/src/core/mcp/validation.h index 53accb05a..4883b43a1 100644 --- a/src/core/mcp/validation.h +++ b/src/core/mcp/validation.h @@ -314,7 +314,7 @@ inline auto metadata_key(const JSON::StringView key, const bool prefix_required) const auto slash{key.find('/')}; JSON::StringView name{key}; if (slash == JSON::StringView::npos) { - if (prefix_required) { + if (prefix_required || key.empty()) { return false; } } else { diff --git a/test/mcp/mcp_headers_test.cc b/test/mcp/mcp_headers_test.cc index 439c2b79e..392b19d85 100644 --- a/test/mcp/mcp_headers_test.cc +++ b/test/mcp/mcp_headers_test.cc @@ -3738,6 +3738,15 @@ TEST(request_meta_validation_rejects_a_prefix_closing_on_a_dot) { sourcemeta::core::MCPRequestMetaStatus::InvalidMetaKey); } +TEST(request_meta_validation_rejects_an_empty_metadata_key) { + const auto envelope{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", "id": 1, "method": "tools/list", + "params": { "_meta": { "": true } } + })JSON")}; + EXPECT_EQ(sourcemeta::core::mcp_validate_request_meta(envelope).first, + sourcemeta::core::MCPRequestMetaStatus::InvalidMetaKey); +} + // A value that cannot travel in a header as it stands is carried base64 // encoded between the markers, so what the comparison sees is what the // encoding spells out rather than the encoded form itself diff --git a/test/mcp/mcp_notifications_test.cc b/test/mcp/mcp_notifications_test.cc index b6a014611..8545a668a 100644 --- a/test/mcp/mcp_notifications_test.cc +++ b/test/mcp/mcp_notifications_test.cc @@ -817,6 +817,19 @@ TEST(make_notification_rejects_malformed_metadata_key_on_2026_07_28) { "Invalid notification metadata"); } +TEST(make_notification_rejects_empty_metadata_key_on_2026_07_28) { + EXPECT_MCP_REJECTS(sourcemeta::core::mcp_make_notification( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, + false, "notifications/cancelled", + sourcemeta::core::parse_json( + R"JSON({ + "requestId": 1, + "_meta": { "": 1 } + })JSON"), + std::nullopt), + "Invalid notification metadata"); +} + TEST(make_notification_rejects_subscription_identifier_on_2025_11_25) { EXPECT_MCP_REJECTS(sourcemeta::core::mcp_make_notification( sourcemeta::core::MCPProtocolVersion::V_2025_11_25, diff --git a/test/mcp/mcp_results_test.cc b/test/mcp/mcp_results_test.cc index 17cc96287..85434fdc0 100644 --- a/test/mcp/mcp_results_test.cc +++ b/test/mcp/mcp_results_test.cc @@ -6725,6 +6725,24 @@ TEST(result_writer_metadata_rejections) { "Discovery must include its serving revision"); } +TEST(result_writer_rejects_empty_metadata_key) { + std::ostringstream stream; + const auto bad{sourcemeta::core::parse_json( + R"JSON({ + "resources": [], + "_meta": { + "": 1 + } + })JSON")}; + EXPECT_MCP_REJECTS(sourcemeta::core::mcp_write_result( + stream, + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, + "resources/list", sourcemeta::core::JSON{0}, bad, + sourcemeta::core::MCPCachePolicy{}), + "Invalid precomputed metadata"); + EXPECT_TRUE(stream.str().empty()); +} + // An elicitation schema may describe a single or a multiple choice, and the // builder carries the schema through untouched // https://modelcontextprotocol.io/specification/2026-07-28/schema#titledsingleselectenumschema