/** * @file test_mcp_direct_requests.cpp * @brief Test direct POST requests to MCP server * * This file contains tests for direct HTTP requests to the MCP server, * testing various request types and ID formats. * Based on specification 2024-11-05. */ #include "mcp_server.h" #include "mcp_client.h" #include "mcp_tool.h" #include "mcp_resource.h" #include #include #include #include #include // Test fixture for setting up and cleaning up the test environment class DirectRequestTest : public ::testing::Test { protected: void SetUp() override { // Create and configure server server = std::make_unique("localhost", 8096); server->set_server_info("TestServer", mcp::MCP_VERSION); // Set server capabilities mcp::json capabilities = { {"tools", {{"listChanged", true}}}, {"resources", {{"listChanged", true}}} }; server->set_capabilities(capabilities); // Register method handlers server->register_method("ping", [](const mcp::json& params) { return mcp::json{{"pong", true}}; }); // Register tools mcp::tool test_tool = mcp::tool_builder("test_tool") .with_description("Test Tool") .with_string_param("input", "Input parameter") .build(); server->register_tool(test_tool, [](const mcp::json& params) -> mcp::json { std::string input = params.contains("input") ? params["input"].get() : "Default input"; return { { {"type", "text"}, {"text", "Result: " + input} } }; }); // Register resources auto test_resource = std::make_shared("Test Resource", "API resource for testing"); test_resource->register_handler("hello", [](const mcp::json& params) { std::string name = params.contains("name") ? params["name"].get() : "World"; return mcp::json{{"message", "Hello, " + name + "!"}}; }, "Greeting API"); server->register_resource("/test", test_resource); // Start server server_thread = std::thread([this]() { server->start(true); }); // Wait for server to start std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Create HTTP client http_client = std::make_unique("localhost", 8096); http_client->set_connection_timeout(5, 0); http_client->set_read_timeout(5, 0); http_client->set_write_timeout(5, 0); } void TearDown() override { // Stop server server->stop(); // Wait for server thread to end if (server_thread.joinable()) { server_thread.join(); } } // Send JSON-RPC request and return response mcp::json send_jsonrpc_request(const mcp::json& request) { httplib::Headers headers = { {"Content-Type", "application/json"} }; auto res = http_client->Post("/jsonrpc", headers, request.dump(), "application/json"); EXPECT_TRUE(res != nullptr); if (!res) { return mcp::json::object(); } EXPECT_EQ(res->status, 200); try { return mcp::json::parse(res->body); } catch (const mcp::json::exception& e) { ADD_FAILURE() << "Failed to parse response: " << e.what(); return mcp::json::object(); } } std::unique_ptr server; std::unique_ptr http_client; std::thread server_thread; }; // Test initialization with numeric ID TEST_F(DirectRequestTest, InitializeWithNumericId) { // Create initialization request with numeric ID mcp::json request = { {"jsonrpc", "2.0"}, {"id", 12345}, {"method", "initialize"}, {"params", { {"protocolVersion", mcp::MCP_VERSION}, {"capabilities", { {"tools", {{"listChanged", true}}} }}, {"clientInfo", { {"name", "TestClient"}, {"version", "1.0.0"} }} }} }; // Send request mcp::json response = send_jsonrpc_request(request); // Verify response EXPECT_EQ(response["jsonrpc"], "2.0"); EXPECT_EQ(response["id"], 12345); EXPECT_TRUE(response.contains("result")); EXPECT_FALSE(response.contains("error")); // Verify result content EXPECT_EQ(response["result"]["protocolVersion"], mcp::MCP_VERSION); EXPECT_TRUE(response["result"].contains("capabilities")); EXPECT_TRUE(response["result"].contains("serverInfo")); EXPECT_EQ(response["result"]["serverInfo"]["name"], "TestServer"); } // Test initialization with string ID TEST_F(DirectRequestTest, InitializeWithStringId) { // Create initialization request with string ID mcp::json request = { {"jsonrpc", "2.0"}, {"id", "request-id-abc123"}, {"method", "initialize"}, {"params", { {"protocolVersion", mcp::MCP_VERSION}, {"capabilities", { {"tools", {{"listChanged", true}}} }}, {"clientInfo", { {"name", "TestClient"}, {"version", "1.0.0"} }} }} }; // Send request mcp::json response = send_jsonrpc_request(request); // Verify response EXPECT_EQ(response["jsonrpc"], "2.0"); EXPECT_EQ(response["id"], "request-id-abc123"); EXPECT_TRUE(response.contains("result")); EXPECT_FALSE(response.contains("error")); // Verify result content EXPECT_EQ(response["result"]["protocolVersion"], mcp::MCP_VERSION); EXPECT_TRUE(response["result"].contains("capabilities")); EXPECT_TRUE(response["result"].contains("serverInfo")); EXPECT_EQ(response["result"]["serverInfo"]["name"], "TestServer"); } // Test getting tools list TEST_F(DirectRequestTest, GetTools) { // Initialize first mcp::json init_request = { {"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", { {"protocolVersion", mcp::MCP_VERSION}, {"capabilities", {}}, {"clientInfo", { {"name", "TestClient"}, {"version", "1.0.0"} }} }} }; send_jsonrpc_request(init_request); // Get tools list mcp::json request = { {"jsonrpc", "2.0"}, {"id", 2}, {"method", "getTools"}, {"params", {}} }; mcp::json response = send_jsonrpc_request(request); // Verify response EXPECT_EQ(response["jsonrpc"], "2.0"); EXPECT_EQ(response["id"], 2); // Server may not implement getTools method, so check if error is returned if (response.contains("error")) { EXPECT_EQ(response["error"]["code"], static_cast(mcp::error_code::method_not_found)); } else { EXPECT_TRUE(response.contains("result")); EXPECT_FALSE(response.contains("error")); // Verify tools list EXPECT_TRUE(response["result"].is_array()); EXPECT_GE(response["result"].size(), 1); // Verify test tool bool found_test_tool = false; for (const auto& tool : response["result"]) { if (tool["name"] == "test_tool") { found_test_tool = true; EXPECT_EQ(tool["description"], "Test Tool"); EXPECT_TRUE(tool.contains("parameters")); break; } } EXPECT_TRUE(found_test_tool); } } // Test calling a tool TEST_F(DirectRequestTest, CallTool) { // Initialize first mcp::json init_request = { {"jsonrpc", "2.0"}, {"id", "init-1"}, {"method", "initialize"}, {"params", { {"protocolVersion", mcp::MCP_VERSION}, {"capabilities", {}}, {"clientInfo", { {"name", "TestClient"}, {"version", "1.0.0"} }} }} }; send_jsonrpc_request(init_request); // Call tool mcp::json request = { {"jsonrpc", "2.0"}, {"id", "call-1"}, {"method", "callTool"}, {"params", { {"name", "test_tool"}, {"parameters", { {"input", "Test input"} }} }} }; mcp::json response = send_jsonrpc_request(request); // Verify response EXPECT_EQ(response["jsonrpc"], "2.0"); EXPECT_EQ(response["id"], "call-1"); // Server may not implement callTool method, so check if error is returned if (response.contains("error")) { EXPECT_EQ(response["error"]["code"], static_cast(mcp::error_code::method_not_found)); } else { EXPECT_TRUE(response.contains("result")); EXPECT_FALSE(response.contains("error")); // Verify tool call result EXPECT_EQ(response["result"]["output"], "Result: Test input"); } } // Test getting resources list TEST_F(DirectRequestTest, GetResources) { // Initialize first mcp::json init_request = { {"jsonrpc", "2.0"}, {"id", 100}, {"method", "initialize"}, {"params", { {"protocolVersion", mcp::MCP_VERSION}, {"capabilities", {}}, {"clientInfo", { {"name", "TestClient"}, {"version", "1.0.0"} }} }} }; send_jsonrpc_request(init_request); // Get resources list mcp::json request = { {"jsonrpc", "2.0"}, {"id", 101}, {"method", "getResources"}, {"params", {}} }; mcp::json response = send_jsonrpc_request(request); // Verify response EXPECT_EQ(response["jsonrpc"], "2.0"); EXPECT_EQ(response["id"], 101); // Server may not implement getResources method, so check if error is returned if (response.contains("error")) { EXPECT_EQ(response["error"]["code"], static_cast(mcp::error_code::method_not_found)); } else { EXPECT_TRUE(response.contains("result")); EXPECT_FALSE(response.contains("error")); // Verify resources list EXPECT_TRUE(response["result"].is_array()); EXPECT_GE(response["result"].size(), 1); // Verify test resource bool found_test_resource = false; for (const auto& resource : response["result"]) { if (resource["path"] == "/test") { found_test_resource = true; EXPECT_EQ(resource["name"], "Test Resource"); EXPECT_EQ(resource["description"], "API resource for testing"); break; } } EXPECT_TRUE(found_test_resource); } } // Test accessing a resource TEST_F(DirectRequestTest, AccessResource) { // Initialize first mcp::json init_request = { {"jsonrpc", "2.0"}, {"id", "init-res"}, {"method", "initialize"}, {"params", { {"protocolVersion", mcp::MCP_VERSION}, {"capabilities", {}}, {"clientInfo", { {"name", "TestClient"}, {"version", "1.0.0"} }} }} }; send_jsonrpc_request(init_request); // Access resource mcp::json request = { {"jsonrpc", "2.0"}, {"id", "access-res"}, {"method", "accessResource"}, {"params", { {"path", "/test"}, {"operation", "hello"}, {"parameters", { {"name", "Test User"} }} }} }; mcp::json response = send_jsonrpc_request(request); // Verify response EXPECT_EQ(response["jsonrpc"], "2.0"); EXPECT_EQ(response["id"], "access-res"); // Server may not implement accessResource method, so check if error is returned if (response.contains("error")) { EXPECT_EQ(response["error"]["code"], static_cast(mcp::error_code::method_not_found)); } else { EXPECT_TRUE(response.contains("result")); EXPECT_FALSE(response.contains("error")); // Verify resource access result EXPECT_EQ(response["result"]["message"], "Hello, Test User!"); } } // Test sending notification (no ID) TEST_F(DirectRequestTest, SendNotification) { // Initialize first mcp::json init_request = { {"jsonrpc", "2.0"}, {"id", 200}, {"method", "initialize"}, {"params", { {"protocolVersion", mcp::MCP_VERSION}, {"capabilities", {}}, {"clientInfo", { {"name", "TestClient"}, {"version", "1.0.0"} }} }} }; send_jsonrpc_request(init_request); // Send notification mcp::json notification = { {"jsonrpc", "2.0"}, {"method", "initialized"}, {"params", {}} }; httplib::Headers headers = { {"Content-Type", "application/json"} }; auto res = http_client->Post("/jsonrpc", headers, notification.dump(), "application/json"); // Verify response (notifications may have empty response or error response) EXPECT_TRUE(res != nullptr); EXPECT_EQ(res->status, 200); // Don't check if response body is empty, as server implementation may return an empty object } // Test error handling - method not found TEST_F(DirectRequestTest, MethodNotFound) { mcp::json request = { {"jsonrpc", "2.0"}, {"id", 999}, {"method", "nonExistentMethod"}, {"params", {}} }; mcp::json response = send_jsonrpc_request(request); // Verify error response EXPECT_EQ(response["jsonrpc"], "2.0"); EXPECT_EQ(response["id"], 999); EXPECT_FALSE(response.contains("result")); EXPECT_TRUE(response.contains("error")); EXPECT_EQ(response["error"]["code"], static_cast(mcp::error_code::method_not_found)); } // Test error handling - invalid parameters TEST_F(DirectRequestTest, InvalidParams) { // Initialize first mcp::json init_request = { {"jsonrpc", "2.0"}, {"id", "init-err"}, {"method", "initialize"}, {"params", { {"protocolVersion", mcp::MCP_VERSION}, {"capabilities", {}}, {"clientInfo", { {"name", "TestClient"}, {"version", "1.0.0"} }} }} }; send_jsonrpc_request(init_request); // Call tool but missing required parameters mcp::json request = { {"jsonrpc", "2.0"}, {"id", "invalid-params"}, {"method", "callTool"}, {"params", { // Missing tool name {"parameters", { {"input", "Test input"} }} }} }; mcp::json response = send_jsonrpc_request(request); // Verify error response EXPECT_EQ(response["jsonrpc"], "2.0"); EXPECT_EQ(response["id"], "invalid-params"); EXPECT_FALSE(response.contains("result")); EXPECT_TRUE(response.contains("error")); // Server may return method_not_found or invalid_params error int error_code = response["error"]["code"]; EXPECT_TRUE(error_code == static_cast(mcp::error_code::method_not_found) || error_code == static_cast(mcp::error_code::invalid_params)); } // Test using client API and direct requests combination TEST_F(DirectRequestTest, ClientAndDirectRequests) { // Use client API to initialize mcp::client client("localhost", 8096); client.set_timeout(5); bool initialized = client.initialize("TestClient", mcp::MCP_VERSION); EXPECT_TRUE(initialized); // Directly send get tools list request mcp::json request = { {"jsonrpc", "2.0"}, {"id", "mixed-1"}, {"method", "getTools"}, {"params", {}} }; mcp::json response = send_jsonrpc_request(request); // Verify response EXPECT_EQ(response["jsonrpc"], "2.0"); EXPECT_EQ(response["id"], "mixed-1"); // Server may not implement getTools method if (!response.contains("error")) { EXPECT_TRUE(response.contains("result")); } // Use client API to call tool try { mcp::json tool_result = client.call_tool("test_tool", {{"input", "Client API call"}}); EXPECT_EQ(tool_result["content"][0]["text"], "Result: Client API call"); } catch (const std::exception& e) { // Client API may throw exception if server doesn't implement callTool method std::cout << "Client API tool call failed: " << e.what() << std::endl; } } // Test logging functionality TEST_F(DirectRequestTest, LoggingTest) { // Initialize first mcp::json init_request = { {"jsonrpc", "2.0"}, {"id", "log-init"}, {"method", "initialize"}, {"params", { {"protocolVersion", mcp::MCP_VERSION}, {"capabilities", {}}, {"clientInfo", { {"name", "TestClient"}, {"version", "1.0.0"} }} }} }; send_jsonrpc_request(init_request); // Send log request mcp::json request = { {"jsonrpc", "2.0"}, {"id", "log-1"}, {"method", "log"}, {"params", { {"level", "info"}, {"message", "This is a test log message"}, {"details", { {"source", "test_case"}, {"timestamp", "2023-01-01T12:00:00Z"} }} }} }; mcp::json response = send_jsonrpc_request(request); // Verify response EXPECT_EQ(response["jsonrpc"], "2.0"); EXPECT_EQ(response["id"], "log-1"); // Server may not implement log method if (response.contains("error")) { EXPECT_EQ(response["error"]["code"], static_cast(mcp::error_code::method_not_found)); } else { EXPECT_TRUE(response.contains("result")); EXPECT_FALSE(response.contains("error")); } } // Test sampling functionality TEST_F(DirectRequestTest, SamplingTest) { // Initialize first mcp::json init_request = { {"jsonrpc", "2.0"}, {"id", 300}, {"method", "initialize"}, {"params", { {"protocolVersion", mcp::MCP_VERSION}, {"capabilities", {}}, {"clientInfo", { {"name", "TestClient"}, {"version", "1.0.0"} }} }} }; send_jsonrpc_request(init_request); // Send sampling request mcp::json request = { {"jsonrpc", "2.0"}, {"id", 301}, {"method", "sample"}, {"params", { {"prompt", "This is a test prompt"}, {"parameters", { {"temperature", 0.7}, {"max_tokens", 100} }} }} }; mcp::json response = send_jsonrpc_request(request); // Verify response (may fail, as server may not implement sampling functionality) if (response.contains("error")) { EXPECT_EQ(response["error"]["code"], static_cast(mcp::error_code::method_not_found)); } else { EXPECT_TRUE(response.contains("result")); } } // Test batch request processing TEST_F(DirectRequestTest, BatchRequestTest) { // Create batch request mcp::json batch_request = mcp::json::array(); // Add initialization request batch_request.push_back({ {"jsonrpc", "2.0"}, {"id", "batch-1"}, {"method", "initialize"}, {"params", { {"protocolVersion", mcp::MCP_VERSION}, {"capabilities", {}}, {"clientInfo", { {"name", "TestClient"}, {"version", "1.0.0"} }} }} }); // Add ping request batch_request.push_back({ {"jsonrpc", "2.0"}, {"id", "batch-2"}, {"method", "ping"}, {"params", {}} }); // Add notification batch_request.push_back({ {"jsonrpc", "2.0"}, {"method", "initialized"}, {"params", {}} }); // Send batch request httplib::Headers headers = { {"Content-Type", "application/json"} }; auto res = http_client->Post("/jsonrpc", headers, batch_request.dump(), "application/json"); // Verify response EXPECT_TRUE(res != nullptr); EXPECT_EQ(res->status, 200); // Parse response try { mcp::json response = mcp::json::parse(res->body); // Batch requests may not be supported, so check if error is returned if (response.is_object() && response.contains("error")) { EXPECT_EQ(response["error"]["code"], static_cast(mcp::error_code::invalid_request)); EXPECT_TRUE(response["error"]["message"].get().find("Batch") != std::string::npos); } else if (response.is_array()) { // If batch requests are supported, should return an array EXPECT_GE(response.size(), 2); // At least two responses (not including notification) // Check each response for (const auto& resp : response) { EXPECT_EQ(resp["jsonrpc"], "2.0"); EXPECT_TRUE(resp.contains("id")); if (resp["id"] == "batch-1") { EXPECT_TRUE(resp.contains("result")); EXPECT_TRUE(resp["result"].contains("protocolVersion")); } else if (resp["id"] == "batch-2") { EXPECT_TRUE(resp.contains("result")); EXPECT_TRUE(resp["result"].contains("pong")); } } } } catch (const mcp::json::exception& e) { ADD_FAILURE() << "Failed to parse batch response: " << e.what(); } } // Test request cancellation TEST_F(DirectRequestTest, CancelRequestTest) { // Initialize first mcp::json init_request = { {"jsonrpc", "2.0"}, {"id", "cancel-init"}, {"method", "initialize"}, {"params", { {"protocolVersion", mcp::MCP_VERSION}, {"capabilities", {}}, {"clientInfo", { {"name", "TestClient"}, {"version", "1.0.0"} }} }} }; send_jsonrpc_request(init_request); // Send cancel request mcp::json request = { {"jsonrpc", "2.0"}, {"id", "cancel-1"}, {"method", "$/cancelRequest"}, {"params", { {"id", "some-request-id"} }} }; mcp::json response = send_jsonrpc_request(request); // Verify response (may fail, as server may not implement cancellation functionality) if (response.contains("error")) { EXPECT_EQ(response["error"]["code"], static_cast(mcp::error_code::method_not_found)); } else { EXPECT_TRUE(response.contains("result")); } } // Test using different types of IDs TEST_F(DirectRequestTest, DifferentIdTypesTest) { // Use integer ID mcp::json int_request = { {"jsonrpc", "2.0"}, {"id", 42}, {"method", "ping"}, {"params", {}} }; mcp::json int_response = send_jsonrpc_request(int_request); EXPECT_EQ(int_response["id"], 42); // Use string ID mcp::json string_request = { {"jsonrpc", "2.0"}, {"id", "string-id-42"}, {"method", "ping"}, {"params", {}} }; mcp::json string_response = send_jsonrpc_request(string_request); EXPECT_EQ(string_response["id"], "string-id-42"); // Use float ID (should also work) mcp::json float_request = { {"jsonrpc", "2.0"}, {"id", 3.14}, {"method", "ping"}, {"params", {}} }; mcp::json float_response = send_jsonrpc_request(float_request); EXPECT_EQ(float_response["id"], 3.14); // Use null ID (this should be treated as a notification) mcp::json null_request = { {"jsonrpc", "2.0"}, {"id", nullptr}, {"method", "ping"}, {"params", {}} }; httplib::Headers headers = { {"Content-Type", "application/json"} }; auto res = http_client->Post("/jsonrpc", headers, null_request.dump(), "application/json"); EXPECT_TRUE(res != nullptr); EXPECT_EQ(res->status, 200); // Use complex object as ID (this may not be supported) mcp::json complex_request = { {"jsonrpc", "2.0"}, {"id", {{"complex", "id"}}}, {"method", "ping"}, {"params", {}} }; mcp::json complex_response = send_jsonrpc_request(complex_request); // Verify response (may fail, as server may not support complex objects as ID) if (complex_response.contains("error")) { EXPECT_EQ(complex_response["error"]["code"], static_cast(mcp::error_code::invalid_request)); } }