cpp-mcp/test/test_mcp_direct_requests.cpp

819 lines
25 KiB
C++
Raw Normal View History

2025-03-09 15:45:09 +08:00
/**
* @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 <gtest/gtest.h>
#include <httplib.h>
#include <thread>
#include <chrono>
#include <filesystem>
// 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<mcp::server>("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();
2025-03-09 17:10:01 +08:00
server->register_tool(test_tool, [](const mcp::json& params) -> mcp::json {
2025-03-09 15:45:09 +08:00
std::string input = params.contains("input") ? params["input"].get<std::string>() : "Default input";
2025-03-09 17:10:01 +08:00
return {
{
{"type", "text"},
{"text", "Result: " + input}
}
};
2025-03-09 15:45:09 +08:00
});
// Register resources
auto test_resource = std::make_shared<mcp::api_resource>("Test Resource", "API resource for testing");
test_resource->register_handler("hello", [](const mcp::json& params) {
std::string name = params.contains("name") ? params["name"].get<std::string>() : "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<httplib::Client>("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<mcp::server> server;
std::unique_ptr<httplib::Client> 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<int>(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<int>(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<int>(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<int>(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<int>(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<int>(mcp::error_code::method_not_found) ||
error_code == static_cast<int>(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"}});
2025-03-09 17:10:01 +08:00
EXPECT_EQ(tool_result["content"][0]["text"], "Result: Client API call");
2025-03-09 15:45:09 +08:00
} 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<int>(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<int>(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<int>(mcp::error_code::invalid_request));
EXPECT_TRUE(response["error"]["message"].get<std::string>().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<int>(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<int>(mcp::error_code::invalid_request));
}
}