yams/tests/unit/mcp/mcp_server_catch2_test.cpp
2026-08-04 21:31:39 -04:00

776 lines
29 KiB
C++

// MCP Server basics tests
// Catch2 migration from GTest (yams-3s4 / yams-84g)
#include <catch2/catch_test_macros.hpp>
#include <chrono>
#include <future>
#include <iostream>
#include <memory>
#include <sstream>
#include <string>
#include <unordered_set>
#include <vector>
#include <nlohmann/json.hpp>
#include <yams/mcp/mcp_server.h>
#include "../../common/test_helpers_catch2.h"
using namespace yams::mcp;
using json = nlohmann::json;
namespace {
// Minimal no-op transport for constructing MCPServer in tests without starting loops.
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;
}
};
} // namespace
TEST_CASE("MCP Transport - StdioTransport basics", "[mcp][transport][catch2]") {
StdioTransport transport;
// Test that transport starts as connected
CHECK(transport.isConnected());
// Test close functionality
transport.close();
CHECK_FALSE(transport.isConnected());
}
TEST_CASE("MCP Types - Basic type availability", "[mcp][types][catch2]") {
// Test basic types are available - compilation test
CHECK(true);
}
// ============================================================================
// Tool Registry Initialization Tests
// ============================================================================
TEST_CASE("MCP Server - Tool registry is initialized and listTools returns tools",
"[mcp][server][tools][registry][catch2]") {
auto transport = std::make_unique<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
// Call testListTools which directly invokes listTools()
json result = server->testListTools();
// Verify the result is a valid JSON object with tools array
REQUIRE(result.is_object());
REQUIRE(result.contains("tools"));
REQUIRE(result["tools"].is_array());
// Verify tools array is not empty (registry was initialized)
CHECK(result["tools"].size() > 0);
// Log the number of tools found for debugging
INFO("Number of tools registered: " << result["tools"].size());
}
TEST_CASE("MCP Server - listTools returns valid tool structure",
"[mcp][server][tools][structure][catch2]") {
auto transport = std::make_unique<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
json result = server->testListTools();
REQUIRE(result.is_object());
REQUIRE(result.contains("tools"));
REQUIRE(result["tools"].is_array());
// Verify each tool has required fields per MCP spec
for (const auto& tool : result["tools"]) {
REQUIRE(tool.is_object());
CHECK(tool.contains("name"));
CHECK(tool.contains("description"));
CHECK(tool.contains("inputSchema"));
// Verify inputSchema is a valid JSON schema object
if (tool.contains("inputSchema")) {
const auto& schema = tool["inputSchema"];
CHECK(schema.is_object());
if (schema.contains("type")) {
CHECK(schema["type"].get<std::string>() == "object");
}
}
}
}
TEST_CASE("MCP Server - Core tools are registered", "[mcp][server][tools][core][catch2]") {
auto transport = std::make_unique<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
json result = server->testListTools();
REQUIRE(result.is_object());
REQUIRE(result.contains("tools"));
// Collect all tool names
std::unordered_set<std::string> toolNames;
for (const auto& tool : result["tools"]) {
if (tool.is_object() && tool.contains("name")) {
toolNames.insert(tool["name"].get<std::string>());
}
}
// Verify composite tools are present (code mode: query, execute, mcp.echo)
CHECK(toolNames.count("query") > 0);
CHECK(toolNames.count("execute") > 0);
CHECK(toolNames.count("mcp.echo") > 0);
// Individual tools should NOT be directly exposed
CHECK(toolNames.count("search") == 0);
CHECK(toolNames.count("grep") == 0);
CHECK(toolNames.count("add") == 0);
}
// ============================================================================
// OpenCode Compatibility Tests
// Based on MCP protocol spec and OpenCode SDK expectations
// ============================================================================
TEST_CASE("MCP Server - tools/list response matches MCP spec format",
"[mcp][server][tools][opencodcompat][catch2]") {
auto transport = std::make_unique<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
// Simulate what OpenCode sends: {"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}
json request = {
{"jsonrpc", "2.0"}, {"id", 1}, {"method", "tools/list"}, {"params", json::object()}};
// Use handleRequestPublic to process the request
auto response = server->handleRequestPublic(request);
REQUIRE(response.has_value());
json result = response.value();
// Verify JSON-RPC structure
REQUIRE(result.is_object());
CHECK(result["jsonrpc"] == "2.0");
CHECK(result["id"] == 1);
REQUIRE(result.contains("result"));
// Verify result contains tools array
const auto& resultObj = result["result"];
REQUIRE(resultObj.is_object());
REQUIRE(resultObj.contains("tools"));
REQUIRE(resultObj["tools"].is_array());
CHECK(resultObj["tools"].size() > 0);
}
TEST_CASE("MCP Server - tools/list handles pagination cursor",
"[mcp][server][tools][pagination][catch2]") {
auto transport = std::make_unique<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
// Test with cursor parameter (as per MCP spec)
json request = {{"jsonrpc", "2.0"},
{"id", 2},
{"method", "tools/list"},
{"params", {{"cursor", "some-cursor-value"}}}};
auto response = server->handleRequestPublic(request);
REQUIRE(response.has_value());
json result = response.value();
REQUIRE(result.is_object());
CHECK(result["jsonrpc"] == "2.0");
CHECK(result["id"] == 2);
REQUIRE(result.contains("result"));
}
TEST_CASE("MCP Server - tool schema has all required MCP fields",
"[mcp][server][tools][schema][catch2]") {
auto transport = std::make_unique<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
json result = server->testListTools();
REQUIRE(result.is_object());
REQUIRE(result.contains("tools"));
REQUIRE(result["tools"].is_array());
REQUIRE(result["tools"].size() > 0);
// Check first tool for complete MCP compliance
const auto& firstTool = result["tools"][0];
REQUIRE(firstTool.is_object());
// Required fields per MCP spec 2025-11-25
CHECK(firstTool.contains("name"));
CHECK(firstTool.contains("description"));
CHECK(firstTool.contains("inputSchema"));
// Verify name is a string
CHECK(firstTool["name"].is_string());
CHECK(!firstTool["name"].get<std::string>().empty());
// Verify description is a string
CHECK(firstTool["description"].is_string());
// Verify inputSchema has proper JSON Schema structure
const auto& schema = firstTool["inputSchema"];
REQUIRE(schema.is_object());
CHECK(schema.contains("type"));
CHECK(schema["type"] == "object");
if (schema.contains("properties")) {
CHECK(schema["properties"].is_object());
}
if (schema.contains("required")) {
CHECK(schema["required"].is_array());
}
}
TEST_CASE("MCP Server - handles tools/list without params field",
"[mcp][server][tools][edgecase][catch2]") {
auto transport = std::make_unique<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
// Some clients might not send params at all
json request = {
{"jsonrpc", "2.0"}, {"id", 3}, {"method", "tools/list"} // No params field
};
auto response = server->handleRequestPublic(request);
REQUIRE(response.has_value());
json result = response.value();
REQUIRE(result.is_object());
CHECK(result["jsonrpc"] == "2.0");
CHECK(result["id"] == 3);
REQUIRE(result.contains("result"));
REQUIRE(result["result"].contains("tools"));
}
TEST_CASE("MCP Server - debug print tools/list response", "[mcp][server][tools][debug][catch2]") {
auto transport = std::make_unique<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
json request = {
{"jsonrpc", "2.0"}, {"id", 99}, {"method", "tools/list"}, {"params", json::object()}};
auto response = server->handleRequestPublic(request);
REQUIRE(response.has_value());
// Print the full response for debugging
std::cout << "\n=== MCP tools/list Response ===\n";
std::cout << response.value().dump(2) << "\n";
std::cout << "================================\n" << std::endl;
// Also print just the listTools result
std::cout << "\n=== Direct listTools() Result ===\n";
std::cout << server->testListTools().dump(2) << "\n";
std::cout << "==================================\n" << std::endl;
}
TEST_CASE("MCP Server - tools/list response has no null fields",
"[mcp][server][tools][nullcheck][catch2]") {
auto transport = std::make_unique<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
json request = {
{"jsonrpc", "2.0"}, {"id", 100}, {"method", "tools/list"}, {"params", json::object()}};
auto response = server->handleRequestPublic(request);
REQUIRE(response.has_value());
std::string responseStr = response.value().dump();
// Check that there are no null values in the response
// Some MCP clients might not handle null values well
CHECK(responseStr.find("null") == std::string::npos);
}
TEST_CASE("MCP Server - tools/list response is valid JSON-RPC 2.0",
"[mcp][server][tools][jsonrpc][catch2]") {
auto transport = std::make_unique<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
json request = {
{"jsonrpc", "2.0"}, {"id", 101}, {"method", "tools/list"}, {"params", json::object()}};
auto response = server->handleRequestPublic(request);
REQUIRE(response.has_value());
json result = response.value();
// Verify strict JSON-RPC 2.0 format
REQUIRE(result.contains("jsonrpc"));
CHECK(result["jsonrpc"] == "2.0");
REQUIRE(result.contains("id"));
CHECK(result["id"] == 101);
REQUIRE(result.contains("result"));
CHECK(!result.contains("error"));
// Verify result structure
const auto& res = result["result"];
REQUIRE(res.is_object());
REQUIRE(res.contains("tools"));
REQUIRE(res["tools"].is_array());
// Each tool must have name and inputSchema
for (const auto& tool : res["tools"]) {
REQUIRE(tool.is_object());
REQUIRE(tool.contains("name"));
REQUIRE(tool["name"].is_string());
REQUIRE(!tool["name"].get<std::string>().empty());
REQUIRE(tool.contains("inputSchema"));
REQUIRE(tool["inputSchema"].is_object());
}
}
// ============================================================================
// Stdio Transport Tests
// ============================================================================
TEST_CASE("MCP Server - StdioTransport send and receive roundtrip",
"[mcp][server][stdio][roundtrip][catch2]") {
// Create a string-based stream for testing stdio transport
std::stringstream inputStream;
std::stringstream outputStream;
// Prepare a tools/list request
json request = {
{"jsonrpc", "2.0"}, {"id", 200}, {"method", "tools/list"}, {"params", json::object()}};
// Write request to input stream (NDJSON format)
inputStream << request.dump() << "\n";
// Note: We can't easily test the full roundtrip without mocking std::cin/cout
// But we can verify the transport state
StdioTransport transport;
CHECK(transport.isConnected());
// The transport should be able to send
json testMessage = {{"jsonrpc", "2.0"}, {"id", 1}, {"result", {{"test", true}}}};
transport.send(testMessage);
// Transport should still be connected after send
CHECK(transport.isConnected());
}
TEST_CASE("MCP Server - verify tools available immediately after construction",
"[mcp][server][tools][immediate][catch2]") {
// This test verifies that tools are available immediately after MCPServer construction
// without any additional initialization steps
auto transport = std::make_unique<NullTransport>();
// Construct server
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
// Immediately query tools - should work without calling start() or any other method
json result = server->testListTools();
REQUIRE(result.is_object());
REQUIRE(result.contains("tools"));
REQUIRE(result["tools"].is_array());
CHECK(result["tools"].size() > 0);
// Verify we can call it multiple times
json result2 = server->testListTools();
CHECK(result2["tools"].size() == result["tools"].size());
}
TEST_CASE("MCP Server - tools/list before initialize should still work",
"[mcp][server][tools][preinit][catch2]") {
// Some MCP clients may query tools/list before sending initialize
// This should still work per MCP spec
auto transport = std::make_unique<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
// Query tools without initializing
json request = {
{"jsonrpc", "2.0"}, {"id", 300}, {"method", "tools/list"}, {"params", json::object()}};
auto response = server->handleRequestPublic(request);
REQUIRE(response.has_value());
json result = response.value();
REQUIRE(result.is_object());
CHECK(result["jsonrpc"] == "2.0");
CHECK(result["id"] == 300);
REQUIRE(result.contains("result"));
REQUIRE(result["result"].contains("tools"));
REQUIRE(result["result"]["tools"].is_array());
CHECK(result["result"]["tools"].size() > 0);
}
// ============================================================================
// Full Handshake Sequence Tests
// ============================================================================
TEST_CASE("MCP Server - full OpenCode handshake sequence",
"[mcp][server][handshake][opencodcompat][catch2]") {
auto transport = std::make_unique<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
// Step 1: OpenCode sends initialize
json initRequest = {{"jsonrpc", "2.0"},
{"id", 1},
{"method", "initialize"},
{"params",
{{"protocolVersion", "2025-11-25"},
{"clientInfo", {{"name", "opencode"}, {"version", "1.0.0"}}},
{"capabilities", json::object()}}}};
auto initResponse = server->handleRequestPublic(initRequest);
REQUIRE(initResponse.has_value());
CHECK(initResponse.value()["jsonrpc"] == "2.0");
CHECK(initResponse.value()["id"] == 1);
REQUIRE(initResponse.value().contains("result"));
// Step 2: OpenCode sends notifications/initialized
json initializedNotification = {
{"jsonrpc", "2.0"}, {"method", "notifications/initialized"}, {"params", json::object()}};
auto notifResponse = server->handleRequestPublic(initializedNotification);
// Notifications don't return responses
// Step 3: OpenCode sends tools/list
json toolsRequest = {
{"jsonrpc", "2.0"}, {"id", 2}, {"method", "tools/list"}, {"params", json::object()}};
auto toolsResponse = server->handleRequestPublic(toolsRequest);
REQUIRE(toolsResponse.has_value());
CHECK(toolsResponse.value()["jsonrpc"] == "2.0");
CHECK(toolsResponse.value()["id"] == 2);
REQUIRE(toolsResponse.value().contains("result"));
REQUIRE(toolsResponse.value()["result"].contains("tools"));
CHECK(toolsResponse.value()["result"]["tools"].size() > 0);
}
TEST_CASE("MCP Server - tools/list without prior initialize",
"[mcp][server][handshake][nopreinit][catch2]") {
// Some clients might query tools before initialize (though not spec-compliant)
auto transport = std::make_unique<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
// Send tools/list without initialize
json toolsRequest = {
{"jsonrpc", "2.0"}, {"id", 1}, {"method", "tools/list"}, {"params", json::object()}};
auto toolsResponse = server->handleRequestPublic(toolsRequest);
REQUIRE(toolsResponse.has_value());
CHECK(toolsResponse.value()["jsonrpc"] == "2.0");
CHECK(toolsResponse.value()["id"] == 1);
REQUIRE(toolsResponse.value().contains("result"));
REQUIRE(toolsResponse.value()["result"].contains("tools"));
CHECK(toolsResponse.value()["result"]["tools"].size() > 0);
}
TEST_CASE("MCP Server - rapid sequential tools/list requests",
"[mcp][server][tools][rapid][catch2]") {
auto transport = std::make_unique<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
// Send multiple tools/list requests rapidly
for (int i = 0; i < 10; ++i) {
json request = {
{"jsonrpc", "2.0"}, {"id", i}, {"method", "tools/list"}, {"params", json::object()}};
auto response = server->handleRequestPublic(request);
REQUIRE(response.has_value());
CHECK(response.value()["id"] == i);
REQUIRE(response.value()["result"]["tools"].is_array());
CHECK(response.value()["result"]["tools"].size() > 0);
}
}
// ============================================================================
// OpenCode-Specific Issue Tests
// Based on analysis of OpenCode's MCP client implementation
// ============================================================================
TEST_CASE("MCP Server - responds quickly to tools/list after construction",
"[mcp][server][tools][timing][opencodcompat][catch2]") {
// OpenCode calls listTools immediately after connect with a timeout
// We need to ensure the response is sent quickly
auto transport = std::make_unique<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
// Simulate the exact sequence OpenCode uses
json request = {
{"jsonrpc", "2.0"}, {"id", 1}, {"method", "tools/list"}, {"params", json::object()}};
auto start = std::chrono::steady_clock::now();
auto response = server->handleRequestPublic(request);
auto end = std::chrono::steady_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
REQUIRE(response.has_value());
REQUIRE(response.value()["result"]["tools"].is_array());
// Should respond in less than 1 second (OpenCode's timeout is 30s but it feels longer)
CHECK(duration.count() < 1000);
}
TEST_CASE("MCP Server - handles concurrent tools/list requests",
"[mcp][server][tools][concurrent][catch2]") {
// OpenCode might query tools multiple times or concurrently
auto transport = std::make_unique<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
std::vector<std::future<MessageResult>> futures;
// Launch multiple concurrent requests
for (int i = 0; i < 5; ++i) {
json request = {
{"jsonrpc", "2.0"}, {"id", i}, {"method", "tools/list"}, {"params", json::object()}};
futures.push_back(std::async(std::launch::async, [server, request]() {
return server->handleRequestPublic(request);
}));
}
// All should succeed
for (size_t i = 0; i < futures.size(); ++i) {
auto response = futures[i].get();
REQUIRE(response.has_value());
CHECK(response.value()["id"] == static_cast<int>(i));
REQUIRE(response.value()["result"]["tools"].is_array());
CHECK(response.value()["result"]["tools"].size() > 0);
}
}
TEST_CASE("MCP Server - tools/list works without daemon client",
"[mcp][server][tools][nodaemon][catch2]") {
// If daemon is not available, tools/list should still work
// The tools are registered in the constructor, not dependent on daemon
auto transport = std::make_unique<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
// Force daemon client to be null by resetting it
server->testShutdown();
json request = {
{"jsonrpc", "2.0"}, {"id", 1}, {"method", "tools/list"}, {"params", json::object()}};
auto response = server->handleRequestPublic(request);
REQUIRE(response.has_value());
REQUIRE(response.value()["result"]["tools"].is_array());
CHECK(response.value()["result"]["tools"].size() > 0);
}
TEST_CASE("MCP Server - stderr logging doesn't block stdout",
"[mcp][server][stdio][stderr][catch2]") {
// This test verifies that stderr logging doesn't interfere with stdout
// OpenCode captures stderr separately, so heavy logging shouldn't block
auto transport = std::make_unique<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
// The server constructor logs to stderr - verify we can still get tools
json result = server->testListTools();
REQUIRE(result.is_object());
REQUIRE(result.contains("tools"));
CHECK(result["tools"].size() > 0);
}
// ============================================================================
// Protocol Version Compatibility Tests
// ============================================================================
TEST_CASE("MCP Server - supports protocol version 2025-11-25",
"[mcp][server][protocol][version][2025-11-25][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();
CHECK(result["jsonrpc"] == "2.0");
CHECK(result["id"] == 1);
REQUIRE(result.contains("result"));
CHECK(result["result"]["protocolVersion"] == "2025-11-25");
REQUIRE(result["result"].contains("capabilities"));
REQUIRE(result["result"].contains("serverInfo"));
}
TEST_CASE("MCP Server - supports protocol version 2025-06-18",
"[mcp][server][protocol][version][2025-06-18][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();
CHECK(result["result"]["protocolVersion"] == "2025-06-18");
}
TEST_CASE("MCP Server - supports protocol version 2025-03-26",
"[mcp][server][protocol][version][2025-03-26][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");
}
TEST_CASE("MCP Server - supports protocol version 2024-11-05",
"[mcp][server][protocol][version][2024-11-05][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");
}
TEST_CASE("MCP Server - supports protocol version 2024-10-07",
"[mcp][server][protocol][version][2024-10-07][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");
}
TEST_CASE("MCP Server - falls back to latest for unsupported protocol version",
"[mcp][server][protocol][version][fallback][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", "2023-01-01"}, // Unsupported version
{"clientInfo", {{"name", "test"}, {"version", "1.0"}}},
{"capabilities", json::object()}}}};
auto response = server->handleRequestPublic(initRequest);
REQUIRE(response.has_value());
json result = response.value();
// Should fall back to latest version (2025-11-25)
CHECK(result["result"]["protocolVersion"] == "2025-11-25");
}
TEST_CASE("MCP Server - strict protocol mode rejects unsupported version",
"[mcp][server][protocol][version][strict][catch2]") {
auto transport = std::make_unique<NullTransport>();
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
// Enable strict protocol mode via environment variable.
yams::test::ScopedEnvVar strictProtocol{"YAMS_MCP_STRICT_PROTOCOL", std::string{"1"}};
// Create a new server with strict mode enabled
auto strictServer = std::make_shared<yams::mcp::MCPServer>(std::make_unique<NullTransport>());
json initRequest = {{"jsonrpc", "2.0"},
{"id", 1},
{"method", "initialize"},
{"params",
{{"protocolVersion", "2023-01-01"}, // Unsupported version
{"clientInfo", {{"name", "test"}, {"version", "1.0"}}},
{"capabilities", json::object()}}}};
auto response = strictServer->handleRequestPublic(initRequest);
REQUIRE(response.has_value());
json result = response.value();
// Should return an error
REQUIRE(result.contains("error"));
CHECK(result["error"]["code"] == -32901); // kErrUnsupportedProtocolVersion
}
TEST_CASE("MCP Server - uses latest version when protocolVersion not specified",
"[mcp][server][protocol][version][default][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",
{
{"clientInfo", {{"name", "test"}, {"version", "1.0"}}},
{"capabilities", json::object()}
// No protocolVersion specified
}}};
auto response = server->handleRequestPublic(initRequest);
REQUIRE(response.has_value());
json result = response.value();
// Should use latest version
CHECK(result["result"]["protocolVersion"] == "2025-11-25");
}