/** * @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 #include #include #include #include // Mock tool handler function static mcp::json test_tool_handler(const mcp::json& params) { if (params.contains("input")) { return { { {"type", "text"}, {"text", "Result: " + params["input"].get()} } }; } else { return { { {"type", "text"}, {"text", "Default result"} } }; } } // 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}}} }; server->set_capabilities(capabilities); // 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, test_tool_handler); // 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(); } // 检查状态码,202表示请求已接受,但响应将通过SSE发送 if (res->status == 202) { // 在实际测试中,我们需要等待SSE响应 // 但在这个测试中,我们只是返回一个空对象 // 实际应用中应该使用客户端类来处理这种情况 std::cout << "收到202 Accepted响应,实际响应将通过SSE发送" << std::endl; 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(); } } void mock_initialize() { // Mock initialization request 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); send_jsonrpc_request({ {"jsonrpc", "2.0"}, {"method", "notifications/initialized"} }); } std::unique_ptr server; std::unique_ptr http_client; std::thread server_thread; }; // Test if server can handle requests w/ and w/o initialization TEST_F(DirectRequestTest, InitializationTest) { // Send request without initialization mcp::json request = { {"jsonrpc", "2.0"}, {"id", "no-init-1"}, {"method", "ping"}, {"params", {}} }; mcp::json response = send_jsonrpc_request(request); // Only ping and logging methods should be supported without initialization EXPECT_EQ(response["jsonrpc"], "2.0"); EXPECT_EQ(response["id"], "no-init-1"); EXPECT_TRUE(response.contains("result")); request = { {"jsonrpc", "2.0"}, {"id", "no-init-2"}, {"method", "tools/call"}, {"params", { {"name", "test_tool"}, {"arguments", { {"input", "Test input"} }} }} }; response = send_jsonrpc_request(request); // Should return error EXPECT_EQ(response["jsonrpc"], "2.0"); EXPECT_EQ(response["id"], "no-init-2"); EXPECT_FALSE(response.contains("result")); EXPECT_TRUE(response.contains("error")); EXPECT_EQ(response["error"]["code"], static_cast(mcp::error_code::invalid_request)); // Mock initialization request 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"} }} }} }; response = send_jsonrpc_request(init_request); EXPECT_EQ(response["jsonrpc"], "2.0"); EXPECT_EQ(response["id"], "init-1"); EXPECT_TRUE(response.contains("result")); EXPECT_FALSE(response.contains("error")); response = send_jsonrpc_request({ {"jsonrpc", "2.0"}, {"method", "notifications/initialized"} }); EXPECT_TRUE(response.empty()); // Now all methods should be supported request = { {"jsonrpc", "2.0"}, {"id", "init-2"}, {"method", "ping"}, {"params", {}} }; response = send_jsonrpc_request(request); // Should return result EXPECT_EQ(response["jsonrpc"], "2.0"); EXPECT_EQ(response["id"], "init-2"); EXPECT_TRUE(response.contains("result")); request = { {"jsonrpc", "2.0"}, {"id", "init-3"}, {"method", "tools/call"}, {"params", { {"name", "test_tool"}, {"arguments", { {"input", "Test input"} }} }} }; response = send_jsonrpc_request(request); // Should return result EXPECT_EQ(response["jsonrpc"], "2.0"); EXPECT_EQ(response["id"], "init-3"); EXPECT_TRUE(response.contains("result")); EXPECT_FALSE(response.contains("error")); EXPECT_TRUE(response["result"].contains("content")); EXPECT_TRUE(response["result"]["content"][0]["text"].get().find("Result: Test input") != std::string::npos); } // 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 mock_initialize(); // 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 mock_initialize(); // Call tool mcp::json request = { {"jsonrpc", "2.0"}, {"id", "call-1"}, {"method", "tools/call"}, {"params", { {"name", "test_tool"}, {"arguments", { {"input", "Test input"} }} }} }; mcp::json response = send_jsonrpc_request(request); // Verify response EXPECT_EQ(response["jsonrpc"], "2.0"); EXPECT_EQ(response["id"], "call-1"); EXPECT_TRUE(response.contains("result")); EXPECT_FALSE(response.contains("error")); // Verify tool call result EXPECT_TRUE(response["result"].contains("content")); EXPECT_TRUE(response["result"]["content"][0]["text"].get().find("Result: Test input") != std::string::npos); } // 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); // 状态码可能是200或202,取决于服务器实现 EXPECT_TRUE(res->status == 200 || res->status == 202); } // Test error handling - method not found TEST_F(DirectRequestTest, MethodNotFound) { mock_initialize(); 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 mock_initialize(); // 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 logging functionality TEST_F(DirectRequestTest, LoggingTest) { // Initialize first mock_initialize(); // 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 mock_initialize(); // 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 mock_initialize(); // 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)); } }