diff --git a/test/mcp/mcp_headers_test.cc b/test/mcp/mcp_headers_test.cc index 5f9764cf2..6c4e28d5f 100644 --- a/test/mcp/mcp_headers_test.cc +++ b/test/mcp/mcp_headers_test.cc @@ -4669,3 +4669,198 @@ TEST(parameter_headers_index_rejects_duplicate_descriptor_names) { sourcemeta::core::JSON{0}, parameters, arguments, headers), "Parameter header names must be case-insensitively unique"); } + +TEST(request_parameters_metadata_version_2025_03_26) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2025-03-26", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "state" + })JSON")}; + EXPECT_EQ(sourcemeta::core::mcp_validate_request_parameters(input).first, + sourcemeta::core::MCPRequestMetaStatus::UnsupportedProtocolVersion); +} + +TEST(request_parameters_metadata_version_2025_06_18) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2025-06-18", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "state" + })JSON")}; + EXPECT_EQ(sourcemeta::core::mcp_validate_request_parameters(input).first, + sourcemeta::core::MCPRequestMetaStatus::UnsupportedProtocolVersion); +} + +TEST(request_parameters_metadata_version_2025_11_25) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2025-11-25", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "state" + })JSON")}; + EXPECT_EQ(sourcemeta::core::mcp_validate_request_parameters(input).first, + sourcemeta::core::MCPRequestMetaStatus::UnsupportedProtocolVersion); +} + +TEST(request_parameters_metadata_version_2026_07_28) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "state" + })JSON")}; + EXPECT_EQ(sourcemeta::core::mcp_validate_request_parameters(input).first, + sourcemeta::core::MCPRequestMetaStatus::Valid); +} + +TEST(parameter_headers_reject_version_2025_03_26) { + const auto arguments{sourcemeta::core::JSON::make_object()}; + EXPECT_MCP_REJECTS(sourcemeta::core::mcp_validate_parameter_headers( + sourcemeta::core::MCPProtocolVersion::V_2025_03_26, + sourcemeta::core::JSON{1}, {}, arguments, {}), + "Parameter headers require MCP 2026-07-28"); +} + +TEST(parameter_headers_reject_version_2025_06_18) { + const auto arguments{sourcemeta::core::JSON::make_object()}; + EXPECT_MCP_REJECTS(sourcemeta::core::mcp_validate_parameter_headers( + sourcemeta::core::MCPProtocolVersion::V_2025_06_18, + sourcemeta::core::JSON{1}, {}, arguments, {}), + "Parameter headers require MCP 2026-07-28"); +} + +TEST(parameter_headers_reject_version_2025_11_25) { + const auto arguments{sourcemeta::core::JSON::make_object()}; + EXPECT_MCP_REJECTS(sourcemeta::core::mcp_validate_parameter_headers( + sourcemeta::core::MCPProtocolVersion::V_2025_11_25, + sourcemeta::core::JSON{1}, {}, arguments, {}), + "Parameter headers require MCP 2026-07-28"); +} + +TEST(parameter_header_round_trip_printable_ascii) { + const std::array descriptors{ + {{.name = "value", + .path = {"value"}, + .type = sourcemeta::core::JSON::Type::String}}}; + const auto arguments{sourcemeta::core::parse_json(R"JSON({ + "value": " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" + })JSON")}; + const auto owned{ + sourcemeta::core::mcp_make_parameter_headers(descriptors, arguments)}; + const std::array, + 1> + views{{{"mCp-pArAm-VaLuE", owned.front().second}}}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_parameter_headers( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, + sourcemeta::core::JSON{1}, descriptors, arguments, views) + .has_value()); +} + +TEST(parameter_header_round_trip_accent) { + const std::array descriptors{ + {{.name = "value", + .path = {"value"}, + .type = sourcemeta::core::JSON::Type::String}}}; + const auto arguments{sourcemeta::core::parse_json(R"JSON({ + "value": "café" + })JSON")}; + const auto owned{ + sourcemeta::core::mcp_make_parameter_headers(descriptors, arguments)}; + const std::array, + 1> + views{{{"mCp-pArAm-VaLuE", owned.front().second}}}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_parameter_headers( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, + sourcemeta::core::JSON{1}, descriptors, arguments, views) + .has_value()); +} + +TEST(parameter_header_round_trip_japanese) { + const std::array descriptors{ + {{.name = "value", + .path = {"value"}, + .type = sourcemeta::core::JSON::Type::String}}}; + const auto arguments{sourcemeta::core::parse_json(R"JSON({ + "value": "日本語" + })JSON")}; + const auto owned{ + sourcemeta::core::mcp_make_parameter_headers(descriptors, arguments)}; + const std::array, + 1> + views{{{"mCp-pArAm-VaLuE", owned.front().second}}}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_parameter_headers( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, + sourcemeta::core::JSON{1}, descriptors, arguments, views) + .has_value()); +} + +TEST(parameter_header_round_trip_emoji) { + const std::array descriptors{ + {{.name = "value", + .path = {"value"}, + .type = sourcemeta::core::JSON::Type::String}}}; + const auto arguments{sourcemeta::core::parse_json(R"JSON({ + "value": "🙂" + })JSON")}; + const auto owned{ + sourcemeta::core::mcp_make_parameter_headers(descriptors, arguments)}; + const std::array, + 1> + views{{{"mCp-pArAm-VaLuE", owned.front().second}}}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_parameter_headers( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, + sourcemeta::core::JSON{1}, descriptors, arguments, views) + .has_value()); +} + +TEST(parameter_header_round_trip_padded_unicode) { + const std::array descriptors{ + {{.name = "value", + .path = {"value"}, + .type = sourcemeta::core::JSON::Type::String}}}; + const auto arguments{sourcemeta::core::parse_json(R"JSON({ + "value": " café " + })JSON")}; + const auto owned{ + sourcemeta::core::mcp_make_parameter_headers(descriptors, arguments)}; + const std::array, + 1> + views{{{"mCp-pArAm-VaLuE", owned.front().second}}}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_parameter_headers( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, + sourcemeta::core::JSON{1}, descriptors, arguments, views) + .has_value()); +} + +TEST(parameter_header_round_trip_rejects_changed_body) { + const std::array descriptors{ + {{.name = "value", + .path = {"value"}, + .type = sourcemeta::core::JSON::Type::String}}}; + const auto original{sourcemeta::core::parse_json(R"JSON({ + "value": "original" + })JSON")}; + const auto owned{ + sourcemeta::core::mcp_make_parameter_headers(descriptors, original)}; + const std::array, + 1> + views{{{owned.front().first, owned.front().second}}}; + const auto changed{sourcemeta::core::parse_json(R"JSON({ + "value": "changed" + })JSON")}; + EXPECT_TRUE(sourcemeta::core::mcp_validate_parameter_headers( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, + sourcemeta::core::JSON{1}, descriptors, changed, views) + .has_value()); +} diff --git a/test/mcp/mcp_notifications_test.cc b/test/mcp/mcp_notifications_test.cc index 8545a668a..3ca9f2434 100644 --- a/test/mcp/mcp_notifications_test.cc +++ b/test/mcp/mcp_notifications_test.cc @@ -1605,3 +1605,612 @@ TEST(validate_continuation_accepts_a_received_resource_link) { sourcemeta::core::MCPProtocolVersion::V_2026_07_28, parameters, prior, std::nullopt)); } + +TEST(validate_continuation_version_2025_03_26) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "state" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2025_03_26, input, prior, + "state")); +} + +TEST(validate_continuation_version_2025_06_18) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "state" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2025_06_18, input, prior, + "state")); +} + +TEST(validate_continuation_version_2025_11_25) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "state" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2025_11_25, input, prior, + "state")); +} + +TEST(validate_continuation_version_2026_07_28) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "state" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_TRUE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "state")); +} + +TEST(validate_continuation_echo_empty_with_empty) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_TRUE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, "")); +} + +TEST(validate_continuation_echo_empty_with_plain) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "state" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, "")); +} + +TEST(validate_continuation_echo_empty_with_uppercase) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "STATE" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, "")); +} + +TEST(validate_continuation_echo_empty_with_padded) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": " state " + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, "")); +} + +TEST(validate_continuation_echo_empty_with_unicode) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "état" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, "")); +} + +TEST(validate_continuation_echo_plain_with_empty) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "state")); +} + +TEST(validate_continuation_echo_plain_with_plain) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "state" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_TRUE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "state")); +} + +TEST(validate_continuation_echo_plain_with_uppercase) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "STATE" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "state")); +} + +TEST(validate_continuation_echo_plain_with_padded) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": " state " + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "state")); +} + +TEST(validate_continuation_echo_plain_with_unicode) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "état" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "state")); +} + +TEST(validate_continuation_echo_uppercase_with_empty) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "STATE")); +} + +TEST(validate_continuation_echo_uppercase_with_plain) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "state" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "STATE")); +} + +TEST(validate_continuation_echo_uppercase_with_uppercase) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "STATE" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_TRUE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "STATE")); +} + +TEST(validate_continuation_echo_uppercase_with_padded) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": " state " + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "STATE")); +} + +TEST(validate_continuation_echo_uppercase_with_unicode) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "état" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "STATE")); +} + +TEST(validate_continuation_echo_padded_with_empty) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + " state ")); +} + +TEST(validate_continuation_echo_padded_with_plain) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "state" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + " state ")); +} + +TEST(validate_continuation_echo_padded_with_uppercase) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "STATE" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + " state ")); +} + +TEST(validate_continuation_echo_padded_with_padded) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": " state " + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_TRUE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + " state ")); +} + +TEST(validate_continuation_echo_padded_with_unicode) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "état" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + " state ")); +} + +TEST(validate_continuation_echo_unicode_with_empty) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "état")); +} + +TEST(validate_continuation_echo_unicode_with_plain) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "state" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "état")); +} + +TEST(validate_continuation_echo_unicode_with_uppercase) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "STATE" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "état")); +} + +TEST(validate_continuation_echo_unicode_with_padded) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": " state " + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "état")); +} + +TEST(validate_continuation_echo_unicode_with_unicode) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "état" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_TRUE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "état")); +} + +TEST(validate_continuation_rejects_unsolicited_empty_state) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + std::nullopt)); +} + +TEST(validate_continuation_rejects_unsolicited_plain_state) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "state" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + std::nullopt)); +} + +TEST(validate_continuation_rejects_unsolicited_uppercase_state) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "STATE" + })JSON")}; + const auto prior{sourcemeta::core::JSON::make_object()}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + std::nullopt)); +} + +TEST(validate_continuation_root_https) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "state", + "inputResponses": { + "r": { + "roots": [ + { + "uri": "https://example.com" + } + ] + } + } + })JSON")}; + const auto prior{sourcemeta::core::parse_json(R"JSON({ + "r": { + "method": "roots/list" + } + })JSON")}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "state")); +} + +TEST(validate_continuation_root_relative) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "state", + "inputResponses": { + "r": { + "roots": [ + { + "uri": "relative/path" + } + ] + } + } + })JSON")}; + const auto prior{sourcemeta::core::parse_json(R"JSON({ + "r": { + "method": "roots/list" + } + })JSON")}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "state")); +} + +TEST(validate_continuation_root_uppercase_scheme) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "state", + "inputResponses": { + "r": { + "roots": [ + { + "uri": "FILE:///tmp" + } + ] + } + } + })JSON")}; + const auto prior{sourcemeta::core::parse_json(R"JSON({ + "r": { + "method": "roots/list" + } + })JSON")}; + EXPECT_FALSE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "state")); +} + +TEST(validate_continuation_root_file_path) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "state", + "inputResponses": { + "r": { + "roots": [ + { + "uri": "file:///tmp" + } + ] + } + } + })JSON")}; + const auto prior{sourcemeta::core::parse_json(R"JSON({ + "r": { + "method": "roots/list" + } + })JSON")}; + EXPECT_TRUE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "state")); +} + +// Characterizes current receive policy, not URI validity. Stricter parsing +// requires maintainer agreement; the receiver currently checks file:// only. +TEST(validate_continuation_root_space_in_authority) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "state", + "inputResponses": { + "r": { + "roots": [ + { + "uri": "file://bad space" + } + ] + } + } + })JSON")}; + const auto prior{sourcemeta::core::parse_json(R"JSON({ + "r": { + "method": "roots/list" + } + })JSON")}; + EXPECT_TRUE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "state")); +} + +// Characterizes current receive policy, not URI validity. Stricter parsing +// requires maintainer agreement; the receiver currently checks file:// only. +TEST(validate_continuation_root_empty_file_authority) { + const auto input{sourcemeta::core::parse_json(R"JSON({ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + }, + "requestState": "state", + "inputResponses": { + "r": { + "roots": [ + { + "uri": "file://" + } + ] + } + } + })JSON")}; + const auto prior{sourcemeta::core::parse_json(R"JSON({ + "r": { + "method": "roots/list" + } + })JSON")}; + EXPECT_TRUE(sourcemeta::core::mcp_validate_continuation( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, input, prior, + "state")); +}