yams/tests/unit/mcp/mcp_protocol_version_features_test.cpp
2026-04-14 10:25:54 -04:00

663 lines
27 KiB
C++

// ============================================================================
// 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 <nlohmann/json.hpp>
#include <cstdlib>
#include <string>
#include <catch2/catch_test_macros.hpp>
#include <yams/mcp/mcp_server.h>
#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<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(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<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(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<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(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<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(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<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(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<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(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<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(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<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(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<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(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<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(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<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(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<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(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<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(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<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(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<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(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<std::string> 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<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(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<std::string> 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<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(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<std::string> 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<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(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<std::string> 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<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(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"));
}
}