cpp-mcp/test/test_mcp_direct_requests.cpp

719 lines
21 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* @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>
// 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"}
}
};
}
}
// 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}}}
};
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<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();
}
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<mcp::server> server;
std::unique_ptr<httplib::Client> 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<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);
}
// 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<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
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<std::string>().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<int>(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<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
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<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
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<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
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<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));
}
}