cpp-mcp/test/test_mcp_direct_requests.cpp

719 lines
21 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 <gtest/gtest.h>
#include <httplib.h>
#include <thread>
#include <chrono>
#include <filesystem>
2025-03-09 23:17:36 +08:00
// 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<std::string>()}
}
};
} else {
return {
{
{"type", "text"},
{"text", "Default result"}
}
};
}
}
2025-03-09 15:45:09 +08:00
// 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 = {
2025-03-09 23:17:36 +08:00
{"tools", {{"listChanged", true}}}
2025-03-09 15:45:09 +08:00
};
server->set_capabilities(capabilities);
2025-03-09 23:17:36 +08:00
2025-03-09 15:45:09 +08:00
// 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 23:17:36 +08:00
server->register_tool(test_tool, test_tool_handler);
2025-03-09 15:45:09 +08:00
// 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();
}
// 检查状态码202表示请求已接受但响应将通过SSE发送
if (res->status == 202) {
// 在实际测试中我们需要等待SSE响应
// 但在这个测试中,我们只是返回一个空对象
// 实际应用中应该使用客户端类来处理这种情况
std::cout << "收到202 Accepted响应实际响应将通过SSE发送" << std::endl;
return mcp::json::object();
}
2025-03-09 15:45:09 +08:00
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();
}
}
2025-03-09 23:17:36 +08:00
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"}
});
}
2025-03-09 15:45:09 +08:00
std::unique_ptr<mcp::server> server;
std::unique_ptr<httplib::Client> http_client;
std::thread server_thread;
};
2025-03-09 23:17:36 +08:00
// 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<int>(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<std::string>().find("Result: Test input") != std::string::npos);
}
2025-03-09 15:45:09 +08:00
// 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
2025-03-09 23:17:36 +08:00
mock_initialize();
2025-03-09 15:45:09 +08:00
// 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
2025-03-09 23:17:36 +08:00
mock_initialize();
2025-03-09 15:45:09 +08:00
// Call tool
mcp::json request = {
{"jsonrpc", "2.0"},
{"id", "call-1"},
2025-03-09 23:17:36 +08:00
{"method", "tools/call"},
2025-03-09 15:45:09 +08:00
{"params", {
{"name", "test_tool"},
2025-03-09 23:17:36 +08:00
{"arguments", {
2025-03-09 15:45:09 +08:00
{"input", "Test input"}
}}
}}
};
mcp::json response = send_jsonrpc_request(request);
// Verify response
EXPECT_EQ(response["jsonrpc"], "2.0");
EXPECT_EQ(response["id"], "call-1");
2025-03-09 23:17:36 +08:00
EXPECT_TRUE(response.contains("result"));
EXPECT_FALSE(response.contains("error"));
2025-03-09 15:45:09 +08:00
2025-03-09 23:17:36 +08:00
// Verify tool call result
EXPECT_TRUE(response["result"].contains("content"));
EXPECT_TRUE(response["result"]["content"][0]["text"].get<std::string>().find("Result: Test input") != std::string::npos);
2025-03-09 15:45:09 +08:00
}
// 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);
2025-03-09 15:45:09 +08:00
}
// Test error handling - method not found
TEST_F(DirectRequestTest, MethodNotFound) {
2025-03-09 23:17:36 +08:00
mock_initialize();
2025-03-09 15:45:09 +08:00
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
2025-03-09 23:17:36 +08:00
mock_initialize();
2025-03-09 15:45:09 +08:00
// 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 logging functionality
TEST_F(DirectRequestTest, LoggingTest) {
// Initialize first
2025-03-09 23:17:36 +08:00
mock_initialize();
2025-03-09 15:45:09 +08:00
// 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
2025-03-09 23:17:36 +08:00
mock_initialize();
2025-03-09 15:45:09 +08:00
// 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
2025-03-09 23:17:36 +08:00
mock_initialize();
2025-03-09 15:45:09 +08:00
// 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));
}
}