// ============================================================================ // MCP Protocol Version Feature Matrix Tests // // This file tests version-specific features to ensure YAMS correctly implements // all claimed protocol versions. // // Version History: // - 2024-10-07: Initial release // - 2024-11-05: Added tool annotations, structuredContent in CallToolResult // - 2025-03-26: Added completions capability, BaseMetadata with title field // - 2025-06-18: Added elicitation, roots listChanged, ModelPreferences with hints // - 2025-11-25: Added tasks, sampling context/tools, icons in Implementation // ============================================================================ #include #include #include #include #include #include "../../common/test_helpers_catch2.h" using namespace yams::mcp; using json = nlohmann::json; namespace { class NullTransport : public yams::mcp::ITransport { public: void send(const json&) override {} yams::mcp::MessageResult receive() override { return yams::Error{yams::ErrorCode::NotImplemented, "Null transport"}; } bool isConnected() const override { return false; } void close() override {} yams::mcp::TransportState getState() const override { return yams::mcp::TransportState::Disconnected; } }; using yams::test::ScopedEnvVar; } // namespace // ============================================================================ // Version 2025-11-25 Specific Features // ============================================================================ TEST_CASE("MCP 2025-11-25 - supports tasks capability", "[mcp][protocol][2025-11-25][features][tasks][catch2]") { auto transport = std::make_unique(); auto server = std::make_shared(std::move(transport)); json initRequest = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", {{"protocolVersion", "2025-11-25"}, {"clientInfo", {{"name", "test"}, {"version", "1.0"}}}, {"capabilities", json::object()}}}}; auto response = server->handleRequestPublic(initRequest); REQUIRE(response.has_value()); json result = response.value(); REQUIRE(result["result"].contains("capabilities")); // Check if tasks capability is present (optional for servers) const auto& caps = result["result"]["capabilities"]; if (caps.contains("tasks")) { CHECK(caps["tasks"].is_object()); } } TEST_CASE("MCP 2025-11-25 - supports sampling context and tools", "[mcp][protocol][2025-11-25][features][sampling][catch2]") { auto transport = std::make_unique(); auto server = std::make_shared(std::move(transport)); json initRequest = { {"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", {{"protocolVersion", "2025-11-25"}, {"clientInfo", {{"name", "test"}, {"version", "1.0"}}}, {"capabilities", {{"sampling", {{"context", json::object()}, {"tools", json::object()}}}}}}}}; auto response = server->handleRequestPublic(initRequest); REQUIRE(response.has_value()); json result = response.value(); CHECK(result["result"]["protocolVersion"] == "2025-11-25"); } TEST_CASE("MCP 2025-11-25 - serverInfo includes name and version", "[mcp][protocol][2025-11-25][features][serverinfo][catch2]") { auto transport = std::make_unique(); auto server = std::make_shared(std::move(transport)); json initRequest = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", {{"protocolVersion", "2025-11-25"}, {"clientInfo", {{"name", "test"}, {"version", "1.0"}}}, {"capabilities", json::object()}}}}; auto response = server->handleRequestPublic(initRequest); REQUIRE(response.has_value()); json result = response.value(); REQUIRE(result["result"].contains("serverInfo")); CHECK(result["result"]["serverInfo"].contains("name")); CHECK(result["result"]["serverInfo"].contains("version")); CHECK(result["result"]["serverInfo"]["name"].is_string()); CHECK(result["result"]["serverInfo"]["version"].is_string()); } // ============================================================================ // Version 2025-06-18 Specific Features // ============================================================================ TEST_CASE("MCP 2025-06-18 - supports completions capability", "[mcp][protocol][2025-06-18][features][completions][catch2]") { auto transport = std::make_unique(); auto server = std::make_shared(std::move(transport)); json initRequest = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", {{"protocolVersion", "2025-06-18"}, {"clientInfo", {{"name", "test"}, {"version", "1.0"}}}, {"capabilities", json::object()}}}}; auto response = server->handleRequestPublic(initRequest); REQUIRE(response.has_value()); json result = response.value(); REQUIRE(result["result"].contains("capabilities")); // completions capability is optional const auto& caps = result["result"]["capabilities"]; if (caps.contains("completions")) { CHECK(caps["completions"].is_object()); } } TEST_CASE("MCP 2025-06-18 - supports roots listChanged", "[mcp][protocol][2025-06-18][features][roots][catch2]") { auto transport = std::make_unique(); auto server = std::make_shared(std::move(transport)); json initRequest = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", {{"protocolVersion", "2025-06-18"}, {"clientInfo", {{"name", "test"}, {"version", "1.0"}}}, {"capabilities", {{"roots", {{"listChanged", true}}}}}}}}; auto response = server->handleRequestPublic(initRequest); REQUIRE(response.has_value()); json result = response.value(); CHECK(result["result"]["protocolVersion"] == "2025-06-18"); } // ============================================================================ // Version 2025-03-26 Specific Features // ============================================================================ TEST_CASE("MCP 2025-03-26 - supports BaseMetadata with title", "[mcp][protocol][2025-03-26][features][metadata][catch2]") { auto transport = std::make_unique(); auto server = std::make_shared(std::move(transport)); json initRequest = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", {{"protocolVersion", "2025-03-26"}, {"clientInfo", {{"name", "test"}, {"version", "1.0"}}}, {"capabilities", json::object()}}}}; auto response = server->handleRequestPublic(initRequest); REQUIRE(response.has_value()); json result = response.value(); CHECK(result["result"]["protocolVersion"] == "2025-03-26"); // Get tools to check if they support title field json toolsRequest = { {"jsonrpc", "2.0"}, {"id", 2}, {"method", "tools/list"}, {"params", json::object()}}; auto toolsResponse = server->handleRequestPublic(toolsRequest); REQUIRE(toolsResponse.has_value()); json toolsResult = toolsResponse.value(); REQUIRE(toolsResult["result"].contains("tools")); // Check if any tool has a title field (optional feature) for (const auto& tool : toolsResult["result"]["tools"]) { if (tool.contains("title")) { CHECK(tool["title"].is_string()); } } } // ============================================================================ // Version 2024-11-05 Specific Features // ============================================================================ TEST_CASE("MCP 2024-11-05 - supports tool annotations", "[mcp][protocol][2024-11-05][features][tool-annotations][catch2]") { auto transport = std::make_unique(); auto server = std::make_shared(std::move(transport)); json initRequest = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", {{"protocolVersion", "2024-11-05"}, {"clientInfo", {{"name", "test"}, {"version", "1.0"}}}, {"capabilities", json::object()}}}}; auto response = server->handleRequestPublic(initRequest); REQUIRE(response.has_value()); json result = response.value(); CHECK(result["result"]["protocolVersion"] == "2024-11-05"); // Get tools to check for annotations json toolsRequest = { {"jsonrpc", "2.0"}, {"id", 2}, {"method", "tools/list"}, {"params", json::object()}}; auto toolsResponse = server->handleRequestPublic(toolsRequest); REQUIRE(toolsResponse.has_value()); json toolsResult = toolsResponse.value(); REQUIRE(toolsResult["result"].contains("tools")); // Check if any tool has annotations (optional feature) for (const auto& tool : toolsResult["result"]["tools"]) { if (tool.contains("annotations")) { const auto& ann = tool["annotations"]; // Verify annotation fields if present if (ann.contains("readOnlyHint")) { CHECK(ann["readOnlyHint"].is_boolean()); } if (ann.contains("destructiveHint")) { CHECK(ann["destructiveHint"].is_boolean()); } if (ann.contains("idempotentHint")) { CHECK(ann["idempotentHint"].is_boolean()); } if (ann.contains("openWorldHint")) { CHECK(ann["openWorldHint"].is_boolean()); } } } } TEST_CASE("MCP 2024-11-05 - supports structuredContent in tool results", "[mcp][protocol][2024-11-05][features][structured-content][catch2]") { auto transport = std::make_unique(); auto server = std::make_shared(std::move(transport)); // Initialize with 2024-11-05 json initRequest = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", {{"protocolVersion", "2024-11-05"}, {"clientInfo", {{"name", "test"}, {"version", "1.0"}}}, {"capabilities", json::object()}}}}; auto initResponse = server->handleRequestPublic(initRequest); REQUIRE(initResponse.has_value()); // Call a tool and check if tool results can include structuredContent. // Use an in-process tool that doesn't require a live daemon connection. json toolRequest = {{"jsonrpc", "2.0"}, {"id", 99}, {"method", "tools/call"}, {"params", {{"name", "mcp.echo"}, {"arguments", {{"text", "hello"}}}}}}; auto toolResponse = server->handleRequestPublic(toolRequest); REQUIRE(toolResponse.has_value()); json toolResult = toolResponse.value(); INFO("tools/call response: " << toolResult.dump(2)); REQUIRE(toolResult.contains("result")); REQUIRE(toolResult["result"].contains("content")); CHECK(toolResult["result"]["content"].is_array()); CHECK_FALSE(toolResult["result"].value("isError", false)); // New in 2024-11-05: structuredContent field. REQUIRE(toolResult["result"].contains("structuredContent")); REQUIRE(toolResult["result"]["structuredContent"].is_object()); CHECK(toolResult["result"]["structuredContent"].value("type", "") == "tool_result"); CHECK(toolResult["result"]["structuredContent"].contains("data")); } // ============================================================================ // Version 2024-10-07 Specific Features // ============================================================================ TEST_CASE("MCP 2024-10-07 - tools/list omits post-version metadata fields", "[mcp][protocol][2024-10-07][features][tools-list-shaping][catch2]") { auto transport = std::make_unique(); auto server = std::make_shared(std::move(transport)); json initRequest = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", {{"protocolVersion", "2024-10-07"}, {"clientInfo", {{"name", "test"}, {"version", "1.0"}}}, {"capabilities", json::object()}}}}; auto initResponse = server->handleRequestPublic(initRequest); REQUIRE(initResponse.has_value()); json toolsRequest = { {"jsonrpc", "2.0"}, {"id", 2}, {"method", "tools/list"}, {"params", json::object()}}; auto toolsResponse = server->handleRequestPublic(toolsRequest); REQUIRE(toolsResponse.has_value()); const auto& tools = toolsResponse.value()["result"]["tools"]; REQUIRE(tools.is_array()); REQUIRE_FALSE(tools.empty()); for (const auto& tool : tools) { CHECK_FALSE(tool.contains("annotations")); CHECK_FALSE(tool.contains("title")); } } TEST_CASE("MCP echo tool remains dotted and callable", "[mcp][protocol][compat][tool-name][catch2]") { auto transport = std::make_unique(); auto server = std::make_shared(std::move(transport)); json initRequest = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", {{"protocolVersion", "2025-11-25"}, {"clientInfo", {{"name", "test"}, {"version", "1.0"}}}, {"capabilities", json::object()}}}}; auto initResponse = server->handleRequestPublic(initRequest); REQUIRE(initResponse.has_value()); json toolsRequest = { {"jsonrpc", "2.0"}, {"id", 2}, {"method", "tools/list"}, {"params", json::object()}}; auto toolsResponse = server->handleRequestPublic(toolsRequest); REQUIRE(toolsResponse.has_value()); const auto& tools = toolsResponse.value()["result"]["tools"]; REQUIRE(tools.is_array()); bool foundDotted = false; for (const auto& tool : tools) { const std::string name = tool.value("name", ""); if (name == "mcp.echo") { foundDotted = true; } } CHECK(foundDotted); json echoCall = {{"jsonrpc", "2.0"}, {"id", 3}, {"method", "tools/call"}, {"params", {{"name", "mcp.echo"}, {"arguments", {{"text", "hello"}}}}}}; auto echoResponse = server->handleRequestPublic(echoCall); REQUIRE(echoResponse.has_value()); REQUIRE(echoResponse.value().contains("result")); REQUIRE(echoResponse.value()["result"].contains("content")); } // ============================================================================ // Version 2024-10-07 (Initial Release) Features // ============================================================================ TEST_CASE("MCP 2024-10-07 - basic protocol features work", "[mcp][protocol][2024-10-07][features][basic][catch2]") { auto transport = std::make_unique(); auto server = std::make_shared(std::move(transport)); json initRequest = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", {{"protocolVersion", "2024-10-07"}, {"clientInfo", {{"name", "test"}, {"version", "1.0"}}}, {"capabilities", json::object()}}}}; auto response = server->handleRequestPublic(initRequest); REQUIRE(response.has_value()); json result = response.value(); CHECK(result["result"]["protocolVersion"] == "2024-10-07"); REQUIRE(result["result"].contains("capabilities")); REQUIRE(result["result"].contains("serverInfo")); } TEST_CASE("MCP 2024-10-07 - supports tools/list", "[mcp][protocol][2024-10-07][features][tools][catch2]") { auto transport = std::make_unique(); auto server = std::make_shared(std::move(transport)); // Initialize json initRequest = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", {{"protocolVersion", "2024-10-07"}, {"clientInfo", {{"name", "test"}, {"version", "1.0"}}}, {"capabilities", json::object()}}}}; auto initResponse = server->handleRequestPublic(initRequest); REQUIRE(initResponse.has_value()); // List tools json toolsRequest = { {"jsonrpc", "2.0"}, {"id", 2}, {"method", "tools/list"}, {"params", json::object()}}; auto toolsResponse = server->handleRequestPublic(toolsRequest); REQUIRE(toolsResponse.has_value()); json toolsResult = toolsResponse.value(); REQUIRE(toolsResult["result"].contains("tools")); CHECK(toolsResult["result"]["tools"].is_array()); CHECK(toolsResult["result"]["tools"].size() > 0); } TEST_CASE("MCP 2024-10-07 - tool results omit structuredContent", "[mcp][protocol][2024-10-07][features][structured-content][catch2]") { auto transport = std::make_unique(); auto server = std::make_shared(std::move(transport)); // Initialize with pre-structuredContent protocol version json initRequest = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", {{"protocolVersion", "2024-10-07"}, {"clientInfo", {{"name", "test"}, {"version", "1.0"}}}, {"capabilities", json::object()}}}}; auto initResponse = server->handleRequestPublic(initRequest); REQUIRE(initResponse.has_value()); CHECK(initResponse.value()["result"]["protocolVersion"] == "2024-10-07"); // Call a tool that returns a tool-result shaped payload. // Use an in-process tool that doesn't require a live daemon connection. json toolRequest = {{"jsonrpc", "2.0"}, {"id", 99}, {"method", "tools/call"}, {"params", {{"name", "mcp.echo"}, {"arguments", {{"text", "hello"}}}}}}; auto toolResponse = server->handleRequestPublic(toolRequest); REQUIRE(toolResponse.has_value()); json toolResult = toolResponse.value(); INFO("tools/call response: " << toolResult.dump(2)); REQUIRE(toolResult.contains("result")); REQUIRE(toolResult["result"].contains("content")); CHECK(toolResult["result"]["content"].is_array()); CHECK_FALSE(toolResult["result"].value("isError", false)); CHECK_FALSE(toolResult["result"].contains("structuredContent")); } TEST_CASE("MCP 2024-10-07 - supports resources/list", "[mcp][protocol][2024-10-07][features][resources][catch2]") { auto transport = std::make_unique(); auto server = std::make_shared(std::move(transport)); // Initialize json initRequest = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", {{"protocolVersion", "2024-10-07"}, {"clientInfo", {{"name", "test"}, {"version", "1.0"}}}, {"capabilities", json::object()}}}}; auto initResponse = server->handleRequestPublic(initRequest); REQUIRE(initResponse.has_value()); // List resources json resourcesRequest = { {"jsonrpc", "2.0"}, {"id", 2}, {"method", "resources/list"}, {"params", json::object()}}; auto resourcesResponse = server->handleRequestPublic(resourcesRequest); REQUIRE(resourcesResponse.has_value()); json resourcesResult = resourcesResponse.value(); REQUIRE(resourcesResult["result"].contains("resources")); CHECK(resourcesResult["result"]["resources"].is_array()); } TEST_CASE("MCP 2024-10-07 - supports prompts/list", "[mcp][protocol][2024-10-07][features][prompts][catch2]") { auto transport = std::make_unique(); auto server = std::make_shared(std::move(transport)); // Initialize json initRequest = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", {{"protocolVersion", "2024-10-07"}, {"clientInfo", {{"name", "test"}, {"version", "1.0"}}}, {"capabilities", json::object()}}}}; auto initResponse = server->handleRequestPublic(initRequest); REQUIRE(initResponse.has_value()); // List prompts json promptsRequest = { {"jsonrpc", "2.0"}, {"id", 2}, {"method", "prompts/list"}, {"params", json::object()}}; auto promptsResponse = server->handleRequestPublic(promptsRequest); REQUIRE(promptsResponse.has_value()); json promptsResult = promptsResponse.value(); REQUIRE(promptsResult["result"].contains("prompts")); CHECK(promptsResult["result"]["prompts"].is_array()); } // ============================================================================ // Cross-Version Compatibility Tests // ============================================================================ TEST_CASE("MCP - all versions support ping", "[mcp][protocol][all-versions][features][ping][catch2]") { std::vector versions = {"2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05", "2024-10-07"}; for (const auto& version : versions) { auto transport = std::make_unique(); auto server = std::make_shared(std::move(transport)); // Initialize json initRequest = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", {{"protocolVersion", version}, {"clientInfo", {{"name", "test"}, {"version", "1.0"}}}, {"capabilities", json::object()}}}}; auto initResponse = server->handleRequestPublic(initRequest); REQUIRE(initResponse.has_value()); // Ping json pingRequest = { {"jsonrpc", "2.0"}, {"id", 2}, {"method", "ping"}, {"params", json::object()}}; auto pingResponse = server->handleRequestPublic(pingRequest); REQUIRE(pingResponse.has_value()); json pingResult = pingResponse.value(); CHECK(pingResult["jsonrpc"] == "2.0"); CHECK(pingResult["id"] == 2); REQUIRE(pingResult.contains("result")); } } TEST_CASE("MCP - all versions support cancellation", "[mcp][protocol][all-versions][features][cancellation][catch2]") { std::vector versions = {"2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05", "2024-10-07"}; for (const auto& version : versions) { auto transport = std::make_unique(); auto server = std::make_shared(std::move(transport)); // Initialize json initRequest = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", {{"protocolVersion", version}, {"clientInfo", {{"name", "test"}, {"version", "1.0"}}}, {"capabilities", json::object()}}}}; auto initResponse = server->handleRequestPublic(initRequest); REQUIRE(initResponse.has_value()); // Cancel (notification, no response expected) json cancelNotification = {{"jsonrpc", "2.0"}, {"method", "notifications/cancelled"}, {"params", {{"requestId", 999}}}}; auto cancelResponse = server->handleRequestPublic(cancelNotification); // Notifications may return an error indicating no response should be sent // This is acceptable behavior } } TEST_CASE("MCP - all versions support progress notifications", "[mcp][protocol][all-versions][features][progress][catch2]") { std::vector versions = {"2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05", "2024-10-07"}; for (const auto& version : versions) { auto transport = std::make_unique(); auto server = std::make_shared(std::move(transport)); // Initialize json initRequest = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", {{"protocolVersion", version}, {"clientInfo", {{"name", "test"}, {"version", "1.0"}}}, {"capabilities", json::object()}}}}; auto initResponse = server->handleRequestPublic(initRequest); REQUIRE(initResponse.has_value()); // Check capabilities include progress support json result = initResponse.value(); // Progress is supported via _meta.progressToken in requests // This is a protocol-level feature available in all versions CHECK(result["result"]["protocolVersion"] == version); } } TEST_CASE("MCP - all versions support logging", "[mcp][protocol][all-versions][features][logging][catch2]") { std::vector versions = {"2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05", "2024-10-07"}; for (const auto& version : versions) { auto transport = std::make_unique(); auto server = std::make_shared(std::move(transport)); json initRequest = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", {{"protocolVersion", version}, {"clientInfo", {{"name", "test"}, {"version", "1.0"}}}, {"capabilities", json::object()}}}}; auto response = server->handleRequestPublic(initRequest); REQUIRE(response.has_value()); json result = response.value(); REQUIRE(result["result"].contains("capabilities")); // Logging capability should be present in all versions const auto& caps = result["result"]["capabilities"]; CHECK(caps.contains("logging")); } }