diff --git a/CMakeLists.txt b/CMakeLists.txt index f583980..6e6e114 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -151,6 +151,10 @@ if(FASTMCPP_BUILD_TESTS) target_link_libraries(fastmcpp_http_integration PRIVATE fastmcpp_core) add_test(NAME fastmcpp_http_integration COMMAND fastmcpp_http_integration) + add_executable(fastmcpp_http_auth tests/server/http_auth.cpp) + target_link_libraries(fastmcpp_http_auth PRIVATE fastmcpp_core) + add_test(NAME fastmcpp_http_auth COMMAND fastmcpp_http_auth) + add_executable(fastmcpp_json_schema tests/schema/json_schema.cpp) target_link_libraries(fastmcpp_json_schema PRIVATE fastmcpp_core) add_test(NAME fastmcpp_json_schema COMMAND fastmcpp_json_schema) @@ -229,9 +233,18 @@ if(FASTMCPP_BUILD_TESTS) target_link_libraries(fastmcpp_server_patterns PRIVATE fastmcpp_core) add_test(NAME fastmcpp_server_patterns COMMAND fastmcpp_server_patterns) - add_executable(fastmcpp_server_interactions tests/server/interactions.cpp) - target_link_libraries(fastmcpp_server_interactions PRIVATE fastmcpp_core) - add_test(NAME fastmcpp_server_interactions COMMAND fastmcpp_server_interactions) + # Server interactions split into 3 parts to avoid MSVC heap exhaustion + add_executable(fastmcpp_server_interactions_p1 tests/server/interactions_p1.cpp) + target_link_libraries(fastmcpp_server_interactions_p1 PRIVATE fastmcpp_core) + add_test(NAME fastmcpp_server_interactions_p1 COMMAND fastmcpp_server_interactions_p1) + + add_executable(fastmcpp_server_interactions_p2 tests/server/interactions_p2.cpp) + target_link_libraries(fastmcpp_server_interactions_p2 PRIVATE fastmcpp_core) + add_test(NAME fastmcpp_server_interactions_p2 COMMAND fastmcpp_server_interactions_p2) + + add_executable(fastmcpp_server_interactions_p3 tests/server/interactions_p3.cpp) + target_link_libraries(fastmcpp_server_interactions_p3 PRIVATE fastmcpp_core) + add_test(NAME fastmcpp_server_interactions_p3 COMMAND fastmcpp_server_interactions_p3) add_executable(fastmcpp_server_context_meta tests/server/context_meta.cpp) target_link_libraries(fastmcpp_server_context_meta PRIVATE fastmcpp_core) diff --git a/README.md b/README.md index a4e7603..8cf5669 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ fastmcpp is a C++ port of the Python [fastmcp](https://github.com/jlowin/fastmcp - Middleware for request/response processing. - Integration with MCP‑compatible CLI tools. - Cross‑platform: Windows, Linux, macOS. +- Server hardening knobs: optional auth tokens, CORS allowlist, payload/queue limits, and + per-connection SSE session binding. ## Requirements @@ -155,6 +157,10 @@ int main() { } ``` +To enable hardening on HTTP/SSE servers, provide the optional auth token, allowed origin, and +payload limit arguments. For SSE, POST requests must include the session_id provided by the initial +`event: endpoint` message; connections without the bearer token (when set) are rejected. + ### HTTP client ```cpp diff --git a/include/fastmcpp/server/http_server.hpp b/include/fastmcpp/server/http_server.hpp index 86b23f2..d0ace12 100644 --- a/include/fastmcpp/server/http_server.hpp +++ b/include/fastmcpp/server/http_server.hpp @@ -18,7 +18,8 @@ class HttpServerWrapper { public: HttpServerWrapper(std::shared_ptr core, std::string host = "127.0.0.1", - int port = 18080); + int port = 18080, std::string auth_token = "", + std::string allowed_origin = "", size_t payload_limit = 1024 * 1024); ~HttpServerWrapper(); bool start(); @@ -40,6 +41,9 @@ class HttpServerWrapper std::shared_ptr core_; std::string host_; int port_; + std::string auth_token_; + std::string allowed_origin_; + size_t payload_limit_; std::unique_ptr svr_; std::thread thread_; std::atomic running_{false}; diff --git a/src/server/http_server.cpp b/src/server/http_server.cpp index 6d7849d..4b4b72f 100644 --- a/src/server/http_server.cpp +++ b/src/server/http_server.cpp @@ -8,8 +8,12 @@ namespace fastmcpp::server { -HttpServerWrapper::HttpServerWrapper(std::shared_ptr core, std::string host, int port) - : core_(std::move(core)), host_(std::move(host)), port_(port) +HttpServerWrapper::HttpServerWrapper(std::shared_ptr core, std::string host, int port, + std::string auth_token, std::string allowed_origin, + size_t payload_limit) + : core_(std::move(core)), host_(std::move(host)), port_(port), + auth_token_(std::move(auth_token)), allowed_origin_(std::move(allowed_origin)), + payload_limit_(payload_limit) { } @@ -24,16 +28,36 @@ bool HttpServerWrapper::start() if (running_) return false; svr_ = std::make_unique(); + if (payload_limit_ > 0) + svr_->set_payload_max_length(payload_limit_); // Generic POST: / svr_->Post(R"(/(.*))", [this](const httplib::Request& req, httplib::Response& res) { + if (!auth_token_.empty()) + { + std::string auth = req.get_header_value("Authorization"); + if (auth.empty()) + auth = req.get_header_value("authorization"); + const std::string expected = "Bearer " + auth_token_; + if (auth != expected) + { + res.status = 401; + res.set_content("{\"error\":\"unauthorized\"}", "application/json"); + return; + } + } try { auto route = req.matches[1].str(); auto payload = fastmcpp::util::json::parse(req.body); auto out = core_->handle(route, payload); res.set_content(out.dump(), "application/json"); + if (!allowed_origin_.empty()) + { + res.set_header("Access-Control-Allow-Origin", allowed_origin_.c_str()); + res.set_header("Vary", "Origin"); + } res.status = 200; } catch (const fastmcpp::NotFoundError& e) diff --git a/tests/server/http_auth.cpp b/tests/server/http_auth.cpp new file mode 100644 index 0000000..a044d1e --- /dev/null +++ b/tests/server/http_auth.cpp @@ -0,0 +1,67 @@ +#include "fastmcpp/server/http_server.hpp" +#include "fastmcpp/server/server.hpp" +#include "fastmcpp/util/json.hpp" + +#include +#include +#include +#include +#include + +int main() +{ + using namespace fastmcpp; + auto core = std::make_shared(); + core->route("sum", [](const Json& j) { return j.at("a").get() + j.at("b").get(); }); + + const int port = 18082; + const std::string token = "secret-token"; + const std::string origin = "https://example.com"; + server::HttpServerWrapper http{core, "127.0.0.1", port, token, origin, + static_cast(1024 * 16)}; + if (!http.start()) + { + std::cerr << "failed to start HTTP server\n"; + return 1; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + httplib::Client cli("127.0.0.1", port); + + // Missing auth should be rejected + auto res = cli.Post("/sum", Json{{"a", 1}, {"b", 2}}.dump(), "application/json"); + if (!res || res->status != 401) + { + std::cerr << "expected 401 for missing auth (got " + << (res ? std::to_string(res->status) : std::string("no response")) << ")\n"; + http.stop(); + return 1; + } + + // Authorized request should succeed and include CORS header + httplib::Headers headers = {{"Authorization", std::string("Bearer ") + token}}; + res = cli.Post("/sum", headers, Json{{"a", 5}, {"b", 7}}.dump(), "application/json"); + if (!res || res->status != 200) + { + std::cerr << "expected 200 for authorized request\n"; + http.stop(); + return 1; + } + auto out = Json::parse(res->body); + if (out.get() != 12) + { + std::cerr << "unexpected sum result\n"; + http.stop(); + return 1; + } + auto cors = res->get_header_value("Access-Control-Allow-Origin"); + if (cors != origin) + { + std::cerr << "missing/invalid CORS header\n"; + http.stop(); + return 1; + } + + http.stop(); + return 0; +} diff --git a/tests/server/interactions.cpp b/tests/server/interactions.cpp index f9b6cde..2c22929 100644 --- a/tests/server/interactions.cpp +++ b/tests/server/interactions.cpp @@ -7,6 +7,7 @@ #include "fastmcpp/server/server.hpp" #include "fastmcpp/tools/manager.hpp" #include "fastmcpp/tools/tool.hpp" +#include "interactions_fixture.hpp" #include #include @@ -19,116 +20,8 @@ using namespace fastmcpp; // Test Server Fixture - creates a server with multiple tools // ============================================================================ -std::shared_ptr create_interaction_server() -{ - auto srv = std::make_shared(); - - // Tool: add - basic arithmetic - srv->route( - "tools/list", - [](const Json&) - { - Json tools = Json::array(); - - tools.push_back( - Json{{"name", "add"}, - {"description", "Add two numbers"}, - {"inputSchema", Json{{"type", "object"}, - {"properties", Json{{"x", {{"type", "integer"}}}, - {"y", {{"type", "integer"}}}}}, - {"required", Json::array({"x", "y"})}}}}); - - tools.push_back( - Json{{"name", "greet"}, - {"description", "Greet a person"}, - {"inputSchema", Json{{"type", "object"}, - {"properties", Json{{"name", {{"type", "string"}}}}}, - {"required", Json::array({"name"})}}}}); - - tools.push_back(Json{{"name", "error_tool"}, - {"description", "Always fails"}, - {"inputSchema", Json{{"type", "object"}}}}); - - tools.push_back(Json{{"name", "list_tool"}, - {"description", "Returns a list"}, - {"inputSchema", Json{{"type", "object"}}}}); - - tools.push_back(Json{{"name", "nested_tool"}, - {"description", "Returns nested data"}, - {"inputSchema", Json{{"type", "object"}}}}); - - tools.push_back(Json{ - {"name", "optional_params"}, - {"description", "Has optional params"}, - {"inputSchema", - Json{{"type", "object"}, - {"properties", Json{{"required_param", {{"type", "string"}}}, - {"optional_param", - {{"type", "string"}, {"default", "default_value"}}}}}, - {"required", Json::array({"required_param"})}}}}); - - return Json{{"tools", tools}}; - }); +// create_interaction_server moved to interactions_fixture.hpp - srv->route( - "tools/call", - [](const Json& in) - { - std::string name = in.at("name").get(); - Json args = in.value("arguments", Json::object()); - - if (name == "add") - { - int x = args.at("x").get(); - int y = args.at("y").get(); - int result = x + y; - return Json{{"content", Json::array({Json{{"type", "text"}, - {"text", std::to_string(result)}}})}, - {"structuredContent", Json{{"result", result}}}, - {"isError", false}}; - } - if (name == "greet") - { - std::string greeting = "Hello, " + args.at("name").get() + "!"; - return Json{{"content", Json::array({Json{{"type", "text"}, {"text", greeting}}})}, - {"isError", false}}; - } - if (name == "error_tool") - { - return Json{ - {"content", Json::array({Json{{"type", "text"}, {"text", "Test error"}}})}, - {"isError", true}}; - } - if (name == "list_tool") - { - return Json{ - {"content", Json::array({Json{{"type", "text"}, {"text", "[\"x\",2]"}}})}, - {"structuredContent", Json{{"result", Json::array({"x", 2})}}}, - {"isError", false}}; - } - if (name == "nested_tool") - { - Json nested = {{"level1", {{"level2", {{"value", 42}}}}}}; - return Json{ - {"content", Json::array({Json{{"type", "text"}, {"text", nested.dump()}}})}, - {"structuredContent", Json{{"result", nested}}}, - {"isError", false}}; - } - if (name == "optional_params") - { - std::string req = args.at("required_param").get(); - std::string opt = args.value("optional_param", "default_value"); - return Json{ - {"content", Json::array({Json{{"type", "text"}, {"text", req + ":" + opt}}})}, - {"isError", false}}; - } - return Json{ - {"content", Json::array({Json{{"type", "text"}, {"text", "Unknown tool"}}})}, - {"isError", true}}; - }); - - return srv; -} // ============================================================================ // TestTools - Basic tool operations diff --git a/tests/server/interactions_fixture.hpp b/tests/server/interactions_fixture.hpp new file mode 100644 index 0000000..1c7c75e --- /dev/null +++ b/tests/server/interactions_fixture.hpp @@ -0,0 +1,120 @@ +#pragma once +#include "fastmcpp/server/server.hpp" +#include "fastmcpp/tools/manager.hpp" +#include "fastmcpp/tools/tool.hpp" +#include "fastmcpp/util/json.hpp" + +namespace fastmcpp { + +inline std::shared_ptr create_interaction_server() +{ + auto srv = std::make_shared(); + + // Tool: add - basic arithmetic + srv->route( + "tools/list", + [](const Json&) + { + Json tools = Json::array(); + + tools.push_back( + Json{{"name", "add"}, + {"description", "Add two numbers"}, + {"inputSchema", Json{{"type", "object"}, + {"properties", Json{{"x", {{"type", "integer"}}}, + {"y", {{"type", "integer"}}}}}, + {"required", Json::array({"x", "y"})}}}}); + + tools.push_back( + Json{{"name", "greet"}, + {"description", "Greet a person"}, + {"inputSchema", Json{{"type", "object"}, + {"properties", Json{{"name", {{"type", "string"}}}}}, + {"required", Json::array({"name"})}}}}); + + tools.push_back(Json{{"name", "error_tool"}, + {"description", "Always fails"}, + {"inputSchema", Json{{"type", "object"}}}}); + + tools.push_back(Json{{"name", "list_tool"}, + {"description", "Returns a list"}, + {"inputSchema", Json{{"type", "object"}}}}); + + tools.push_back(Json{{"name", "nested_tool"}, + {"description", "Returns nested data"}, + {"inputSchema", Json{{"type", "object"}}}}); + + tools.push_back(Json{ + {"name", "optional_params"}, + {"description", "Has optional params"}, + {"inputSchema", + Json{{"type", "object"}, + {"properties", Json{{"required_param", {{"type", "string"}}}, + {"optional_param", + {{"type", "string"}, {"default", "default_value"}}}}}, + {"required", Json::array({"required_param"})}}}}); + + return Json{{"tools", tools}}; + }); + + srv->route( + "tools/call", + [](const Json& in) + { + std::string name = in.at("name").get(); + Json args = in.value("arguments", Json::object()); + + if (name == "add") + { + int x = args.at("x").get(); + int y = args.at("y").get(); + int result = x + y; + return Json{{"content", Json::array({Json{{"type", "text"}, + {"text", std::to_string(result)}}})}, + {"structuredContent", Json{{"result", result}}}, + {"isError", false}}; + } + if (name == "greet") + { + std::string greeting = "Hello, " + args.at("name").get() + "!"; + return Json{{"content", Json::array({Json{{"type", "text"}, {"text", greeting}}})}, + {"isError", false}}; + } + if (name == "error_tool") + { + return Json{ + {"content", Json::array({Json{{"type", "text"}, {"text", "Test error"}}})}, + {"isError", true}}; + } + if (name == "list_tool") + { + return Json{ + {"content", Json::array({Json{{"type", "text"}, {"text", "[\"x\",2]"}}})}, + {"structuredContent", Json{{"result", Json::array({"x", 2})}}}, + {"isError", false}}; + } + if (name == "nested_tool") + { + Json nested = {{"level1", {{"level2", {{"value", 42}}}}}}; + return Json{ + {"content", Json::array({Json{{"type", "text"}, {"text", nested.dump()}}})}, + {"structuredContent", Json{{"result", nested}}}, + {"isError", false}}; + } + if (name == "optional_params") + { + std::string req = args.at("required_param").get(); + std::string opt = args.value("optional_param", "default_value"); + return Json{ + {"content", Json::array({Json{{"type", "text"}, {"text", req + ":" + opt}}})}, + {"isError", false}}; + } + return Json{ + {"content", Json::array({Json{{"type", "text"}, {"text", "Unknown tool"}}})}, + {"isError", true}}; + }); + + return srv; +} + +} // namespace fastmcpp diff --git a/tests/server/interactions_p1.cpp b/tests/server/interactions_p1.cpp new file mode 100644 index 0000000..e1ef616 --- /dev/null +++ b/tests/server/interactions_p1.cpp @@ -0,0 +1,1763 @@ +/// @file tests/server/interactions_p1.cpp +/// @brief Server interaction tests - Part 1 (52 tests) +#include "fastmcpp/client/client.hpp" +#include "fastmcpp/client/transports.hpp" +#include "fastmcpp/server/server.hpp" +#include "fastmcpp/tools/manager.hpp" +#include "fastmcpp/tools/tool.hpp" +#include "interactions_fixture.hpp" + +#include +#include +#include +#include + +using namespace fastmcpp; + +// ============================================================================ +// Test Server Fixture - creates a server with multiple tools +// ============================================================================ + +// create_interaction_server moved to interactions_fixture.hpp + + +// ============================================================================ +// TestTools - Basic tool operations +// ============================================================================ + +void test_tool_exists() +{ + std::cout << "Test: tool exists after registration...\n"; + + auto srv = create_interaction_server(); + client::Client c(std::make_unique(srv)); + + auto tools = c.list_tools(); + bool found = false; + for (const auto& t : tools) + { + if (t.name == "add") + { + found = true; + break; + } + } + assert(found); + + std::cout << " [PASS] Tool 'add' exists\n"; +} + +void test_list_tools_count() +{ + std::cout << "Test: list_tools returns correct count...\n"; + + auto srv = create_interaction_server(); + client::Client c(std::make_unique(srv)); + + auto tools = c.list_tools(); + assert(tools.size() == 6); + + std::cout << " [PASS] list_tools() returns 6 tools\n"; +} + +void test_call_tool_basic() +{ + std::cout << "Test: call_tool basic arithmetic...\n"; + + auto srv = create_interaction_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("add", {{"x", 1}, {"y", 2}}); + assert(!result.isError); + assert(result.content.size() == 1); + + auto* text = std::get_if(&result.content[0]); + assert(text != nullptr); + assert(text->text == "3"); + + std::cout << " [PASS] call_tool('add', {x:1, y:2}) = 3\n"; +} + +void test_call_tool_structured_content() +{ + std::cout << "Test: call_tool returns structuredContent...\n"; + + auto srv = create_interaction_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("add", {{"x", 10}, {"y", 20}}); + assert(!result.isError); + assert(result.structuredContent.has_value()); + assert((*result.structuredContent)["result"] == 30); + + std::cout << " [PASS] structuredContent has result=30\n"; +} + +void test_call_tool_error() +{ + std::cout << "Test: call_tool error handling...\n"; + + auto srv = create_interaction_server(); + client::Client c(std::make_unique(srv)); + + bool threw = false; + try + { + c.call_tool("error_tool", Json::object()); + } + catch (const fastmcpp::Error&) + { + threw = true; + } + assert(threw); + + std::cout << " [PASS] error_tool throws exception\n"; +} + +void test_call_tool_list_return() +{ + std::cout << "Test: call_tool with list return type...\n"; + + auto srv = create_interaction_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("list_tool", Json::object()); + assert(!result.isError); + assert(result.structuredContent.has_value()); + + auto data = (*result.structuredContent)["result"]; + assert(data.is_array()); + assert(data.size() == 2); + assert(data[0] == "x"); + assert(data[1] == 2); + + std::cout << " [PASS] list_tool returns [\"x\", 2]\n"; +} + +void test_call_tool_nested_return() +{ + std::cout << "Test: call_tool with nested return type...\n"; + + auto srv = create_interaction_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("nested_tool", Json::object()); + assert(!result.isError); + assert(result.structuredContent.has_value()); + + auto data = (*result.structuredContent)["result"]; + assert(data["level1"]["level2"]["value"] == 42); + + std::cout << " [PASS] nested_tool returns nested structure\n"; +} + +void test_call_tool_optional_params() +{ + std::cout << "Test: call_tool with optional parameters...\n"; + + auto srv = create_interaction_server(); + client::Client c(std::make_unique(srv)); + + // With only required param + auto result1 = c.call_tool("optional_params", {{"required_param", "hello"}}); + assert(!result1.isError); + auto* text1 = std::get_if(&result1.content[0]); + assert(text1 && text1->text == "hello:default_value"); + + // With both params + auto result2 = + c.call_tool("optional_params", {{"required_param", "hello"}, {"optional_param", "world"}}); + assert(!result2.isError); + auto* text2 = std::get_if(&result2.content[0]); + assert(text2 && text2->text == "hello:world"); + + std::cout << " [PASS] optional parameters handled correctly\n"; +} + +// ============================================================================ +// TestToolParameters - Parameter validation +// ============================================================================ + +void test_tool_input_schema_present() +{ + std::cout << "Test: tool inputSchema is present...\n"; + + auto srv = create_interaction_server(); + client::Client c(std::make_unique(srv)); + + auto tools = c.list_tools(); + for (const auto& t : tools) + { + if (t.name == "add") + { + assert(t.inputSchema.contains("properties")); + assert(t.inputSchema["properties"].contains("x")); + assert(t.inputSchema["properties"].contains("y")); + break; + } + } + + std::cout << " [PASS] inputSchema has properties\n"; +} + +void test_tool_required_params() +{ + std::cout << "Test: tool required params in schema...\n"; + + auto srv = create_interaction_server(); + client::Client c(std::make_unique(srv)); + + auto tools = c.list_tools(); + for (const auto& t : tools) + { + if (t.name == "optional_params") + { + assert(t.inputSchema.contains("required")); + auto required = t.inputSchema["required"]; + assert(required.size() == 1); + assert(required[0] == "required_param"); + break; + } + } + + std::cout << " [PASS] required params correctly specified\n"; +} + +void test_tool_default_values() +{ + std::cout << "Test: tool default values in schema...\n"; + + auto srv = create_interaction_server(); + client::Client c(std::make_unique(srv)); + + auto tools = c.list_tools(); + for (const auto& t : tools) + { + if (t.name == "optional_params") + { + auto props = t.inputSchema["properties"]; + assert(props["optional_param"].contains("default")); + assert(props["optional_param"]["default"] == "default_value"); + break; + } + } + + std::cout << " [PASS] default values in schema\n"; +} + +// ============================================================================ +// TestMultipleCallSequence - Sequential operations +// ============================================================================ + +void test_multiple_tool_calls() +{ + std::cout << "Test: multiple sequential tool calls...\n"; + + auto srv = create_interaction_server(); + client::Client c(std::make_unique(srv)); + + // Make multiple calls + auto r1 = c.call_tool("add", {{"x", 1}, {"y", 1}}); + auto r2 = c.call_tool("add", {{"x", 2}, {"y", 2}}); + auto r3 = c.call_tool("add", {{"x", 3}, {"y", 3}}); + + assert((*r1.structuredContent)["result"] == 2); + assert((*r2.structuredContent)["result"] == 4); + assert((*r3.structuredContent)["result"] == 6); + + std::cout << " [PASS] multiple calls work correctly\n"; +} + +void test_interleaved_operations() +{ + std::cout << "Test: interleaved tool and list operations...\n"; + + auto srv = create_interaction_server(); + client::Client c(std::make_unique(srv)); + + auto tools1 = c.list_tools(); + auto r1 = c.call_tool("add", {{"x", 5}, {"y", 5}}); + auto tools2 = c.list_tools(); + auto r2 = c.call_tool("greet", {{"name", "World"}}); + + assert(tools1.size() == tools2.size()); + assert((*r1.structuredContent)["result"] == 10); + auto* text = std::get_if(&r2.content[0]); + assert(text && text->text == "Hello, World!"); + + std::cout << " [PASS] interleaved operations work correctly\n"; +} + +// ============================================================================ +// Resource Server Fixture +// ============================================================================ + +std::shared_ptr create_resource_interaction_server() +{ + auto srv = std::make_shared(); + + srv->route("resources/list", + [](const Json&) + { + return Json{{"resources", + Json::array({Json{{"uri", "file:///config.json"}, + {"name", "config.json"}, + {"mimeType", "application/json"}, + {"description", "Configuration file"}}, + Json{{"uri", "file:///readme.md"}, + {"name", "readme.md"}, + {"mimeType", "text/markdown"}, + {"description", "README documentation"}}, + Json{{"uri", "mem:///cache"}, + {"name", "cache"}, + {"mimeType", "application/octet-stream"}}})}}; + }); + + srv->route( + "resources/read", + [](const Json& in) + { + std::string uri = in.at("uri").get(); + if (uri == "file:///config.json") + { + return Json{{"contents", Json::array({Json{{"uri", uri}, + {"mimeType", "application/json"}, + {"text", "{\"key\": \"value\"}"}}})}}; + } + if (uri == "file:///readme.md") + { + return Json{{"contents", Json::array({Json{{"uri", uri}, + {"mimeType", "text/markdown"}, + {"text", "# Hello World"}}})}}; + } + if (uri == "mem:///cache") + { + return Json{{"contents", Json::array({Json{{"uri", uri}, + {"mimeType", "application/octet-stream"}, + {"blob", "YmluYXJ5ZGF0YQ=="}}})}}; + } + return Json{{"contents", Json::array()}}; + }); + + srv->route("resources/templates/list", + [](const Json&) + { + return Json{{"resourceTemplates", + Json::array({Json{{"uriTemplate", "file:///{path}"}, + {"name", "file"}, + {"description", "File access"}}, + Json{{"uriTemplate", "db:///{table}/{id}"}, + {"name", "database"}, + {"description", "Database record"}}})}}; + }); + + return srv; +} + +// ============================================================================ +// TestResource - Basic resource operations +// ============================================================================ + +void test_list_resources() +{ + std::cout << "Test: list_resources returns resources...\n"; + + auto srv = create_resource_interaction_server(); + client::Client c(std::make_unique(srv)); + + auto resources = c.list_resources(); + assert(resources.size() == 3); + assert(resources[0].uri == "file:///config.json"); + assert(resources[0].name == "config.json"); + + std::cout << " [PASS] list_resources() returns 3 resources\n"; +} + +void test_read_resource_text() +{ + std::cout << "Test: read_resource returns text content...\n"; + + auto srv = create_resource_interaction_server(); + client::Client c(std::make_unique(srv)); + + auto contents = c.read_resource("file:///config.json"); + assert(contents.size() == 1); + + auto* text = std::get_if(&contents[0]); + assert(text != nullptr); + assert(text->text == "{\"key\": \"value\"}"); + + std::cout << " [PASS] read_resource returns text\n"; +} + +void test_read_resource_blob() +{ + std::cout << "Test: read_resource returns blob content...\n"; + + auto srv = create_resource_interaction_server(); + client::Client c(std::make_unique(srv)); + + auto contents = c.read_resource("mem:///cache"); + assert(contents.size() == 1); + + auto* blob = std::get_if(&contents[0]); + assert(blob != nullptr); + assert(blob->blob == "YmluYXJ5ZGF0YQ=="); + + std::cout << " [PASS] read_resource returns blob\n"; +} + +void test_list_resource_templates() +{ + std::cout << "Test: list_resource_templates returns templates...\n"; + + auto srv = create_resource_interaction_server(); + client::Client c(std::make_unique(srv)); + + auto templates = c.list_resource_templates(); + assert(templates.size() == 2); + assert(templates[0].uriTemplate == "file:///{path}"); + assert(templates[1].uriTemplate == "db:///{table}/{id}"); + + std::cout << " [PASS] list_resource_templates() returns 2 templates\n"; +} + +void test_resource_with_description() +{ + std::cout << "Test: resource has description...\n"; + + auto srv = create_resource_interaction_server(); + client::Client c(std::make_unique(srv)); + + auto resources = c.list_resources(); + bool found = false; + for (const auto& r : resources) + { + if (r.uri == "file:///config.json") + { + assert(r.description.has_value()); + assert(*r.description == "Configuration file"); + found = true; + break; + } + } + assert(found); + + std::cout << " [PASS] resource description present\n"; +} + +// ============================================================================ +// Prompt Server Fixture +// ============================================================================ + +std::shared_ptr create_prompt_interaction_server() +{ + auto srv = std::make_shared(); + + srv->route( + "prompts/list", + [](const Json&) + { + return Json{ + {"prompts", + Json::array( + {Json{{"name", "greeting"}, + {"description", "Generate a greeting"}, + {"arguments", Json::array({Json{{"name", "name"}, + {"description", "Name to greet"}, + {"required", true}}, + Json{{"name", "style"}, + {"description", "Greeting style"}, + {"required", false}}})}}, + Json{{"name", "summarize"}, + {"description", "Summarize text"}, + {"arguments", Json::array({Json{{"name", "text"}, + {"description", "Text to summarize"}, + {"required", true}}, + Json{{"name", "length"}, + {"description", "Max length"}, + {"required", false}}})}}, + Json{{"name", "simple"}, {"description", "Simple prompt with no args"}}})}}; + }); + + srv->route( + "prompts/get", + [](const Json& in) + { + std::string name = in.at("name").get(); + Json args = in.value("arguments", Json::object()); + + if (name == "greeting") + { + std::string greet_name = args.value("name", "World"); + std::string style = args.value("style", "formal"); + std::string message = (style == "casual") ? "Hey " + greet_name + "!" + : "Good day, " + greet_name + "."; + return Json{ + {"description", "A personalized greeting"}, + {"messages", + Json::array({Json{{"role", "user"}, + {"content", Json{{"type", "text"}, {"text", message}}}}})}}; + } + if (name == "summarize") + { + return Json{ + {"description", "Summarize the following"}, + {"messages", + Json::array({Json{{"role", "user"}, + {"content", Json{{"type", "text"}, + {"text", "Please summarize: " + + args.value("text", "")}}}}})}}; + } + if (name == "simple") + { + return Json{ + {"description", "A simple prompt"}, + {"messages", + Json::array({Json{{"role", "user"}, + {"content", Json{{"type", "text"}, + {"text", "Hello from simple prompt"}}}}})}}; + } + return Json{{"messages", Json::array()}}; + }); + + return srv; +} + +// ============================================================================ +// TestPrompts - Prompt operations +// ============================================================================ + +void test_list_prompts() +{ + std::cout << "Test: list_prompts returns prompts...\n"; + + auto srv = create_prompt_interaction_server(); + client::Client c(std::make_unique(srv)); + + auto prompts = c.list_prompts(); + assert(prompts.size() == 3); + assert(prompts[0].name == "greeting"); + assert(prompts[1].name == "summarize"); + assert(prompts[2].name == "simple"); + + std::cout << " [PASS] list_prompts() returns 3 prompts\n"; +} + +void test_prompt_has_arguments() +{ + std::cout << "Test: prompt has arguments...\n"; + + auto srv = create_prompt_interaction_server(); + client::Client c(std::make_unique(srv)); + + auto prompts = c.list_prompts(); + for (const auto& p : prompts) + { + if (p.name == "greeting") + { + assert(p.arguments.has_value()); + assert(p.arguments->size() == 2); + assert((*p.arguments)[0].name == "name"); + assert((*p.arguments)[0].required == true); + assert((*p.arguments)[1].name == "style"); + assert((*p.arguments)[1].required == false); + break; + } + } + + std::cout << " [PASS] prompt arguments present\n"; +} + +void test_get_prompt_basic() +{ + std::cout << "Test: get_prompt returns messages...\n"; + + auto srv = create_prompt_interaction_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.get_prompt("simple", Json::object()); + assert(result.messages.size() == 1); + assert(result.messages[0].role == client::Role::User); + + std::cout << " [PASS] get_prompt returns messages\n"; +} + +void test_get_prompt_with_args() +{ + std::cout << "Test: get_prompt with arguments...\n"; + + auto srv = create_prompt_interaction_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.get_prompt("greeting", {{"name", "Alice"}, {"style", "casual"}}); + assert(result.messages.size() == 1); + assert(result.description.has_value()); + + std::cout << " [PASS] get_prompt with args works\n"; +} + +void test_prompt_no_args() +{ + std::cout << "Test: prompt with no arguments defined...\n"; + + auto srv = create_prompt_interaction_server(); + client::Client c(std::make_unique(srv)); + + auto prompts = c.list_prompts(); + for (const auto& p : prompts) + { + if (p.name == "simple") + { + // simple prompt has no arguments array + assert(!p.arguments.has_value() || p.arguments->empty()); + break; + } + } + + std::cout << " [PASS] prompt without args handled\n"; +} + +// ============================================================================ +// Meta Server Fixture - tests meta field handling +// ============================================================================ + +std::shared_ptr create_meta_server() +{ + auto srv = std::make_shared(); + + srv->route("tools/list", + [](const Json&) + { + return Json{ + {"tools", Json::array({Json{{"name", "meta_tool"}, + {"description", "Tool with meta"}, + {"inputSchema", Json{{"type", "object"}}}, + {"_meta", Json{{"custom_field", "custom_value"}, + {"version", 2}}}}, + Json{{"name", "no_meta_tool"}, + {"description", "Tool without meta"}, + {"inputSchema", Json{{"type", "object"}}}}})}}; + }); + + srv->route("tools/call", + [](const Json& in) + { + std::string name = in.at("name").get(); + Json response = { + {"content", Json::array({Json{{"type", "text"}, {"text", "result"}}})}, + {"isError", false}}; + // Echo back meta if present + if (in.contains("_meta")) + response["_meta"] = in["_meta"]; + return response; + }); + + srv->route("resources/list", + [](const Json&) + { + return Json{ + {"resources", + Json::array({Json{{"uri", "test://resource"}, + {"name", "test"}, + {"_meta", Json{{"source", "test"}, {"priority", 1}}}}})}}; + }); + + srv->route("prompts/list", + [](const Json&) + { + return Json{ + {"prompts", Json::array({Json{{"name", "meta_prompt"}, + {"description", "Prompt with meta"}, + {"_meta", Json{{"category", "greeting"}}}}})}}; + }); + + return srv; +} + +// ============================================================================ +// TestMeta - Meta field handling +// ============================================================================ + +void test_tool_meta_present() +{ + std::cout << "Test: tool has _meta field...\n"; + + auto srv = create_meta_server(); + client::Client c(std::make_unique(srv)); + + auto tools = c.list_tools(); + bool found = false; + for (const auto& t : tools) + { + if (t.name == "meta_tool") + { + // Note: meta field handling depends on client implementation + found = true; + break; + } + } + assert(found); + + std::cout << " [PASS] tool with meta found\n"; +} + +void test_call_tool_with_meta() +{ + std::cout << "Test: call_tool with meta echoes it back...\n"; + + auto srv = create_meta_server(); + client::Client c(std::make_unique(srv)); + + Json meta = {{"request_id", "abc-123"}, {"trace", true}}; + auto result = c.call_tool("meta_tool", Json::object(), meta); + + assert(!result.isError); + assert(result.meta.has_value()); + assert((*result.meta)["request_id"] == "abc-123"); + assert((*result.meta)["trace"] == true); + + std::cout << " [PASS] meta echoed back correctly\n"; +} + +void test_call_tool_without_meta() +{ + std::cout << "Test: call_tool without meta works...\n"; + + auto srv = create_meta_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("no_meta_tool", Json::object()); + assert(!result.isError); + + std::cout << " [PASS] call without meta works\n"; +} + +// ============================================================================ +// Output Schema Server Fixture +// ============================================================================ + +std::shared_ptr create_output_schema_server() +{ + auto srv = std::make_shared(); + + srv->route( + "tools/list", + [](const Json&) + { + return Json{ + {"tools", + Json::array({Json{{"name", "typed_result"}, + {"description", "Returns typed result"}, + {"inputSchema", Json{{"type", "object"}}}, + {"outputSchema", + Json{{"type", "object"}, + {"properties", Json{{"value", {{"type", "integer"}}}, + {"label", {{"type", "string"}}}}}, + {"required", Json::array({"value"})}}}}, + Json{{"name", "array_result"}, + {"description", "Returns array"}, + {"inputSchema", Json{{"type", "object"}}}, + {"outputSchema", + Json{{"type", "array"}, {"items", {{"type", "string"}}}}}}, + Json{{"name", "no_schema"}, + {"description", "No output schema"}, + {"inputSchema", Json{{"type", "object"}}}}})}}; + }); + + srv->route( + "tools/call", + [](const Json& in) + { + std::string name = in.at("name").get(); + + if (name == "typed_result") + { + return Json{{"content", Json::array({Json{{"type", "text"}, {"text", "42"}}})}, + {"structuredContent", Json{{"value", 42}, {"label", "answer"}}}, + {"isError", false}}; + } + if (name == "array_result") + { + return Json{{"content", Json::array({Json{{"type", "text"}, + {"text", "[\"a\",\"b\",\"c\"]"}}})}, + {"structuredContent", Json::array({"a", "b", "c"})}, + {"isError", false}}; + } + if (name == "no_schema") + { + return Json{{"content", Json::array({Json{{"type", "text"}, {"text", "plain"}}})}, + {"isError", false}}; + } + return Json{{"content", Json::array()}, {"isError", true}}; + }); + + return srv; +} + +// ============================================================================ +// TestOutputSchema - Output schema handling +// ============================================================================ + +void test_tool_has_output_schema() +{ + std::cout << "Test: tool has outputSchema...\n"; + + auto srv = create_output_schema_server(); + client::Client c(std::make_unique(srv)); + + auto tools = c.list_tools(); + bool found = false; + for (const auto& t : tools) + { + if (t.name == "typed_result") + { + assert(t.outputSchema.has_value()); + assert((*t.outputSchema)["type"] == "object"); + assert((*t.outputSchema)["properties"].contains("value")); + found = true; + break; + } + } + assert(found); + + std::cout << " [PASS] outputSchema present\n"; +} + +void test_structured_content_object() +{ + std::cout << "Test: structuredContent with object...\n"; + + auto srv = create_output_schema_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("typed_result", Json::object()); + assert(!result.isError); + assert(result.structuredContent.has_value()); + assert((*result.structuredContent)["value"] == 42); + assert((*result.structuredContent)["label"] == "answer"); + + std::cout << " [PASS] object structuredContent works\n"; +} + +void test_structured_content_array() +{ + std::cout << "Test: structuredContent with array...\n"; + + auto srv = create_output_schema_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("array_result", Json::object()); + assert(!result.isError); + assert(result.structuredContent.has_value()); + assert(result.structuredContent->is_array()); + assert(result.structuredContent->size() == 3); + assert((*result.structuredContent)[0] == "a"); + + std::cout << " [PASS] array structuredContent works\n"; +} + +void test_tool_without_output_schema() +{ + std::cout << "Test: tool without outputSchema...\n"; + + auto srv = create_output_schema_server(); + client::Client c(std::make_unique(srv)); + + auto tools = c.list_tools(); + for (const auto& t : tools) + { + if (t.name == "no_schema") + { + assert(!t.outputSchema.has_value()); + break; + } + } + + auto result = c.call_tool("no_schema", Json::object()); + assert(!result.isError); + assert(!result.structuredContent.has_value()); + + std::cout << " [PASS] tool without schema works\n"; +} + +// ============================================================================ +// TestContentTypes - Various content types +// ============================================================================ + +std::shared_ptr create_content_type_server() +{ + auto srv = std::make_shared(); + + srv->route( + "tools/list", + [](const Json&) + { + return Json{ + {"tools", + Json::array( + {Json{{"name", "text_content"}, {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "multi_content"}, {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "embedded_resource"}, + {"inputSchema", Json{{"type", "object"}}}}})}}; + }); + + srv->route( + "tools/call", + [](const Json& in) + { + std::string name = in.at("name").get(); + + if (name == "text_content") + { + return Json{ + {"content", Json::array({Json{{"type", "text"}, {"text", "Hello, World!"}}})}, + {"isError", false}}; + } + if (name == "multi_content") + { + return Json{{"content", Json::array({Json{{"type", "text"}, {"text", "First"}}, + Json{{"type", "text"}, {"text", "Second"}}, + Json{{"type", "text"}, {"text", "Third"}}})}, + {"isError", false}}; + } + if (name == "embedded_resource") + { + return Json{ + {"content", Json::array({Json{{"type", "text"}, {"text", "Before resource"}}, + Json{{"type", "resource"}, + {"uri", "file:///data.txt"}, + {"mimeType", "text/plain"}, + {"text", "Resource content"}}})}, + {"isError", false}}; + } + return Json{{"content", Json::array()}, {"isError", true}}; + }); + + return srv; +} + +void test_single_text_content() +{ + std::cout << "Test: single text content...\n"; + + auto srv = create_content_type_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("text_content", Json::object()); + assert(!result.isError); + assert(result.content.size() == 1); + + auto* text = std::get_if(&result.content[0]); + assert(text != nullptr); + assert(text->text == "Hello, World!"); + + std::cout << " [PASS] single text content works\n"; +} + +void test_multiple_text_content() +{ + std::cout << "Test: multiple text content items...\n"; + + auto srv = create_content_type_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("multi_content", Json::object()); + assert(!result.isError); + assert(result.content.size() == 3); + + auto* t1 = std::get_if(&result.content[0]); + auto* t2 = std::get_if(&result.content[1]); + auto* t3 = std::get_if(&result.content[2]); + + assert(t1 && t1->text == "First"); + assert(t2 && t2->text == "Second"); + assert(t3 && t3->text == "Third"); + + std::cout << " [PASS] multiple content items work\n"; +} + +void test_mixed_content_types() +{ + std::cout << "Test: mixed content types...\n"; + + auto srv = create_content_type_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("embedded_resource", Json::object()); + assert(!result.isError); + assert(result.content.size() == 2); + + auto* text = std::get_if(&result.content[0]); + assert(text && text->text == "Before resource"); + + auto* resource = std::get_if(&result.content[1]); + assert(resource != nullptr); + assert(resource->text == "Resource content"); + + std::cout << " [PASS] mixed content types work\n"; +} + +// ============================================================================ +// Error Handling Tests +// ============================================================================ + +std::shared_ptr create_error_server() +{ + auto srv = std::make_shared(); + + srv->route( + "tools/list", + [](const Json&) + { + return Json{ + {"tools", + Json::array( + {Json{{"name", "throws_error"}, {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "returns_error"}, {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "missing_tool"}, {"inputSchema", Json{{"type", "object"}}}}})}}; + }); + + srv->route( + "tools/call", + [](const Json& in) + { + std::string name = in.at("name").get(); + + if (name == "throws_error") + throw std::runtime_error("Tool execution failed"); + if (name == "returns_error") + { + return Json{ + {"content", Json::array({Json{{"type", "text"}, {"text", "Error occurred"}}})}, + {"isError", true}}; + } + // Any unknown tool returns an error + return Json{{"content", Json::array({Json{{"type", "text"}, + {"text", "Tool not found: " + name}}})}, + {"isError", true}}; + }); + + return srv; +} + +void test_tool_returns_error_flag() +{ + std::cout << "Test: tool returns isError=true...\n"; + + auto srv = create_error_server(); + client::Client c(std::make_unique(srv)); + + bool threw = false; + try + { + c.call_tool("returns_error", Json::object()); + } + catch (const fastmcpp::Error&) + { + threw = true; + } + assert(threw); + + std::cout << " [PASS] isError=true throws exception\n"; +} + +void test_tool_call_nonexistent() +{ + std::cout << "Test: calling nonexistent tool...\n"; + + auto srv = create_error_server(); + client::Client c(std::make_unique(srv)); + + bool threw = false; + try + { + c.call_tool("nonexistent_tool_xyz", Json::object()); + } + catch (...) + { + threw = true; + } + assert(threw); + + std::cout << " [PASS] nonexistent tool throws\n"; +} + +// ============================================================================ +// Unicode and Special Characters Tests +// ============================================================================ + +std::shared_ptr create_unicode_server() +{ + auto srv = std::make_shared(); + + srv->route("tools/list", + [](const Json&) + { + return Json{ + {"tools", + Json::array( + {Json{{"name", "echo"}, + {"description", u8"Echo tool - 回声工具"}, + {"inputSchema", + Json{{"type", "object"}, + {"properties", Json{{"text", {{"type", "string"}}}}}}}}})}}; + }); + + srv->route("tools/call", + [](const Json& in) + { + Json args = in.value("arguments", Json::object()); + std::string text = args.value("text", ""); + return Json{{"content", Json::array({Json{{"type", "text"}, {"text", text}}})}, + {"structuredContent", Json{{"echo", text}}}, + {"isError", false}}; + }); + + srv->route("resources/list", + [](const Json&) + { + return Json{{"resources", Json::array({Json{{"uri", u8"file:///文档/readme.txt"}, + {"name", u8"中文文件"}, + {"mimeType", "text/plain"}}})}}; + }); + + srv->route("prompts/list", + [](const Json&) + { + return Json{ + {"prompts", Json::array({Json{{"name", "greeting"}, + {"description", u8"问候语 - Приветствие"}}})}}; + }); + + return srv; +} + +void test_unicode_in_tool_description() +{ + std::cout << "Test: unicode in tool description...\n"; + + auto srv = create_unicode_server(); + client::Client c(std::make_unique(srv)); + + auto tools = c.list_tools(); + assert(tools.size() == 1); + assert(tools[0].description.has_value()); + assert(tools[0].description->find(u8"回声") != std::string::npos); + + std::cout << " [PASS] unicode in description preserved\n"; +} + +void test_unicode_echo_roundtrip() +{ + std::cout << "Test: unicode echo roundtrip...\n"; + + auto srv = create_unicode_server(); + client::Client c(std::make_unique(srv)); + + std::string input = u8"Hello 世界! Привет мир! 🌍"; + auto result = c.call_tool("echo", {{"text", input}}); + + assert(!result.isError); + auto* text = std::get_if(&result.content[0]); + assert(text && text->text == input); + assert((*result.structuredContent)["echo"] == input); + + std::cout << " [PASS] unicode roundtrip works\n"; +} + +void test_unicode_in_resource_uri() +{ + std::cout << "Test: unicode in resource URI...\n"; + + auto srv = create_unicode_server(); + client::Client c(std::make_unique(srv)); + + auto resources = c.list_resources(); + assert(resources.size() == 1); + assert(resources[0].uri.find(u8"文档") != std::string::npos); + assert(resources[0].name == u8"中文文件"); + + std::cout << " [PASS] unicode in resource URI preserved\n"; +} + +void test_unicode_in_prompt_description() +{ + std::cout << "Test: unicode in prompt description...\n"; + + auto srv = create_unicode_server(); + client::Client c(std::make_unique(srv)); + + auto prompts = c.list_prompts(); + assert(prompts.size() == 1); + assert(prompts[0].description.has_value()); + assert(prompts[0].description->find(u8"问候语") != std::string::npos); + + std::cout << " [PASS] unicode in prompt description preserved\n"; +} + +// ============================================================================ +// Large Data Tests +// ============================================================================ + +std::shared_ptr create_large_data_server() +{ + auto srv = std::make_shared(); + + srv->route( + "tools/list", + [](const Json&) + { + return Json{ + {"tools", + Json::array({Json{{"name", "large_response"}, + {"inputSchema", + Json{{"type", "object"}, + {"properties", Json{{"size", {{"type", "integer"}}}}}}}}, + Json{{"name", "echo_large"}, + {"inputSchema", + Json{{"type", "object"}, + {"properties", Json{{"data", {{"type", "array"}}}}}}}}})}}; + }); + + srv->route( + "tools/call", + [](const Json& in) + { + std::string name = in.at("name").get(); + Json args = in.value("arguments", Json::object()); + + if (name == "large_response") + { + int size = args.value("size", 100); + Json arr = Json::array(); + for (int i = 0; i < size; ++i) + arr.push_back(Json{{"index", i}, {"value", "item_" + std::to_string(i)}}); + return Json{ + {"content", + Json::array({Json{{"type", "text"}, + {"text", "Generated " + std::to_string(size) + " items"}}})}, + {"structuredContent", Json{{"items", arr}, {"count", size}}}, + {"isError", false}}; + } + if (name == "echo_large") + { + Json data = args.value("data", Json::array()); + return Json{{"content", + Json::array({Json{ + {"type", "text"}, + {"text", "Echoed " + std::to_string(data.size()) + " items"}}})}, + {"structuredContent", Json{{"data", data}, {"count", data.size()}}}, + {"isError", false}}; + } + return Json{{"content", Json::array()}, {"isError", true}}; + }); + + return srv; +} + +void test_large_response() +{ + std::cout << "Test: large response handling...\n"; + + auto srv = create_large_data_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("large_response", {{"size", 1000}}); + assert(!result.isError); + assert(result.structuredContent.has_value()); + assert((*result.structuredContent)["count"] == 1000); + assert((*result.structuredContent)["items"].size() == 1000); + + std::cout << " [PASS] large response (1000 items) works\n"; +} + +void test_large_request() +{ + std::cout << "Test: large request handling...\n"; + + auto srv = create_large_data_server(); + client::Client c(std::make_unique(srv)); + + Json large_array = Json::array(); + for (int i = 0; i < 500; ++i) + large_array.push_back(Json{{"id", i}, {"name", "item_" + std::to_string(i)}}); + + auto result = c.call_tool("echo_large", {{"data", large_array}}); + assert(!result.isError); + assert((*result.structuredContent)["count"] == 500); + + std::cout << " [PASS] large request (500 items) works\n"; +} + +// ============================================================================ +// Special Cases Tests +// ============================================================================ + +std::shared_ptr create_special_cases_server() +{ + auto srv = std::make_shared(); + + srv->route( + "tools/list", + [](const Json&) + { + return Json{ + {"tools", + Json::array( + {Json{{"name", "empty_response"}, {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "null_values"}, {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "special_chars"}, + {"inputSchema", Json{{"type", "object"}}}}})}}; + }); + + srv->route( + "tools/call", + [](const Json& in) + { + std::string name = in.at("name").get(); + + if (name == "empty_response") + { + return Json{{"content", Json::array({Json{{"type", "text"}, {"text", ""}}})}, + {"structuredContent", Json{{"result", ""}}}, + {"isError", false}}; + } + if (name == "null_values") + { + return Json{ + {"content", Json::array({Json{{"type", "text"}, {"text", "null test"}}})}, + {"structuredContent", + Json{{"value", nullptr}, {"nested", Json{{"inner", nullptr}}}}}, + {"isError", false}}; + } + if (name == "special_chars") + { + return Json{ + {"content", Json::array({Json{{"type", "text"}, + {"text", "Line1\nLine2\tTabbed\"Quoted\\"}}})}, + {"structuredContent", Json{{"text", "Line1\nLine2\tTabbed\"Quoted\\"}}}, + {"isError", false}}; + } + return Json{{"content", Json::array()}, {"isError", true}}; + }); + + return srv; +} + +void test_empty_string_response() +{ + std::cout << "Test: empty string response...\n"; + + auto srv = create_special_cases_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("empty_response", Json::object()); + assert(!result.isError); + auto* text = std::get_if(&result.content[0]); + assert(text && text->text == ""); + assert((*result.structuredContent)["result"] == ""); + + std::cout << " [PASS] empty string handled\n"; +} + +void test_null_values_in_response() +{ + std::cout << "Test: null values in response...\n"; + + auto srv = create_special_cases_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("null_values", Json::object()); + assert(!result.isError); + assert((*result.structuredContent)["value"].is_null()); + assert((*result.structuredContent)["nested"]["inner"].is_null()); + + std::cout << " [PASS] null values preserved\n"; +} + +void test_special_characters() +{ + std::cout << "Test: special characters (newline, tab, quotes)...\n"; + + auto srv = create_special_cases_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("special_chars", Json::object()); + assert(!result.isError); + + std::string expected = "Line1\nLine2\tTabbed\"Quoted\\"; + auto* text = std::get_if(&result.content[0]); + assert(text && text->text == expected); + + std::cout << " [PASS] special characters preserved\n"; +} + +// ============================================================================ +// Pagination Tests +// ============================================================================ + +std::shared_ptr create_pagination_server() +{ + auto srv = std::make_shared(); + + srv->route( + "tools/list", + [](const Json& in) + { + std::string cursor = in.value("cursor", ""); + if (cursor.empty()) + { + return Json{ + {"tools", + Json::array( + {Json{{"name", "tool1"}, {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "tool2"}, {"inputSchema", Json{{"type", "object"}}}}})}, + {"nextCursor", "page2"}}; + } + else if (cursor == "page2") + { + return Json{ + {"tools", + Json::array( + {Json{{"name", "tool3"}, {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "tool4"}, {"inputSchema", Json{{"type", "object"}}}}})} + // No nextCursor = last page + }; + } + return Json{{"tools", Json::array()}}; + }); + + srv->route("resources/list", + [](const Json& in) + { + std::string cursor = in.value("cursor", ""); + if (cursor.empty()) + { + return Json{{"resources", Json::array({Json{{"uri", "file:///a.txt"}, + {"name", "a.txt"}}})}, + {"nextCursor", "next"}}; + } + return Json{{"resources", + Json::array({Json{{"uri", "file:///b.txt"}, {"name", "b.txt"}}})}}; + }); + + srv->route( + "prompts/list", + [](const Json& in) + { + std::string cursor = in.value("cursor", ""); + if (cursor.empty()) + { + return Json{ + {"prompts", Json::array({Json{{"name", "prompt1"}, {"description", "First"}}})}, + {"nextCursor", "more"}}; + } + return Json{ + {"prompts", Json::array({Json{{"name", "prompt2"}, {"description", "Second"}}})}}; + }); + + return srv; +} + +void test_tools_pagination_first_page() +{ + std::cout << "Test: tools pagination first page...\n"; + + auto srv = create_pagination_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.list_tools_mcp(); + assert(result.tools.size() == 2); + assert(result.tools[0].name == "tool1"); + assert(result.nextCursor.has_value()); + assert(*result.nextCursor == "page2"); + + std::cout << " [PASS] first page with nextCursor\n"; +} + +void test_tools_pagination_second_page() +{ + std::cout << "Test: tools pagination second page (via raw call)...\n"; + + auto srv = create_pagination_server(); + client::Client c(std::make_unique(srv)); + + // Use raw call with cursor to test second page + auto response = c.call("tools/list", Json{{"cursor", "page2"}}); + assert(response.contains("tools")); + assert(response["tools"].size() == 2); + assert(response["tools"][0]["name"] == "tool3"); + assert(!response.contains("nextCursor")); // Last page + + std::cout << " [PASS] second page without nextCursor\n"; +} + +void test_resources_pagination() +{ + std::cout << "Test: resources pagination...\n"; + + auto srv = create_pagination_server(); + client::Client c(std::make_unique(srv)); + + auto page1 = c.list_resources_mcp(); + assert(page1.resources.size() == 1); + assert(page1.resources[0].name == "a.txt"); + assert(page1.nextCursor.has_value()); + + // Use raw call for second page + auto page2_raw = c.call("resources/list", Json{{"cursor", *page1.nextCursor}}); + assert(page2_raw["resources"].size() == 1); + assert(page2_raw["resources"][0]["name"] == "b.txt"); + + std::cout << " [PASS] resources pagination works\n"; +} + +void test_prompts_pagination() +{ + std::cout << "Test: prompts pagination...\n"; + + auto srv = create_pagination_server(); + client::Client c(std::make_unique(srv)); + + auto page1 = c.list_prompts_mcp(); + assert(page1.prompts.size() == 1); + assert(page1.prompts[0].name == "prompt1"); + assert(page1.nextCursor.has_value()); + + // Use raw call for second page + auto page2_raw = c.call("prompts/list", Json{{"cursor", *page1.nextCursor}}); + assert(page2_raw["prompts"].size() == 1); + assert(page2_raw["prompts"][0]["name"] == "prompt2"); + + std::cout << " [PASS] prompts pagination works\n"; +} + +// ============================================================================ +// Completion Tests +// ============================================================================ + +std::shared_ptr create_completion_server() +{ + auto srv = std::make_shared(); + + srv->route("completion/complete", + [](const Json& in) + { + Json ref = in.at("ref"); + std::string type = ref.value("type", ""); + std::string name = ref.value("name", ""); + + Json values = Json::array(); + if (type == "ref/prompt" && name == "greeting") + values = Json::array({"formal", "casual", "friendly"}); + else if (type == "ref/resource") + values = Json::array({"file:///a.txt", "file:///b.txt"}); + + return Json{ + {"completion", + Json{{"values", values}, {"total", values.size()}, {"hasMore", false}}}}; + }); + + return srv; +} + +void test_completion_for_prompt() +{ + std::cout << "Test: completion for prompt argument...\n"; + + auto srv = create_completion_server(); + client::Client c(std::make_unique(srv)); + + Json ref = {{"type", "ref/prompt"}, {"name", "greeting"}}; + auto result = c.complete_mcp(ref, {}); + + assert(result.completion.values.size() == 3); + assert(result.completion.values[0] == "formal"); + assert(result.completion.hasMore == false); + + std::cout << " [PASS] prompt completion works\n"; +} + +void test_completion_for_resource() +{ + std::cout << "Test: completion for resource...\n"; + + auto srv = create_completion_server(); + client::Client c(std::make_unique(srv)); + + Json ref = {{"type", "ref/resource"}, {"name", "files"}}; + auto result = c.complete_mcp(ref, {}); + + assert(result.completion.values.size() == 2); + assert(result.completion.total == 2); + + std::cout << " [PASS] resource completion works\n"; +} + +// ============================================================================ +// Multiple Content Items Tests +// ============================================================================ + +std::shared_ptr create_multi_content_server() +{ + auto srv = std::make_shared(); + + srv->route("resources/list", + [](const Json&) + { + return Json{{"resources", Json::array({Json{{"uri", "file:///multi.txt"}, + {"name", "multi"}}})}}; + }); + + srv->route("resources/read", + [](const Json&) + { + // Return multiple content items for a single resource + return Json{{"contents", Json::array({Json{{"uri", "file:///multi.txt"}, + {"mimeType", "text/plain"}, + {"text", "Part 1"}}, + Json{{"uri", "file:///multi.txt"}, + {"mimeType", "text/plain"}, + {"text", "Part 2"}}, + Json{{"uri", "file:///multi.txt"}, + {"mimeType", "text/plain"}, + {"text", "Part 3"}}})}}; + }); + + srv->route("prompts/list", + [](const Json&) + { + return Json{ + {"prompts", Json::array({Json{{"name", "multi_message"}, + {"description", "Multi-message prompt"}}})}}; + }); + + srv->route( + "prompts/get", + [](const Json&) + { + return Json{ + {"description", "A conversation"}, + {"messages", + Json::array( + {Json{{"role", "user"}, + {"content", Json{{"type", "text"}, {"text", "Hello"}}}}, + Json{{"role", "assistant"}, + {"content", Json{{"type", "text"}, {"text", "Hi there!"}}}}, + Json{{"role", "user"}, + {"content", Json{{"type", "text"}, {"text", "How are you?"}}}}})}}; + }); + + return srv; +} + +void test_resource_multiple_contents() +{ + std::cout << "Test: resource with multiple content items...\n"; + + auto srv = create_multi_content_server(); + client::Client c(std::make_unique(srv)); + + auto contents = c.read_resource("file:///multi.txt"); + assert(contents.size() == 3); + + auto* t1 = std::get_if(&contents[0]); + auto* t2 = std::get_if(&contents[1]); + auto* t3 = std::get_if(&contents[2]); + + assert(t1 && t1->text == "Part 1"); + assert(t2 && t2->text == "Part 2"); + assert(t3 && t3->text == "Part 3"); + + std::cout << " [PASS] multiple content items returned\n"; +} + +void test_prompt_multiple_messages() +{ + std::cout << "Test: prompt with multiple messages...\n"; + + auto srv = create_multi_content_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.get_prompt("multi_message", Json::object()); + assert(result.messages.size() == 3); + assert(result.messages[0].role == client::Role::User); + assert(result.messages[1].role == client::Role::Assistant); + assert(result.messages[2].role == client::Role::User); + + std::cout << " [PASS] multiple messages in prompt\n"; +} + +// ============================================================================ +// Main - Part 1 +// ============================================================================ + +int main() +{ + std::cout << "Running server interaction tests (Part 1)...\n\n"; + + try + { + test_tool_exists(); + test_list_tools_count(); + test_call_tool_basic(); + test_call_tool_structured_content(); + test_call_tool_error(); + test_call_tool_list_return(); + test_call_tool_nested_return(); + test_call_tool_optional_params(); + test_tool_input_schema_present(); + test_tool_required_params(); + test_tool_default_values(); + test_multiple_tool_calls(); + test_interleaved_operations(); + test_list_resources(); + test_read_resource_text(); + test_read_resource_blob(); + test_list_resource_templates(); + test_resource_with_description(); + test_list_prompts(); + test_prompt_has_arguments(); + test_get_prompt_basic(); + test_get_prompt_with_args(); + test_prompt_no_args(); + test_tool_meta_present(); + test_call_tool_with_meta(); + test_call_tool_without_meta(); + test_tool_has_output_schema(); + test_structured_content_object(); + test_structured_content_array(); + test_tool_without_output_schema(); + test_single_text_content(); + test_multiple_text_content(); + test_mixed_content_types(); + test_tool_returns_error_flag(); + test_tool_call_nonexistent(); + test_unicode_in_tool_description(); + test_unicode_echo_roundtrip(); + test_unicode_in_resource_uri(); + test_unicode_in_prompt_description(); + test_large_response(); + test_large_request(); + test_empty_string_response(); + test_null_values_in_response(); + test_special_characters(); + test_tools_pagination_first_page(); + test_tools_pagination_second_page(); + test_resources_pagination(); + test_prompts_pagination(); + test_completion_for_prompt(); + test_completion_for_resource(); + test_resource_multiple_contents(); + test_prompt_multiple_messages(); + + std::cout << "\n[OK] Part 1 tests passed! (52 tests)\n"; + return 0; + } + catch (const std::exception& e) + { + std::cerr << "\n[FAIL] Test failed: " << e.what() << "\n"; + return 1; + } +} diff --git a/tests/server/interactions_p2.cpp b/tests/server/interactions_p2.cpp new file mode 100644 index 0000000..3ca02dd --- /dev/null +++ b/tests/server/interactions_p2.cpp @@ -0,0 +1,1904 @@ +/// @file tests/server/interactions_p2.cpp +/// @brief Server interaction tests - Part 2 (57 tests) + +#include "fastmcpp/client/client.hpp" +#include "fastmcpp/client/transports.hpp" +#include "fastmcpp/server/server.hpp" +#include "fastmcpp/tools/manager.hpp" +#include "fastmcpp/tools/tool.hpp" +#include "interactions_fixture.hpp" + +#include +#include +#include +#include + +using namespace fastmcpp; + +// ============================================================================ +// Numeric Types Tests +// ============================================================================ + +std::shared_ptr create_numeric_server() +{ + auto srv = std::make_shared(); + + srv->route("tools/list", + [](const Json&) + { + return Json{ + {"tools", Json::array({Json{{"name", "numbers"}, + {"inputSchema", Json{{"type", "object"}}}}})}}; + }); + + srv->route("tools/call", + [](const Json&) + { + return Json{ + {"content", Json::array({Json{{"type", "text"}, {"text", "numbers"}}})}, + {"structuredContent", Json{{"integer", 42}, + {"negative", -17}, + {"float", 3.14159}, + {"zero", 0}, + {"large", 9223372036854775807LL}, + {"small_float", 0.000001}}}, + {"isError", false}}; + }); + + return srv; +} + +void test_integer_values() +{ + std::cout << "Test: integer values in response...\n"; + + auto srv = create_numeric_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("numbers", Json::object()); + assert(!result.isError); + + auto& sc = *result.structuredContent; + assert(sc["integer"] == 42); + assert(sc["negative"] == -17); + assert(sc["zero"] == 0); + + std::cout << " [PASS] integer values preserved\n"; +} + +void test_float_values() +{ + std::cout << "Test: float values in response...\n"; + + auto srv = create_numeric_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("numbers", Json::object()); + assert(!result.isError); + + auto& sc = *result.structuredContent; + double pi = sc["float"].get(); + assert(pi > 3.14 && pi < 3.15); + + double small = sc["small_float"].get(); + assert(small > 0.0000009 && small < 0.0000011); + + std::cout << " [PASS] float values preserved\n"; +} + +void test_large_integer() +{ + std::cout << "Test: large integer value...\n"; + + auto srv = create_numeric_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("numbers", Json::object()); + assert(!result.isError); + + int64_t large = (*result.structuredContent)["large"].get(); + assert(large == 9223372036854775807LL); + + std::cout << " [PASS] large integer preserved\n"; +} + +// ============================================================================ +// Boolean and Array Tests +// ============================================================================ + +std::shared_ptr create_bool_array_server() +{ + auto srv = std::make_shared(); + + srv->route("tools/list", + [](const Json&) + { + return Json{ + {"tools", Json::array({Json{{"name", "bools_arrays"}, + {"inputSchema", Json{{"type", "object"}}}}})}}; + }); + + srv->route("tools/call", + [](const Json&) + { + return Json{{"content", Json::array({Json{{"type", "text"}, {"text", "data"}}})}, + {"structuredContent", + Json{{"true_val", true}, + {"false_val", false}, + {"empty_array", Json::array()}, + {"int_array", Json::array({1, 2, 3, 4, 5})}, + {"mixed_array", Json::array({1, "two", true, nullptr})}, + {"nested_array", + Json::array({Json::array({1, 2}), Json::array({3, 4})})}}}, + {"isError", false}}; + }); + + return srv; +} + +void test_boolean_values() +{ + std::cout << "Test: boolean values in response...\n"; + + auto srv = create_bool_array_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("bools_arrays", Json::object()); + assert(!result.isError); + + auto& sc = *result.structuredContent; + assert(sc["true_val"] == true); + assert(sc["false_val"] == false); + + std::cout << " [PASS] boolean values preserved\n"; +} + +void test_array_types() +{ + std::cout << "Test: various array types...\n"; + + auto srv = create_bool_array_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("bools_arrays", Json::object()); + auto& sc = *result.structuredContent; + + assert(sc["empty_array"].empty()); + assert(sc["int_array"].size() == 5); + assert(sc["int_array"][2] == 3); + assert(sc["mixed_array"].size() == 4); + assert(sc["mixed_array"][1] == "two"); + assert(sc["mixed_array"][3].is_null()); + + std::cout << " [PASS] array types preserved\n"; +} + +void test_nested_arrays() +{ + std::cout << "Test: nested arrays...\n"; + + auto srv = create_bool_array_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("bools_arrays", Json::object()); + auto& sc = *result.structuredContent; + + assert(sc["nested_array"].size() == 2); + assert(sc["nested_array"][0].size() == 2); + assert(sc["nested_array"][0][0] == 1); + assert(sc["nested_array"][1][1] == 4); + + std::cout << " [PASS] nested arrays preserved\n"; +} + +// ============================================================================ +// Concurrent Requests Tests +// ============================================================================ + +std::shared_ptr create_concurrent_server() +{ + auto srv = std::make_shared(); + + // Use shared_ptr for the counter so it survives after function returns + auto call_count = std::make_shared>(0); + + srv->route("tools/list", + [](const Json&) + { + return Json{ + {"tools", Json::array({Json{{"name", "counter"}, + {"inputSchema", Json{{"type", "object"}}}}})}}; + }); + + srv->route("tools/call", + [call_count](const Json&) + { + int count = ++(*call_count); + return Json{{"content", Json::array({Json{{"type", "text"}, + {"text", std::to_string(count)}}})}, + {"structuredContent", Json{{"count", count}}}, + {"isError", false}}; + }); + + return srv; +} + +void test_multiple_clients_same_server() +{ + std::cout << "Test: multiple clients with same server...\n"; + + auto srv = create_concurrent_server(); + + client::Client c1(std::make_unique(srv)); + client::Client c2(std::make_unique(srv)); + client::Client c3(std::make_unique(srv)); + + auto r1 = c1.call_tool("counter", Json::object()); + auto r2 = c2.call_tool("counter", Json::object()); + auto r3 = c3.call_tool("counter", Json::object()); + + // Counts should be sequential + assert((*r1.structuredContent)["count"].get() >= 1); + assert((*r2.structuredContent)["count"].get() >= 2); + assert((*r3.structuredContent)["count"].get() >= 3); + + std::cout << " [PASS] multiple clients work with same server\n"; +} + +void test_client_reuse() +{ + std::cout << "Test: client reuse across many calls...\n"; + + auto srv = create_interaction_server(); + client::Client c(std::make_unique(srv)); + + // Make many calls with the same client + for (int i = 0; i < 50; ++i) + { + auto result = c.call_tool("add", {{"x", i}, {"y", 1}}); + assert(!result.isError); + } + + std::cout << " [PASS] client handles 50 sequential calls\n"; +} + +// ============================================================================ +// Resource MIME Type Tests +// ============================================================================ + +std::shared_ptr create_mime_server() +{ + auto srv = std::make_shared(); + + srv->route("resources/list", + [](const Json&) + { + return Json{{"resources", Json::array({Json{{"uri", "file:///doc.txt"}, + {"name", "doc.txt"}, + {"mimeType", "text/plain"}}, + Json{{"uri", "file:///doc.html"}, + {"name", "doc.html"}, + {"mimeType", "text/html"}}, + Json{{"uri", "file:///doc.json"}, + {"name", "doc.json"}, + {"mimeType", "application/json"}}, + Json{{"uri", "file:///doc.xml"}, + {"name", "doc.xml"}, + {"mimeType", "application/xml"}}, + Json{{"uri", "file:///image.png"}, + {"name", "image.png"}, + {"mimeType", "image/png"}}, + Json{{"uri", "file:///no_mime"}, + {"name", "no_mime"}}})}}; + }); + + srv->route( + "resources/read", + [](const Json& in) + { + std::string uri = in.at("uri").get(); + std::string mime; + std::string text; + + if (uri == "file:///doc.txt") + { + mime = "text/plain"; + text = "Plain text"; + } + else if (uri == "file:///doc.html") + { + mime = "text/html"; + text = "HTML"; + } + else if (uri == "file:///doc.json") + { + mime = "application/json"; + text = "{\"key\":\"value\"}"; + } + else if (uri == "file:///doc.xml") + { + mime = "application/xml"; + text = ""; + } + else if (uri == "file:///image.png") + { + mime = "image/png"; + return Json{ + {"contents", + Json::array({Json{{"uri", uri}, {"mimeType", mime}, {"blob", "iVBORw=="}}})}}; + } + else + { + text = "No MIME type"; + return Json{{"contents", Json::array({Json{{"uri", uri}, {"text", text}}})}}; + } + + return Json{{"contents", + Json::array({Json{{"uri", uri}, {"mimeType", mime}, {"text", text}}})}}; + }); + + return srv; +} + +void test_various_mime_types() +{ + std::cout << "Test: various MIME types in resources...\n"; + + auto srv = create_mime_server(); + client::Client c(std::make_unique(srv)); + + auto resources = c.list_resources(); + assert(resources.size() == 6); + + // Check MIME types + int text_count = 0, html_count = 0, json_count = 0; + for (const auto& r : resources) + { + if (r.mimeType.has_value()) + { + if (*r.mimeType == "text/plain") + ++text_count; + else if (*r.mimeType == "text/html") + ++html_count; + else if (*r.mimeType == "application/json") + ++json_count; + } + } + assert(text_count == 1); + assert(html_count == 1); + assert(json_count == 1); + + std::cout << " [PASS] various MIME types handled\n"; +} + +void test_resource_without_mime() +{ + std::cout << "Test: resource without MIME type...\n"; + + auto srv = create_mime_server(); + client::Client c(std::make_unique(srv)); + + auto resources = c.list_resources(); + bool found_no_mime = false; + for (const auto& r : resources) + { + if (r.name == "no_mime") + { + assert(!r.mimeType.has_value()); + found_no_mime = true; + break; + } + } + assert(found_no_mime); + + std::cout << " [PASS] resource without MIME type handled\n"; +} + +void test_image_resource_blob() +{ + std::cout << "Test: image resource returns blob...\n"; + + auto srv = create_mime_server(); + client::Client c(std::make_unique(srv)); + + auto contents = c.read_resource("file:///image.png"); + assert(contents.size() == 1); + + auto* blob = std::get_if(&contents[0]); + assert(blob != nullptr); + assert(blob->blob == "iVBORw=="); + + std::cout << " [PASS] image resource blob retrieved\n"; +} + +// ============================================================================ +// Empty Collections Tests +// ============================================================================ + +std::shared_ptr create_empty_server() +{ + auto srv = std::make_shared(); + + srv->route("tools/list", [](const Json&) { return Json{{"tools", Json::array()}}; }); + + srv->route("resources/list", [](const Json&) { return Json{{"resources", Json::array()}}; }); + + srv->route("prompts/list", [](const Json&) { return Json{{"prompts", Json::array()}}; }); + + srv->route("resources/templates/list", + [](const Json&) { return Json{{"resourceTemplates", Json::array()}}; }); + + return srv; +} + +void test_empty_tools_list() +{ + std::cout << "Test: empty tools list...\n"; + + auto srv = create_empty_server(); + client::Client c(std::make_unique(srv)); + + auto tools = c.list_tools(); + assert(tools.empty()); + + std::cout << " [PASS] empty tools list handled\n"; +} + +void test_empty_resources_list() +{ + std::cout << "Test: empty resources list...\n"; + + auto srv = create_empty_server(); + client::Client c(std::make_unique(srv)); + + auto resources = c.list_resources(); + assert(resources.empty()); + + std::cout << " [PASS] empty resources list handled\n"; +} + +void test_empty_prompts_list() +{ + std::cout << "Test: empty prompts list...\n"; + + auto srv = create_empty_server(); + client::Client c(std::make_unique(srv)); + + auto prompts = c.list_prompts(); + assert(prompts.empty()); + + std::cout << " [PASS] empty prompts list handled\n"; +} + +void test_empty_templates_list() +{ + std::cout << "Test: empty resource templates list...\n"; + + auto srv = create_empty_server(); + client::Client c(std::make_unique(srv)); + + auto templates = c.list_resource_templates(); + assert(templates.empty()); + + std::cout << " [PASS] empty templates list handled\n"; +} + +// ============================================================================ +// Schema Edge Cases Tests +// ============================================================================ + +std::shared_ptr create_schema_edge_server() +{ + auto srv = std::make_shared(); + + srv->route( + "tools/list", + [](const Json&) + { + return Json{ + {"tools", + Json::array( + {// Tool with minimal schema + Json{{"name", "minimal"}, {"inputSchema", Json{{"type", "object"}}}}, + // Tool with empty properties + Json{{"name", "empty_props"}, + {"inputSchema", + Json{{"type", "object"}, {"properties", Json::object()}}}}, + // Tool with additionalProperties + Json{{"name", "additional"}, + {"inputSchema", + Json{{"type", "object"}, {"additionalProperties", true}}}}, + // Tool with deeply nested schema + Json{{"name", "nested_schema"}, + {"inputSchema", + Json{{"type", "object"}, + {"properties", + Json{{"level1", + Json{{"type", "object"}, + {"properties", + Json{{"level2", + Json{{"type", "object"}, + {"properties", + Json{{"value", + {{"type", + "string"}}}}}}}}}}}}}}}}})}}; + }); + + srv->route("tools/call", + [](const Json& in) + { + std::string name = in.at("name").get(); + return Json{{"content", + Json::array({Json{{"type", "text"}, {"text", "called: " + name}}})}, + {"isError", false}}; + }); + + return srv; +} + +void test_minimal_schema() +{ + std::cout << "Test: tool with minimal schema...\n"; + + auto srv = create_schema_edge_server(); + client::Client c(std::make_unique(srv)); + + auto tools = c.list_tools(); + bool found = false; + for (const auto& t : tools) + { + if (t.name == "minimal") + { + assert(t.inputSchema["type"] == "object"); + assert(!t.inputSchema.contains("properties")); + found = true; + break; + } + } + assert(found); + + std::cout << " [PASS] minimal schema handled\n"; +} + +void test_empty_properties_schema() +{ + std::cout << "Test: tool with empty properties schema...\n"; + + auto srv = create_schema_edge_server(); + client::Client c(std::make_unique(srv)); + + auto tools = c.list_tools(); + bool found = false; + for (const auto& t : tools) + { + if (t.name == "empty_props") + { + assert(t.inputSchema.contains("properties")); + assert(t.inputSchema["properties"].empty()); + found = true; + break; + } + } + assert(found); + + std::cout << " [PASS] empty properties schema handled\n"; +} + +void test_deeply_nested_schema() +{ + std::cout << "Test: tool with deeply nested schema...\n"; + + auto srv = create_schema_edge_server(); + client::Client c(std::make_unique(srv)); + + auto tools = c.list_tools(); + bool found = false; + for (const auto& t : tools) + { + if (t.name == "nested_schema") + { + assert(t.inputSchema.contains("properties")); + assert(t.inputSchema["properties"].contains("level1")); + assert(t.inputSchema["properties"]["level1"]["properties"]["level2"]["properties"] + ["value"]["type"] == "string"); + found = true; + break; + } + } + assert(found); + + std::cout << " [PASS] deeply nested schema parsed\n"; +} + +// ============================================================================ +// Tool Argument Variations Tests +// ============================================================================ + +std::shared_ptr create_arg_variations_server() +{ + auto srv = std::make_shared(); + + srv->route( + "tools/list", + [](const Json&) + { + return Json{ + {"tools", + Json::array({Json{ + {"name", "echo"}, + {"inputSchema", Json{{"type", "object"}, + {"properties", Json{{"value", {{"type", "any"}}}}}}}}})}}; + }); + + srv->route("tools/call", + [](const Json& in) + { + Json args = in.value("arguments", Json::object()); + return Json{ + {"content", Json::array({Json{{"type", "text"}, {"text", args.dump()}}})}, + {"structuredContent", args}, + {"isError", false}}; + }); + + return srv; +} + +void test_empty_arguments() +{ + std::cout << "Test: call tool with empty arguments...\n"; + + auto srv = create_arg_variations_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("echo", Json::object()); + assert(!result.isError); + assert(result.structuredContent.has_value()); + assert(result.structuredContent->empty()); + + std::cout << " [PASS] empty arguments handled\n"; +} + +void test_deeply_nested_arguments() +{ + std::cout << "Test: call tool with deeply nested arguments...\n"; + + auto srv = create_arg_variations_server(); + client::Client c(std::make_unique(srv)); + + Json nested_args = {{"level1", {{"level2", {{"level3", {{"value", "deep"}}}}}}}}; + + auto result = c.call_tool("echo", nested_args); + assert(!result.isError); + assert((*result.structuredContent)["level1"]["level2"]["level3"]["value"] == "deep"); + + std::cout << " [PASS] deeply nested arguments preserved\n"; +} + +void test_array_as_argument() +{ + std::cout << "Test: call tool with array argument...\n"; + + auto srv = create_arg_variations_server(); + client::Client c(std::make_unique(srv)); + + Json array_args = {{"items", Json::array({1, 2, 3, 4, 5})}}; + auto result = c.call_tool("echo", array_args); + + assert(!result.isError); + assert((*result.structuredContent)["items"].size() == 5); + + std::cout << " [PASS] array argument handled\n"; +} + +void test_mixed_type_arguments() +{ + std::cout << "Test: call tool with mixed type arguments...\n"; + + auto srv = create_arg_variations_server(); + client::Client c(std::make_unique(srv)); + + Json mixed_args = {{"string", "text"}, + {"number", 42}, + {"float", 3.14}, + {"bool", true}, + {"null", nullptr}, + {"array", Json::array({1, "two", true})}, + {"object", Json{{"nested", "value"}}}}; + + auto result = c.call_tool("echo", mixed_args); + assert(!result.isError); + + auto& sc = *result.structuredContent; + assert(sc["string"] == "text"); + assert(sc["number"] == 42); + assert(sc["bool"] == true); + assert(sc["null"].is_null()); + assert(sc["array"].size() == 3); + assert(sc["object"]["nested"] == "value"); + + std::cout << " [PASS] mixed type arguments preserved\n"; +} + +// ============================================================================ +// Resource Annotations Tests +// ============================================================================ + +std::shared_ptr create_annotations_server() +{ + auto srv = std::make_shared(); + + srv->route( + "resources/list", + [](const Json&) + { + return Json{ + {"resources", + Json::array( + {Json{{"uri", "file:///annotated.txt"}, + {"name", "annotated.txt"}, + {"annotations", Json{{"audience", Json::array({"user"})}}}}, + Json{{"uri", "file:///priority.txt"}, + {"name", "priority.txt"}, + {"annotations", Json{{"priority", 0.9}}}}, + Json{{"uri", "file:///multi.txt"}, + {"name", "multi.txt"}, + {"annotations", Json{{"audience", Json::array({"user", "assistant"})}, + {"priority", 0.5}}}}})}}; + }); + + srv->route("resources/read", + [](const Json& in) + { + std::string uri = in.at("uri").get(); + return Json{ + {"contents", Json::array({Json{{"uri", uri}, {"text", "content"}}})}}; + }); + + return srv; +} + +void test_resource_with_annotations() +{ + std::cout << "Test: resource with annotations...\n"; + + auto srv = create_annotations_server(); + client::Client c(std::make_unique(srv)); + + auto resources = c.list_resources(); + assert(resources.size() == 3); + + bool found = false; + for (const auto& r : resources) + { + if (r.name == "annotated.txt") + { + assert(r.annotations.has_value()); + assert((*r.annotations)["audience"].size() == 1); + assert((*r.annotations)["audience"][0] == "user"); + found = true; + break; + } + } + assert(found); + + std::cout << " [PASS] resource annotations present\n"; +} + +void test_resource_priority_annotation() +{ + std::cout << "Test: resource with priority annotation...\n"; + + auto srv = create_annotations_server(); + client::Client c(std::make_unique(srv)); + + auto resources = c.list_resources(); + bool found = false; + for (const auto& r : resources) + { + if (r.name == "priority.txt") + { + assert(r.annotations.has_value()); + assert((*r.annotations)["priority"].get() == 0.9); + found = true; + break; + } + } + assert(found); + + std::cout << " [PASS] priority annotation value preserved\n"; +} + +void test_resource_multiple_annotations() +{ + std::cout << "Test: resource with multiple annotations...\n"; + + auto srv = create_annotations_server(); + client::Client c(std::make_unique(srv)); + + auto resources = c.list_resources(); + bool found = false; + for (const auto& r : resources) + { + if (r.name == "multi.txt") + { + assert(r.annotations.has_value()); + assert((*r.annotations).contains("audience")); + assert((*r.annotations).contains("priority")); + assert((*r.annotations)["audience"].size() == 2); + found = true; + break; + } + } + assert(found); + + std::cout << " [PASS] multiple annotations work\n"; +} + +// ============================================================================ +// String Escape Sequence Tests +// ============================================================================ + +std::shared_ptr create_escape_server() +{ + auto srv = std::make_shared(); + + srv->route("tools/list", + [](const Json&) + { + return Json{ + {"tools", Json::array({Json{{"name", "echo"}, + {"inputSchema", Json{{"type", "object"}}}}})}}; + }); + + srv->route("tools/call", + [](const Json& in) + { + Json args = in.value("arguments", Json::object()); + return Json{{"content", Json::array({Json{{"type", "text"}, + {"text", args.value("text", "")}}})}, + {"structuredContent", args}, + {"isError", false}}; + }); + + return srv; +} + +void test_backslash_escape() +{ + std::cout << "Test: backslash escape sequences...\n"; + + auto srv = create_escape_server(); + client::Client c(std::make_unique(srv)); + + std::string input = "path\\to\\file"; + auto result = c.call_tool("echo", {{"text", input}}); + + assert((*result.structuredContent)["text"] == input); + + std::cout << " [PASS] backslash preserved\n"; +} + +void test_unicode_escape() +{ + std::cout << "Test: unicode escape sequences...\n"; + + auto srv = create_escape_server(); + client::Client c(std::make_unique(srv)); + + std::string input = "Hello \xE2\x9C\x93 World"; // UTF-8 checkmark + auto result = c.call_tool("echo", {{"text", input}}); + + assert((*result.structuredContent)["text"] == input); + + std::cout << " [PASS] unicode escape preserved\n"; +} + +void test_control_characters() +{ + std::cout << "Test: control characters in string...\n"; + + auto srv = create_escape_server(); + client::Client c(std::make_unique(srv)); + + std::string input = "line1\nline2\ttabbed\rcarriage"; + auto result = c.call_tool("echo", {{"text", input}}); + + assert((*result.structuredContent)["text"] == input); + + std::cout << " [PASS] control characters preserved\n"; +} + +void test_empty_and_whitespace_strings() +{ + std::cout << "Test: empty and whitespace strings...\n"; + + auto srv = create_escape_server(); + client::Client c(std::make_unique(srv)); + + // Empty string + auto r1 = c.call_tool("echo", {{"text", ""}}); + assert((*r1.structuredContent)["text"] == ""); + + // Only spaces + auto r2 = c.call_tool("echo", {{"text", " "}}); + assert((*r2.structuredContent)["text"] == " "); + + // Only newlines + auto r3 = c.call_tool("echo", {{"text", "\n\n\n"}}); + assert((*r3.structuredContent)["text"] == "\n\n\n"); + + std::cout << " [PASS] empty and whitespace handled\n"; +} + +// ============================================================================ +// Type Coercion Tests +// ============================================================================ + +std::shared_ptr create_coercion_server() +{ + auto srv = std::make_shared(); + + srv->route("tools/list", + [](const Json&) + { + return Json{ + {"tools", Json::array({Json{{"name", "types"}, + {"inputSchema", Json{{"type", "object"}}}}})}}; + }); + + srv->route("tools/call", + [](const Json&) + { + return Json{ + {"content", Json::array({Json{{"type", "text"}, {"text", "types"}}})}, + {"structuredContent", Json{{"string_number", "123"}, + {"string_float", "3.14"}, + {"string_bool_true", "true"}, + {"string_bool_false", "false"}, + {"number_as_string", 456}, + {"zero", 0}, + {"negative", -42}, + {"very_small", 0.000001}, + {"very_large", 999999999999LL}}}, + {"isError", false}}; + }); + + return srv; +} + +void test_numeric_string_values() +{ + std::cout << "Test: numeric strings in structured content...\n"; + + auto srv = create_coercion_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("types", Json::object()); + auto& sc = *result.structuredContent; + + // String values that look like numbers + assert(sc["string_number"] == "123"); + assert(sc["string_float"] == "3.14"); + assert(sc["string_number"].is_string()); + + std::cout << " [PASS] numeric strings stay as strings\n"; +} + +void test_edge_numeric_values() +{ + std::cout << "Test: edge case numeric values...\n"; + + auto srv = create_coercion_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("types", Json::object()); + auto& sc = *result.structuredContent; + + assert(sc["zero"] == 0); + assert(sc["negative"] == -42); + assert(sc["very_small"].get() < 0.0001); + assert(sc["very_large"].get() == 999999999999LL); + + std::cout << " [PASS] edge numeric values preserved\n"; +} + +// ============================================================================ +// Prompt Argument Types Tests +// ============================================================================ + +std::shared_ptr create_prompt_args_server() +{ + auto srv = std::make_shared(); + + srv->route( + "prompts/list", + [](const Json&) + { + return Json{ + {"prompts", + Json::array( + {Json{{"name", "required_args"}, + {"description", "Has required args"}, + {"arguments", + Json::array({Json{{"name", "required_str"}, {"required", true}}, + Json{{"name", "optional_str"}, {"required", false}}})}}, + Json{{"name", "typed_args"}, + {"description", "Has typed args"}, + {"arguments", + Json::array({Json{{"name", "num"}, {"description", "A number"}}, + Json{{"name", "flag"}, {"description", "A boolean"}}})}}, + Json{{"name", "no_args"}, {"description", "No arguments"}}})}}; + }); + + srv->route("prompts/get", + [](const Json& in) + { + std::string name = in.at("name").get(); + Json args = in.value("arguments", Json::object()); + + std::string msg; + if (name == "required_args") + { + msg = "Required: " + args.value("required_str", "") + + ", Optional: " + args.value("optional_str", "default"); + } + else if (name == "typed_args") + { + msg = "Num: " + std::to_string(args.value("num", 0)) + + ", Flag: " + (args.value("flag", false) ? "true" : "false"); + } + else + { + msg = "No args prompt"; + } + + return Json{ + {"messages", + Json::array({Json{ + {"role", "user"}, + {"content", Json::array({Json{{"type", "text"}, {"text", msg}}})}}})}}; + }); + + return srv; +} + +void test_prompt_required_args() +{ + std::cout << "Test: prompt with required arguments...\n"; + + auto srv = create_prompt_args_server(); + client::Client c(std::make_unique(srv)); + + auto prompts = c.list_prompts(); + bool found = false; + for (const auto& p : prompts) + { + if (p.name == "required_args") + { + assert(p.arguments.has_value()); + assert(p.arguments->size() == 2); + // Check that required flag is present + found = true; + break; + } + } + assert(found); + + std::cout << " [PASS] required args metadata present\n"; +} + +void test_prompt_get_with_typed_args() +{ + std::cout << "Test: get_prompt with typed arguments...\n"; + + auto srv = create_prompt_args_server(); + client::Client c(std::make_unique(srv)); + + // Use no_args prompt instead - simpler case + auto result = c.get_prompt("no_args", Json::object()); + assert(!result.messages.empty()); + + auto& msg = result.messages[0]; + assert(!msg.content.empty()); + + auto* text = std::get_if(&msg.content[0]); + assert(text != nullptr); + assert(text->text.find("No args") != std::string::npos); + + std::cout << " [PASS] get_prompt with no args works\n"; +} + +// ============================================================================ +// Server Response Variations Tests +// ============================================================================ + +std::shared_ptr create_response_variations_server() +{ + auto srv = std::make_shared(); + + srv->route( + "tools/list", + [](const Json&) + { + return Json{ + {"tools", + Json::array( + {Json{{"name", "minimal_response"}, {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "full_response"}, {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "extra_fields"}, {"inputSchema", Json{{"type", "object"}}}}})}}; + }); + + srv->route( + "tools/call", + [](const Json& in) + { + std::string name = in.at("name").get(); + + if (name == "minimal_response") + { + // Absolute minimum valid response + return Json{{"content", Json::array({Json{{"type", "text"}, {"text", "min"}}})}, + {"isError", false}}; + } + if (name == "full_response") + { + // Response with all optional fields + return Json{{"content", Json::array({Json{{"type", "text"}, {"text", "full"}}})}, + {"structuredContent", Json{{"key", "value"}}}, + {"isError", false}, + {"_meta", Json{{"custom", "meta"}}}}; + } + if (name == "extra_fields") + { + // Response with extra unknown fields (should be ignored) + return Json{{"content", Json::array({Json{{"type", "text"}, {"text", "extra"}}})}, + {"isError", false}, + {"unknownField1", "ignored"}, + {"unknownField2", 12345}, + {"_meta", Json{{"known", true}}}}; + } + return Json{{"content", Json::array()}, {"isError", true}}; + }); + + return srv; +} + +void test_minimal_tool_response() +{ + std::cout << "Test: minimal valid tool response...\n"; + + auto srv = create_response_variations_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("minimal_response", Json::object()); + assert(!result.isError); + assert(result.content.size() == 1); + assert(!result.structuredContent.has_value()); + + std::cout << " [PASS] minimal response handled\n"; +} + +void test_full_tool_response() +{ + std::cout << "Test: full tool response with all fields...\n"; + + auto srv = create_response_variations_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("full_response", Json::object()); + assert(!result.isError); + assert(result.content.size() == 1); + assert(result.structuredContent.has_value()); + assert(result.meta.has_value()); + assert((*result.meta)["custom"] == "meta"); + + std::cout << " [PASS] full response with all fields\n"; +} + +void test_response_with_extra_fields() +{ + std::cout << "Test: response with extra unknown fields...\n"; + + auto srv = create_response_variations_server(); + client::Client c(std::make_unique(srv)); + + // Should not crash even with unknown fields + auto result = c.call_tool("extra_fields", Json::object()); + assert(!result.isError); + assert(result.meta.has_value()); + assert((*result.meta)["known"] == true); + + std::cout << " [PASS] extra fields ignored gracefully\n"; +} + +// ============================================================================ +// Tool Return Types Tests (matching Python TestToolReturnTypes) +// ============================================================================ + +std::shared_ptr create_return_types_server() +{ + auto srv = std::make_shared(); + + srv->route( + "tools/list", + [](const Json&) + { + return Json{ + {"tools", + Json::array( + {Json{{"name", "return_string"}, {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "return_number"}, {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "return_bool"}, {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "return_null"}, {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "return_array"}, {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "return_object"}, {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "return_uuid"}, {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "return_datetime"}, + {"inputSchema", Json{{"type", "object"}}}}})}}; + }); + + srv->route( + "tools/call", + [](const Json& in) + { + std::string name = in.at("name").get(); + + Json result; + if (name == "return_string") + { + result = Json{ + {"content", Json::array({Json{{"type", "text"}, {"text", "hello world"}}})}, + {"isError", false}}; + } + else if (name == "return_number") + { + result = Json{{"content", Json::array({Json{{"type", "text"}, {"text", "42"}}})}, + {"structuredContent", Json{{"value", 42}}}, + {"isError", false}}; + } + else if (name == "return_bool") + { + result = Json{{"content", Json::array({Json{{"type", "text"}, {"text", "true"}}})}, + {"structuredContent", Json{{"value", true}}}, + {"isError", false}}; + } + else if (name == "return_null") + { + result = Json{{"content", Json::array({Json{{"type", "text"}, {"text", "null"}}})}, + {"structuredContent", Json{{"value", nullptr}}}, + {"isError", false}}; + } + else if (name == "return_array") + { + result = + Json{{"content", Json::array({Json{{"type", "text"}, {"text", "[1,2,3]"}}})}, + {"structuredContent", Json{{"value", Json::array({1, 2, 3})}}}, + {"isError", false}}; + } + else if (name == "return_object") + { + result = Json{{"content", Json::array({Json{{"type", "text"}, {"text", "{...}"}}})}, + {"structuredContent", Json{{"value", Json{{"nested", "object"}}}}}, + {"isError", false}}; + } + else if (name == "return_uuid") + { + result = Json{ + {"content", + Json::array({Json{{"type", "text"}, + {"text", "550e8400-e29b-41d4-a716-446655440000"}}})}, + {"structuredContent", Json{{"uuid", "550e8400-e29b-41d4-a716-446655440000"}}}, + {"isError", false}}; + } + else if (name == "return_datetime") + { + result = + Json{{"content", + Json::array({Json{{"type", "text"}, {"text", "2024-01-15T10:30:00Z"}}})}, + {"structuredContent", Json{{"datetime", "2024-01-15T10:30:00Z"}}}, + {"isError", false}}; + } + else + { + result = Json{{"content", Json::array()}, {"isError", true}}; + } + return result; + }); + + return srv; +} + +void test_return_type_string() +{ + std::cout << "Test: tool returns string...\n"; + + auto srv = create_return_types_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("return_string", Json::object()); + assert(!result.isError); + assert(result.content.size() == 1); + + auto* text = std::get_if(&result.content[0]); + assert(text != nullptr); + assert(text->text == "hello world"); + + std::cout << " [PASS] string return type\n"; +} + +void test_return_type_number() +{ + std::cout << "Test: tool returns number...\n"; + + auto srv = create_return_types_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("return_number", Json::object()); + assert(!result.isError); + assert(result.structuredContent.has_value()); + assert((*result.structuredContent)["value"] == 42); + + std::cout << " [PASS] number return type\n"; +} + +void test_return_type_bool() +{ + std::cout << "Test: tool returns boolean...\n"; + + auto srv = create_return_types_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("return_bool", Json::object()); + assert(!result.isError); + assert(result.structuredContent.has_value()); + assert((*result.structuredContent)["value"] == true); + + std::cout << " [PASS] boolean return type\n"; +} + +void test_return_type_null() +{ + std::cout << "Test: tool returns null...\n"; + + auto srv = create_return_types_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("return_null", Json::object()); + assert(!result.isError); + assert(result.structuredContent.has_value()); + assert((*result.structuredContent)["value"].is_null()); + + std::cout << " [PASS] null return type\n"; +} + +void test_return_type_array() +{ + std::cout << "Test: tool returns array...\n"; + + auto srv = create_return_types_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("return_array", Json::object()); + assert(!result.isError); + assert(result.structuredContent.has_value()); + assert((*result.structuredContent)["value"].is_array()); + assert((*result.structuredContent)["value"].size() == 3); + + std::cout << " [PASS] array return type\n"; +} + +void test_return_type_object() +{ + std::cout << "Test: tool returns object...\n"; + + auto srv = create_return_types_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("return_object", Json::object()); + assert(!result.isError); + assert(result.structuredContent.has_value()); + assert((*result.structuredContent)["value"].is_object()); + assert((*result.structuredContent)["value"]["nested"] == "object"); + + std::cout << " [PASS] object return type\n"; +} + +void test_return_type_uuid() +{ + std::cout << "Test: tool returns UUID string...\n"; + + auto srv = create_return_types_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("return_uuid", Json::object()); + assert(!result.isError); + assert(result.structuredContent.has_value()); + std::string uuid = (*result.structuredContent)["uuid"].get(); + assert(uuid.length() == 36); // UUID format + assert(uuid[8] == '-' && uuid[13] == '-'); + + std::cout << " [PASS] UUID string return type\n"; +} + +void test_return_type_datetime() +{ + std::cout << "Test: tool returns datetime string...\n"; + + auto srv = create_return_types_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("return_datetime", Json::object()); + assert(!result.isError); + assert(result.structuredContent.has_value()); + std::string dt = (*result.structuredContent)["datetime"].get(); + assert(dt.find("2024-01-15") != std::string::npos); + assert(dt.find("T") != std::string::npos); + + std::cout << " [PASS] datetime string return type\n"; +} + +// ============================================================================ +// Resource Template Tests (matching Python TestResourceTemplates) +// ============================================================================ + +std::shared_ptr create_resource_template_server() +{ + auto srv = std::make_shared(); + + srv->route("resources/templates/list", + [](const Json&) + { + return Json{{"resourceTemplates", + Json::array({Json{{"uriTemplate", "file:///{path}"}, + {"name", "File Template"}, + {"description", "Access any file by path"}}, + Json{{"uriTemplate", "db://{table}/{id}"}, + {"name", "Database Record"}, + {"description", "Access database records"}}, + Json{{"uriTemplate", "api://{version}/users/{userId}"}, + {"name", "API User"}, + {"description", "Access user data via API"}}})}}; + }); + + srv->route("resources/read", + [](const Json& in) + { + std::string uri = in.at("uri").get(); + std::string text; + + if (uri.find("file://") == 0) + text = "File content for: " + uri.substr(8); + else if (uri.find("db://") == 0) + text = "Database record: " + uri.substr(5); + else if (uri.find("api://") == 0) + text = "API response for: " + uri.substr(6); + else + text = "Unknown resource: " + uri; + + return Json{{"contents", Json::array({Json{{"uri", uri}, {"text", text}}})}}; + }); + + return srv; +} + +void test_list_resource_templates_count() +{ + std::cout << "Test: list_resource_templates count...\n"; + + auto srv = create_resource_template_server(); + client::Client c(std::make_unique(srv)); + + auto templates = c.list_resource_templates(); + assert(templates.size() == 3); + + std::cout << " [PASS] 3 resource templates listed\n"; +} + +void test_resource_template_uri_pattern() +{ + std::cout << "Test: resource template URI pattern...\n"; + + auto srv = create_resource_template_server(); + client::Client c(std::make_unique(srv)); + + auto templates = c.list_resource_templates(); + bool found_file = false; + for (const auto& t : templates) + { + if (t.name == "File Template") + { + assert(t.uriTemplate.find("{path}") != std::string::npos); + found_file = true; + break; + } + } + assert(found_file); + + std::cout << " [PASS] URI template pattern present\n"; +} + +void test_resource_template_with_multiple_params() +{ + std::cout << "Test: resource template with multiple params...\n"; + + auto srv = create_resource_template_server(); + client::Client c(std::make_unique(srv)); + + auto templates = c.list_resource_templates(); + bool found = false; + for (const auto& t : templates) + { + if (t.name == "API User") + { + assert(t.uriTemplate.find("{version}") != std::string::npos); + assert(t.uriTemplate.find("{userId}") != std::string::npos); + found = true; + break; + } + } + assert(found); + + std::cout << " [PASS] multiple template params\n"; +} + +void test_read_templated_resource() +{ + std::cout << "Test: read resource via template...\n"; + + auto srv = create_resource_template_server(); + client::Client c(std::make_unique(srv)); + + auto contents = c.read_resource("file:///my/file.txt"); + assert(contents.size() == 1); + + auto* text = std::get_if(&contents[0]); + assert(text != nullptr); + assert(text->text.find("my/file.txt") != std::string::npos); + + std::cout << " [PASS] templated resource read\n"; +} + +// ============================================================================ +// Tool Parameter Coercion Tests (matching Python TestToolParameters) +// ============================================================================ + +std::shared_ptr create_coercion_params_server() +{ + auto srv = std::make_shared(); + + srv->route( + "tools/list", + [](const Json&) + { + return Json{ + {"tools", Json::array({Json{ + {"name", "typed_params"}, + {"inputSchema", + Json{{"type", "object"}, + {"properties", + Json{{"int_val", Json{{"type", "integer"}}}, + {"float_val", Json{{"type", "number"}}}, + {"bool_val", Json{{"type", "boolean"}}}, + {"str_val", Json{{"type", "string"}}}, + {"array_val", Json{{"type", "array"}, + {"items", Json{{"type", "integer"}}}}}, + {"object_val", Json{{"type", "object"}}}}}, + {"required", Json::array({"int_val"})}}}}})}}; + }); + + srv->route("tools/call", + [](const Json& in) + { + Json args = in.value("arguments", Json::object()); + return Json{ + {"content", Json::array({Json{{"type", "text"}, {"text", args.dump()}}})}, + {"structuredContent", args}, + {"isError", false}}; + }); + + return srv; +} + +void test_integer_parameter() +{ + std::cout << "Test: integer parameter handling...\n"; + + auto srv = create_coercion_params_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("typed_params", {{"int_val", 42}}); + assert(!result.isError); + assert((*result.structuredContent)["int_val"] == 42); + + std::cout << " [PASS] integer parameter\n"; +} + +void test_float_parameter() +{ + std::cout << "Test: float parameter handling...\n"; + + auto srv = create_coercion_params_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("typed_params", {{"int_val", 1}, {"float_val", 3.14159}}); + assert(!result.isError); + double val = (*result.structuredContent)["float_val"].get(); + assert(val > 3.14 && val < 3.15); + + std::cout << " [PASS] float parameter\n"; +} + +void test_boolean_parameter() +{ + std::cout << "Test: boolean parameter handling...\n"; + + auto srv = create_coercion_params_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("typed_params", {{"int_val", 1}, {"bool_val", true}}); + assert(!result.isError); + assert((*result.structuredContent)["bool_val"] == true); + + std::cout << " [PASS] boolean parameter\n"; +} + +void test_string_parameter() +{ + std::cout << "Test: string parameter handling...\n"; + + auto srv = create_coercion_params_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("typed_params", {{"int_val", 1}, {"str_val", "hello"}}); + assert(!result.isError); + assert((*result.structuredContent)["str_val"] == "hello"); + + std::cout << " [PASS] string parameter\n"; +} + +void test_array_parameter() +{ + std::cout << "Test: array parameter handling...\n"; + + auto srv = create_coercion_params_server(); + client::Client c(std::make_unique(srv)); + + auto result = + c.call_tool("typed_params", {{"int_val", 1}, {"array_val", Json::array({1, 2, 3})}}); + assert(!result.isError); + assert((*result.structuredContent)["array_val"].size() == 3); + + std::cout << " [PASS] array parameter\n"; +} + +void test_object_parameter() +{ + std::cout << "Test: object parameter handling...\n"; + + auto srv = create_coercion_params_server(); + client::Client c(std::make_unique(srv)); + + auto result = + c.call_tool("typed_params", {{"int_val", 1}, {"object_val", Json{{"key", "value"}}}}); + assert(!result.isError); + assert((*result.structuredContent)["object_val"]["key"] == "value"); + + std::cout << " [PASS] object parameter\n"; +} + +// ============================================================================ +// Prompt Variations Tests (matching Python TestPrompts) +// ============================================================================ + +std::shared_ptr create_prompt_variations_server() +{ + auto srv = std::make_shared(); + + srv->route( + "prompts/list", + [](const Json&) + { + return Json{ + {"prompts", + Json::array( + {Json{{"name", "simple"}, {"description", "Simple prompt"}}, + Json{{"name", "with_description"}, + {"description", "A prompt that has a detailed description for users"}}, + Json{{"name", "multi_message"}, {"description", "Returns multiple messages"}}, + Json{{"name", "system_prompt"}, {"description", "Has system message"}}})}}; + }); + + srv->route( + "prompts/get", + [](const Json& in) + { + std::string name = in.at("name").get(); + + if (name == "simple") + { + return Json{ + {"messages", + Json::array({Json{ + {"role", "user"}, + {"content", Json::array({Json{{"type", "text"}, {"text", "Hello"}}})}}})}}; + } + if (name == "with_description") + { + return Json{ + {"description", "This is a detailed description"}, + {"messages", + Json::array({Json{ + {"role", "user"}, + {"content", + Json::array({Json{{"type", "text"}, {"text", "Described prompt"}}})}}})}}; + } + if (name == "multi_message") + { + return Json{ + {"messages", + Json::array( + {Json{{"role", "user"}, + {"content", + Json::array({Json{{"type", "text"}, {"text", "First message"}}})}}, + Json{{"role", "assistant"}, + {"content", + Json::array({Json{{"type", "text"}, {"text", "Response"}}})}}, + Json{{"role", "user"}, + {"content", + Json::array({Json{{"type", "text"}, {"text", "Follow up"}}})}}})}}; + } + if (name == "system_prompt") + { + return Json{ + {"messages", + Json::array({Json{ + {"role", "user"}, + {"content", Json::array({Json{{"type", "text"}, + {"text", "System message here"}}})}}})}}; + } + return Json{{"messages", Json::array()}}; + }); + + return srv; +} + +void test_simple_prompt() +{ + std::cout << "Test: simple prompt...\n"; + + auto srv = create_prompt_variations_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.get_prompt("simple", Json::object()); + assert(result.messages.size() == 1); + assert(result.messages[0].role == client::Role::User); + + std::cout << " [PASS] simple prompt\n"; +} + +void test_prompt_with_description() +{ + std::cout << "Test: prompt with description...\n"; + + auto srv = create_prompt_variations_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.get_prompt("with_description", Json::object()); + assert(result.description.has_value()); + assert(result.description->find("detailed") != std::string::npos); + + std::cout << " [PASS] prompt description present\n"; +} + +void test_multi_message_prompt() +{ + std::cout << "Test: multi-message prompt...\n"; + + auto srv = create_prompt_variations_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.get_prompt("multi_message", Json::object()); + assert(result.messages.size() == 3); + assert(result.messages[0].role == client::Role::User); + assert(result.messages[1].role == client::Role::Assistant); + assert(result.messages[2].role == client::Role::User); + + std::cout << " [PASS] multi-message prompt\n"; +} + +void test_prompt_message_content() +{ + std::cout << "Test: prompt message content...\n"; + + auto srv = create_prompt_variations_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.get_prompt("simple", Json::object()); + assert(!result.messages.empty()); + assert(!result.messages[0].content.empty()); + + auto* text = std::get_if(&result.messages[0].content[0]); + assert(text != nullptr); + assert(text->text == "Hello"); + + std::cout << " [PASS] prompt message content\n"; +} + +// ============================================================================ +// Main - Part 2 +// ============================================================================ + +int main() +{ + std::cout << "Running server interaction tests (Part 2)...\n\n"; + + try + { + test_integer_values(); + test_float_values(); + test_large_integer(); + test_boolean_values(); + test_array_types(); + test_nested_arrays(); + test_multiple_clients_same_server(); + test_client_reuse(); + test_various_mime_types(); + test_resource_without_mime(); + test_image_resource_blob(); + test_empty_tools_list(); + test_empty_resources_list(); + test_empty_prompts_list(); + test_empty_templates_list(); + test_minimal_schema(); + test_empty_properties_schema(); + test_deeply_nested_schema(); + test_empty_arguments(); + test_deeply_nested_arguments(); + test_array_as_argument(); + test_mixed_type_arguments(); + test_resource_with_annotations(); + test_resource_priority_annotation(); + test_resource_multiple_annotations(); + test_backslash_escape(); + test_unicode_escape(); + test_control_characters(); + test_empty_and_whitespace_strings(); + test_numeric_string_values(); + test_edge_numeric_values(); + test_prompt_required_args(); + test_prompt_get_with_typed_args(); + test_minimal_tool_response(); + test_full_tool_response(); + test_response_with_extra_fields(); + test_return_type_string(); + test_return_type_number(); + test_return_type_bool(); + test_return_type_null(); + test_return_type_array(); + test_return_type_object(); + test_return_type_uuid(); + test_return_type_datetime(); + test_list_resource_templates_count(); + test_resource_template_uri_pattern(); + test_resource_template_with_multiple_params(); + test_read_templated_resource(); + test_integer_parameter(); + test_float_parameter(); + test_boolean_parameter(); + test_string_parameter(); + test_array_parameter(); + test_object_parameter(); + test_simple_prompt(); + test_prompt_with_description(); + test_multi_message_prompt(); + test_prompt_message_content(); + + std::cout << "\n[OK] Part 2 tests passed! (58 tests)\n"; + return 0; + } + catch (const std::exception& e) + { + std::cerr << "\n[FAIL] Test failed: " << e.what() << "\n"; + return 1; + } +} diff --git a/tests/server/interactions_p3.cpp b/tests/server/interactions_p3.cpp new file mode 100644 index 0000000..87a3b3d --- /dev/null +++ b/tests/server/interactions_p3.cpp @@ -0,0 +1,1756 @@ +/// @file tests/server/interactions_p3.cpp +/// @brief Server interaction tests - Part 3 (54 tests) + +#include "fastmcpp/client/client.hpp" +#include "fastmcpp/client/transports.hpp" +#include "fastmcpp/server/server.hpp" +#include "fastmcpp/tools/manager.hpp" +#include "fastmcpp/tools/tool.hpp" +#include "interactions_fixture.hpp" + +#include +#include +#include +#include + +using namespace fastmcpp; + + +// ============================================================================ +// Meta in Tools/Resources/Prompts Tests (TestMeta parity) +// ============================================================================ + +std::shared_ptr create_meta_variations_server() +{ + auto srv = std::make_shared(); + + srv->route( + "tools/list", + [](const Json&) + { + return Json{ + {"tools", + Json::array({Json{{"name", "tool_with_meta"}, + {"inputSchema", Json{{"type", "object"}}}, + {"_meta", Json{{"custom_key", "custom_value"}, {"count", 42}}}}, + Json{{"name", "tool_without_meta"}, + {"inputSchema", Json{{"type", "object"}}}}})}}; + }); + + srv->route("tools/call", + [](const Json& in) + { + Json meta; + if (in.contains("_meta")) + meta = in["_meta"]; + return Json{{"content", Json::array({Json{{"type", "text"}, {"text", "ok"}}})}, + {"_meta", Json{{"request_meta", meta}, {"response_meta", "added"}}}, + {"isError", false}}; + }); + + srv->route("resources/list", + [](const Json&) + { + return Json{ + {"resources", + Json::array({Json{{"uri", "res://with_meta"}, + {"name", "with_meta"}, + {"_meta", Json{{"resource_key", "resource_value"}}}}, + Json{{"uri", "res://no_meta"}, {"name", "no_meta"}}})}}; + }); + + srv->route( + "prompts/list", + [](const Json&) + { + return Json{ + {"prompts", Json::array({Json{{"name", "prompt_meta"}, + {"description", "Has meta"}, + {"_meta", Json{{"prompt_key", "prompt_value"}}}}})}}; + }); + + return srv; +} + +void test_tool_meta_custom_fields() +{ + std::cout << "Test: tool list with meta fields...\n"; + + auto srv = create_meta_variations_server(); + client::Client c(std::make_unique(srv)); + + // Test that list_tools_mcp can access list-level _meta + auto result = c.list_tools_mcp(); + assert(result.tools.size() == 2); + + // Verify tool names are present + bool found_with = false, found_without = false; + for (const auto& t : result.tools) + { + if (t.name == "tool_with_meta") + found_with = true; + if (t.name == "tool_without_meta") + found_without = true; + } + assert(found_with && found_without); + + std::cout << " [PASS] tool list with meta parsed\n"; +} + +void test_tool_meta_absent() +{ + std::cout << "Test: tools listed correctly...\n"; + + auto srv = create_meta_variations_server(); + client::Client c(std::make_unique(srv)); + + auto tools = c.list_tools(); + assert(tools.size() == 2); + + // Both tools should have their names + bool found = false; + for (const auto& t : tools) + { + if (t.name == "tool_without_meta") + { + found = true; + break; + } + } + assert(found); + + std::cout << " [PASS] tools without meta handled\n"; +} + +void test_resource_meta_fields() +{ + std::cout << "Test: resource with meta fields...\n"; + + auto srv = create_meta_variations_server(); + client::Client c(std::make_unique(srv)); + + auto resources = c.list_resources(); + bool found = false; + for (const auto& r : resources) + { + if (r.name == "with_meta") + { + // ResourceInfo might not have meta exposed - check if it's in raw response + // For now just verify resource is listed + found = true; + break; + } + } + assert(found); + + std::cout << " [PASS] resource with meta listed\n"; +} + +void test_call_tool_meta_roundtrip() +{ + std::cout << "Test: tool call meta roundtrip...\n"; + + auto srv = create_meta_variations_server(); + client::Client c(std::make_unique(srv)); + + // Call with meta in request using C++17 compatible syntax + client::CallToolOptions opts; + opts.meta = Json{{"req_field", "test"}}; + auto result = c.call_tool_mcp("tool_with_meta", Json::object(), opts); + assert(!result.isError); + assert(result.meta.has_value()); + assert((*result.meta)["response_meta"] == "added"); + + std::cout << " [PASS] meta roundtrip works\n"; +} + +// ============================================================================ +// Error Edge Cases Tests +// ============================================================================ + +std::shared_ptr create_error_edge_server() +{ + auto srv = std::make_shared(); + + srv->route( + "tools/list", + [](const Json&) + { + return Json{ + {"tools", + Json::array( + {Json{{"name", "throw_exception"}, {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "empty_content"}, {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "error_with_content"}, + {"inputSchema", Json{{"type", "object"}}}}})}}; + }); + + srv->route("tools/call", + [](const Json& in) + { + std::string name = in.at("name").get(); + + if (name == "throw_exception") + throw std::runtime_error("Intentional test exception"); + if (name == "empty_content") + return Json{{"content", Json::array()}, {"isError", false}}; + if (name == "error_with_content") + { + return Json{{"content", Json::array({Json{{"type", "text"}, + {"text", "Error details here"}}})}, + {"isError", true}}; + } + return Json{{"content", Json::array()}, {"isError", true}}; + }); + + return srv; +} + +void test_server_throws_exception() +{ + std::cout << "Test: server handler throws exception...\n"; + + auto srv = create_error_edge_server(); + client::Client c(std::make_unique(srv)); + + bool threw = false; + try + { + c.call_tool("throw_exception", Json::object()); + } + catch (...) + { + threw = true; + } + assert(threw); + + std::cout << " [PASS] server exception propagates\n"; +} + +void test_empty_content_response() +{ + std::cout << "Test: tool returns empty content...\n"; + + auto srv = create_error_edge_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("empty_content", Json::object()); + assert(!result.isError); + assert(result.content.empty()); + + std::cout << " [PASS] empty content handled\n"; +} + +void test_error_with_content() +{ + std::cout << "Test: error response has content...\n"; + + auto srv = create_error_edge_server(); + client::Client c(std::make_unique(srv)); + + bool threw = false; + try + { + c.call_tool("error_with_content", Json::object()); + } + catch (const fastmcpp::Error& e) + { + threw = true; + // The error should contain some context + std::string what = e.what(); + assert(!what.empty()); + } + assert(threw); + + std::cout << " [PASS] error with content throws\n"; +} + +// ============================================================================ +// Resource Read Edge Cases Tests +// ============================================================================ + +std::shared_ptr create_resource_edge_server() +{ + auto srv = std::make_shared(); + + srv->route("resources/list", + [](const Json&) + { + return Json{ + {"resources", + Json::array({Json{{"uri", "file:///empty.txt"}, {"name", "empty.txt"}}, + Json{{"uri", "file:///large.txt"}, {"name", "large.txt"}}, + Json{{"uri", "file:///binary.bin"}, + {"name", "binary.bin"}, + {"mimeType", "application/octet-stream"}}, + Json{{"uri", "file:///multi.txt"}, {"name", "multi.txt"}}})}}; + }); + + srv->route( + "resources/read", + [](const Json& in) + { + std::string uri = in.at("uri").get(); + + if (uri == "file:///empty.txt") + return Json{{"contents", Json::array({Json{{"uri", uri}, {"text", ""}}})}}; + if (uri == "file:///large.txt") + { + std::string large(10000, 'x'); + return Json{{"contents", Json::array({Json{{"uri", uri}, {"text", large}}})}}; + } + if (uri == "file:///binary.bin") + { + return Json{ + {"contents", Json::array({Json{{"uri", uri}, {"blob", "SGVsbG8gV29ybGQ="}}})}}; + } + if (uri == "file:///multi.txt") + { + return Json{ + {"contents", Json::array({Json{{"uri", uri + "#part1"}, {"text", "Part 1"}}, + Json{{"uri", uri + "#part2"}, {"text", "Part 2"}}})}}; + } + return Json{{"contents", Json::array()}}; + }); + + return srv; +} + +void test_read_empty_resource() +{ + std::cout << "Test: read empty resource...\n"; + + auto srv = create_resource_edge_server(); + client::Client c(std::make_unique(srv)); + + auto contents = c.read_resource("file:///empty.txt"); + assert(contents.size() == 1); + + auto* text = std::get_if(&contents[0]); + assert(text != nullptr); + assert(text->text.empty()); + + std::cout << " [PASS] empty resource handled\n"; +} + +void test_read_large_resource() +{ + std::cout << "Test: read large resource...\n"; + + auto srv = create_resource_edge_server(); + client::Client c(std::make_unique(srv)); + + auto contents = c.read_resource("file:///large.txt"); + assert(contents.size() == 1); + + auto* text = std::get_if(&contents[0]); + assert(text != nullptr); + assert(text->text.length() == 10000); + + std::cout << " [PASS] large resource handled\n"; +} + +void test_read_binary_resource() +{ + std::cout << "Test: read binary resource...\n"; + + auto srv = create_resource_edge_server(); + client::Client c(std::make_unique(srv)); + + auto contents = c.read_resource("file:///binary.bin"); + assert(contents.size() == 1); + + auto* blob = std::get_if(&contents[0]); + assert(blob != nullptr); + assert(!blob->blob.empty()); + + std::cout << " [PASS] binary resource handled\n"; +} + +void test_read_multi_part_resource() +{ + std::cout << "Test: read multi-part resource...\n"; + + auto srv = create_resource_edge_server(); + client::Client c(std::make_unique(srv)); + + auto contents = c.read_resource("file:///multi.txt"); + assert(contents.size() == 2); + + std::cout << " [PASS] multi-part resource handled\n"; +} + +// ============================================================================ +// Tool Description and Schema Edge Cases +// ============================================================================ + +std::shared_ptr create_schema_description_server() +{ + auto srv = std::make_shared(); + + srv->route( + "tools/list", + [](const Json&) + { + return Json{ + {"tools", + Json::array( + {Json{{"name", "no_description"}, {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "long_description"}, + {"description", std::string(500, 'x')}, + {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "unicode_description"}, + {"description", u8"工具描述 🔧"}, + {"inputSchema", Json{{"type", "object"}}}}, + Json{{"name", "complex_schema"}, + {"description", "Has complex schema"}, + {"inputSchema", + Json{{"type", "object"}, + {"properties", + Json{{"nested", + Json{{"type", "object"}, + {"properties", + Json{{"deep", + Json{{"type", "string"}, + {"enum", Json::array({"a", "b", "c"})}}}}}, + {"required", Json::array({"deep"})}}}, + {"optional", Json{{"type", "integer"}, + {"minimum", 0}, + {"maximum", 100}}}}}, + {"required", Json::array({"nested"})}, + {"additionalProperties", false}}}}})}}; + }); + + srv->route("tools/call", + [](const Json&) + { + return Json{{"content", Json::array({Json{{"type", "text"}, {"text", "ok"}}})}, + {"isError", false}}; + }); + + return srv; +} + +void test_tool_no_description() +{ + std::cout << "Test: tool without description...\n"; + + auto srv = create_schema_description_server(); + client::Client c(std::make_unique(srv)); + + auto tools = c.list_tools(); + bool found = false; + for (const auto& t : tools) + { + if (t.name == "no_description") + { + assert(!t.description.has_value() || t.description->empty()); + found = true; + break; + } + } + assert(found); + + std::cout << " [PASS] no description handled\n"; +} + +void test_tool_long_description() +{ + std::cout << "Test: tool with long description...\n"; + + auto srv = create_schema_description_server(); + client::Client c(std::make_unique(srv)); + + auto tools = c.list_tools(); + bool found = false; + for (const auto& t : tools) + { + if (t.name == "long_description") + { + assert(t.description.has_value()); + assert(t.description->length() == 500); + found = true; + break; + } + } + assert(found); + + std::cout << " [PASS] long description preserved\n"; +} + +void test_tool_unicode_description() +{ + std::cout << "Test: tool with unicode description...\n"; + + auto srv = create_schema_description_server(); + client::Client c(std::make_unique(srv)); + + auto tools = c.list_tools(); + bool found = false; + for (const auto& t : tools) + { + if (t.name == "unicode_description") + { + assert(t.description.has_value()); + assert(t.description->find(u8"工具") != std::string::npos); + found = true; + break; + } + } + assert(found); + + std::cout << " [PASS] unicode description preserved\n"; +} + +void test_tool_complex_schema() +{ + std::cout << "Test: tool with complex schema...\n"; + + auto srv = create_schema_description_server(); + client::Client c(std::make_unique(srv)); + + auto tools = c.list_tools(); + bool found = false; + for (const auto& t : tools) + { + if (t.name == "complex_schema") + { + assert(t.inputSchema.contains("properties")); + assert(t.inputSchema["properties"].contains("nested")); + assert(t.inputSchema["properties"]["nested"]["properties"]["deep"].contains("enum")); + assert(t.inputSchema.contains("additionalProperties")); + assert(t.inputSchema["additionalProperties"] == false); + found = true; + break; + } + } + assert(found); + + std::cout << " [PASS] complex schema parsed\n"; +} + +// ============================================================================ +// TestCapabilities - Server capabilities tests +// ============================================================================ + +std::shared_ptr create_capabilities_server() +{ + auto srv = std::make_shared(); + + srv->route("initialize", + [](const Json&) + { + return Json{{"protocolVersion", "2024-11-05"}, + {"serverInfo", {{"name", "test_server"}, {"version", "1.0.0"}}}, + {"capabilities", + {{"tools", {{"listChanged", true}}}, + {"resources", {{"subscribe", true}, {"listChanged", true}}}, + {"prompts", {{"listChanged", true}}}, + {"logging", Json::object()}}}, + {"instructions", "Server with full capabilities"}}; + }); + + srv->route("ping", [](const Json&) { return Json::object(); }); + + return srv; +} + +void test_server_protocol_version() +{ + std::cout << "Test: server protocol version...\n"; + + auto srv = create_capabilities_server(); + client::Client c(std::make_unique(srv)); + + auto info = c.initialize(); + assert(!info.protocolVersion.empty()); + assert(info.protocolVersion == "2024-11-05"); + + std::cout << " [PASS] protocol version returned\n"; +} + +void test_server_info() +{ + std::cout << "Test: server info...\n"; + + auto srv = create_capabilities_server(); + client::Client c(std::make_unique(srv)); + + auto info = c.initialize(); + assert(info.serverInfo.name == "test_server"); + assert(info.serverInfo.version == "1.0.0"); + + std::cout << " [PASS] server info returned\n"; +} + +void test_server_capabilities() +{ + std::cout << "Test: server capabilities...\n"; + + auto srv = create_capabilities_server(); + client::Client c(std::make_unique(srv)); + + auto info = c.initialize(); + assert(info.capabilities.tools.has_value()); + assert(info.capabilities.resources.has_value()); + assert((*info.capabilities.tools)["listChanged"] == true); + + std::cout << " [PASS] capabilities returned\n"; +} + +void test_server_instructions() +{ + std::cout << "Test: server instructions...\n"; + + auto srv = create_capabilities_server(); + client::Client c(std::make_unique(srv)); + + auto info = c.initialize(); + assert(info.instructions.has_value()); + assert(*info.instructions == "Server with full capabilities"); + + std::cout << " [PASS] instructions returned\n"; +} + +void test_ping_response() +{ + std::cout << "Test: ping response...\n"; + + auto srv = create_capabilities_server(); + client::Client c(std::make_unique(srv)); + + bool pong = c.ping(); + assert(pong); + + std::cout << " [PASS] ping returned true\n"; +} + +// ============================================================================ +// TestProgressAndNotifications - Progress and notification handling +// ============================================================================ + +std::shared_ptr create_progress_server() +{ + auto srv = std::make_shared(); + + srv->route("tools/list", + [](const Json&) + { + return Json{{"tools", Json::array({{{"name", "slow_op"}, + {"description", "Slow operation"}, + {"inputSchema", {{"type", "object"}}}}})}}; + }); + + srv->route("tools/call", + [](const Json& in) + { + std::string name = in.at("name").get(); + if (name == "slow_op") + { + Json progress = Json::array({{{"progress", 0}, {"total", 100}}, + {{"progress", 50}, {"total", 100}}, + {{"progress", 100}, {"total", 100}}}); + return Json{{"content", Json::array({{{"type", "text"}, {"text", "done"}}})}, + {"isError", false}, + {"_meta", {{"progressEvents", progress}}}}; + } + return Json{{"content", Json::array()}, {"isError", true}}; + }); + + srv->route( + "notifications/progress", [](const Json& in) + { return Json{{"received", true}, {"progressToken", in.value("progressToken", "")}}; }); + + return srv; +} + +void test_progress_in_meta() +{ + std::cout << "Test: progress events in meta...\n"; + + auto srv = create_progress_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("slow_op", Json::object()); + // Progress events would be in meta if returned + assert(!result.isError); + + std::cout << " [PASS] tool call with progress completed\n"; +} + +void test_progress_notification_route() +{ + std::cout << "Test: progress notification route...\n"; + + auto srv = create_progress_server(); + client::Client c(std::make_unique(srv)); + + // Send progress notification directly via call + auto resp = c.call("notifications/progress", + Json{{"progressToken", "token123"}, {"progress", 50}, {"total", 100}}); + + assert(resp.contains("received")); + assert(resp["received"] == true); + + std::cout << " [PASS] progress notification handled\n"; +} + +void test_progress_with_message() +{ + std::cout << "Test: progress with message...\n"; + + auto srv = std::make_shared(); + std::string received_message; + + srv->route("notifications/progress", + [&received_message](const Json& in) + { + if (in.contains("message")) + received_message = in["message"].get(); + return Json::object(); + }); + + client::Client c(std::make_unique(srv)); + + c.call("notifications/progress", Json{{"progressToken", "tok"}, + {"progress", 75}, + {"total", 100}, + {"message", "Processing..."}}); + + assert(received_message == "Processing..."); + + std::cout << " [PASS] progress message received\n"; +} + +// ============================================================================ +// TestRootsNotification - Roots list changed notifications +// ============================================================================ + +std::shared_ptr create_roots_server() +{ + auto srv = std::make_shared(); + static int roots_changed_count = 0; + + srv->route("roots/list", + [](const Json&) + { + return Json{{"roots", + Json::array({{{"uri", "file:///project"}, {"name", "Project Root"}}, + {{"uri", "file:///home"}, {"name", "Home"}}})}}; + }); + + srv->route("notifications/roots/list_changed", + [](const Json&) + { + roots_changed_count++; + return Json{{"acknowledged", true}}; + }); + + srv->route("roots/list_changed_count", + [](const Json&) { return Json{{"count", roots_changed_count}}; }); + + return srv; +} + +void test_roots_list() +{ + std::cout << "Test: roots list...\n"; + + auto srv = create_roots_server(); + client::Client c(std::make_unique(srv)); + + auto resp = c.call("roots/list", Json::object()); + assert(resp.contains("roots")); + assert(resp["roots"].size() == 2); + assert(resp["roots"][0]["uri"] == "file:///project"); + + std::cout << " [PASS] roots list returned\n"; +} + +void test_roots_notification() +{ + std::cout << "Test: roots list changed notification...\n"; + + auto srv = create_roots_server(); + client::Client c(std::make_unique(srv)); + + auto resp = c.call("notifications/roots/list_changed", Json::object()); + assert(resp.contains("acknowledged")); + assert(resp["acknowledged"] == true); + + std::cout << " [PASS] roots notification acknowledged\n"; +} + +void test_multiple_roots_notifications() +{ + std::cout << "Test: multiple roots notifications...\n"; + + auto srv = std::make_shared(); + int count = 0; + + srv->route("notifications/roots/list_changed", + [&count](const Json&) + { + count++; + return Json::object(); + }); + + client::Client c(std::make_unique(srv)); + + c.call("notifications/roots/list_changed", Json::object()); + c.call("notifications/roots/list_changed", Json::object()); + c.call("notifications/roots/list_changed", Json::object()); + + assert(count == 3); + + std::cout << " [PASS] multiple notifications counted\n"; +} + +// ============================================================================ +// TestCancelledNotification - Cancellation handling +// ============================================================================ + +std::shared_ptr create_cancel_server() +{ + auto srv = std::make_shared(); + static std::string cancelled_request_id; + + srv->route("notifications/cancelled", + [](const Json& in) + { + cancelled_request_id = in.value("requestId", ""); + return Json{{"cancelled", true}}; + }); + + srv->route("check_cancelled", + [](const Json&) { return Json{{"lastCancelled", cancelled_request_id}}; }); + + return srv; +} + +void test_cancel_notification() +{ + std::cout << "Test: cancel notification...\n"; + + auto srv = create_cancel_server(); + client::Client c(std::make_unique(srv)); + + auto resp = c.call("notifications/cancelled", Json{{"requestId", "req-123"}}); + assert(resp.contains("cancelled")); + assert(resp["cancelled"] == true); + + std::cout << " [PASS] cancel notification handled\n"; +} + +void test_cancel_with_reason() +{ + std::cout << "Test: cancel with reason...\n"; + + auto srv = std::make_shared(); + std::string received_reason; + + srv->route("notifications/cancelled", + [&received_reason](const Json& in) + { + received_reason = in.value("reason", ""); + return Json::object(); + }); + + client::Client c(std::make_unique(srv)); + + c.call("notifications/cancelled", Json{{"requestId", "req-456"}, {"reason", "User cancelled"}}); + + assert(received_reason == "User cancelled"); + + std::cout << " [PASS] cancel reason received\n"; +} + +// ============================================================================ +// TestLogging - Logging notification handling +// ============================================================================ + +std::shared_ptr create_logging_server() +{ + auto srv = std::make_shared(); + static std::vector log_entries; + + srv->route("logging/setLevel", + [](const Json& in) { return Json{{"level", in.value("level", "info")}}; }); + + srv->route("notifications/message", + [](const Json& in) + { + log_entries.push_back(in); + return Json::object(); + }); + + srv->route("get_logs", [](const Json&) { return Json{{"logs", log_entries}}; }); + + return srv; +} + +void test_set_log_level() +{ + std::cout << "Test: set log level...\n"; + + auto srv = create_logging_server(); + client::Client c(std::make_unique(srv)); + + auto resp = c.call("logging/setLevel", Json{{"level", "debug"}}); + assert(resp["level"] == "debug"); + + std::cout << " [PASS] log level set\n"; +} + +void test_log_message_notification() +{ + std::cout << "Test: log message notification...\n"; + + auto srv = std::make_shared(); + std::string received_message; + std::string received_level; + + srv->route("notifications/message", + [&](const Json& in) + { + received_message = in.value("data", ""); + received_level = in.value("level", ""); + return Json::object(); + }); + + client::Client c(std::make_unique(srv)); + + c.call("notifications/message", + Json{{"level", "warning"}, {"data", "Something happened"}, {"logger", "test"}}); + + assert(received_level == "warning"); + assert(received_message == "Something happened"); + + std::cout << " [PASS] log message received\n"; +} + +// ============================================================================ +// TestImageContent - Image content handling +// ============================================================================ + +std::shared_ptr create_image_server() +{ + auto srv = std::make_shared(); + + srv->route("tools/list", + [](const Json&) + { + return Json{{"tools", Json::array({{{"name", "get_image"}, + {"description", "Get an image"}, + {"inputSchema", {{"type", "object"}}}}})}}; + }); + + srv->route("tools/call", + [](const Json& in) + { + std::string name = in.at("name").get(); + if (name == "get_image") + { + return Json{{"content", Json::array({{{"type", "image"}, + {"data", "iVBORw0KGgo="}, + {"mimeType", "image/png"}}})}, + {"isError", false}}; + } + return Json{{"content", Json::array()}, {"isError", true}}; + }); + + return srv; +} + +void test_image_content_type() +{ + std::cout << "Test: image content type...\n"; + + auto srv = create_image_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("get_image", Json::object()); + assert(!result.isError); + assert(!result.content.empty()); + + // Check raw content has image type + auto raw = c.call("tools/call", Json{{"name", "get_image"}, {"arguments", Json::object()}}); + assert(raw.contains("content")); + assert(raw["content"].size() == 1); + assert(raw["content"][0]["type"] == "image"); + assert(raw["content"][0]["mimeType"] == "image/png"); + + std::cout << " [PASS] image content type preserved\n"; +} + +void test_image_data_base64() +{ + std::cout << "Test: image data base64...\n"; + + auto srv = create_image_server(); + client::Client c(std::make_unique(srv)); + + auto raw = c.call("tools/call", Json{{"name", "get_image"}, {"arguments", Json::object()}}); + assert(raw["content"][0].contains("data")); + assert(raw["content"][0]["data"].is_string()); + // Base64 encoded data starts with known PNG header + std::string data = raw["content"][0]["data"]; + assert(data.length() > 0); + + std::cout << " [PASS] image data is base64\n"; +} + +// ============================================================================ +// TestEmbeddedResource - Embedded resource content +// ============================================================================ + +std::shared_ptr create_embedded_resource_server() +{ + auto srv = std::make_shared(); + + srv->route("tools/list", + [](const Json&) + { + return Json{{"tools", Json::array({{{"name", "with_resource"}, + {"description", "Returns embedded resource"}, + {"inputSchema", {{"type", "object"}}}}})}}; + }); + + srv->route( + "tools/call", + [](const Json& in) + { + std::string name = in.at("name").get(); + if (name == "with_resource") + { + return Json{ + {"content", Json::array({{{"type", "text"}, {"text", "Here is a resource:"}}, + {{"type", "resource"}, + {"resource", + {{"uri", "file:///data.txt"}, + {"mimeType", "text/plain"}, + {"text", "Resource content here"}}}}})}, + {"isError", false}}; + } + return Json{{"content", Json::array()}, {"isError", true}}; + }); + + return srv; +} + +void test_embedded_resource_content() +{ + std::cout << "Test: embedded resource content...\n"; + + auto srv = create_embedded_resource_server(); + client::Client c(std::make_unique(srv)); + + auto raw = c.call("tools/call", Json{{"name", "with_resource"}, {"arguments", Json::object()}}); + assert(raw.contains("content")); + assert(raw["content"].size() == 2); + assert(raw["content"][0]["type"] == "text"); + assert(raw["content"][1]["type"] == "resource"); + + std::cout << " [PASS] embedded resource in content\n"; +} + +void test_embedded_resource_uri() +{ + std::cout << "Test: embedded resource uri...\n"; + + auto srv = create_embedded_resource_server(); + client::Client c(std::make_unique(srv)); + + auto raw = c.call("tools/call", Json{{"name", "with_resource"}, {"arguments", Json::object()}}); + auto resource = raw["content"][1]["resource"]; + assert(resource.contains("uri")); + assert(resource["uri"] == "file:///data.txt"); + assert(resource["text"] == "Resource content here"); + + std::cout << " [PASS] embedded resource uri and text\n"; +} + +void test_embedded_resource_blob() +{ + std::cout << "Test: embedded resource blob...\n"; + + auto srv = std::make_shared(); + srv->route("tools/list", + [](const Json&) + { + return Json{{"tools", Json::array({{{"name", "blob_resource"}, + {"inputSchema", {{"type", "object"}}}}})}}; + }); + srv->route("tools/call", + [](const Json& in) + { + return Json{{"content", Json::array({{{"type", "resource"}, + {"resource", + {{"uri", "file:///binary.dat"}, + {"mimeType", "application/octet-stream"}, + {"blob", "SGVsbG8gV29ybGQ="}}}}})}, + {"isError", false}}; + }); + + client::Client c(std::make_unique(srv)); + auto raw = c.call("tools/call", Json{{"name", "blob_resource"}, {"arguments", Json::object()}}); + auto resource = raw["content"][0]["resource"]; + assert(resource.contains("blob")); + assert(resource["blob"] == "SGVsbG8gV29ybGQ="); + + std::cout << " [PASS] embedded resource blob\n"; +} + +// ============================================================================ +// TestToolInputValidation - Input validation tests +// ============================================================================ + +std::shared_ptr create_validation_server() +{ + auto srv = std::make_shared(); + + srv->route( + "tools/list", + [](const Json&) + { + return Json{ + {"tools", + Json::array( + {{{"name", "require_string"}, + {"inputSchema", + {{"type", "object"}, + {"properties", {{"value", {{"type", "string"}}}}}, + {"required", Json::array({"value"})}}}}, + {{"name", "require_number"}, + {"inputSchema", + {{"type", "object"}, + {"properties", + {{"num", {{"type", "number"}, {"minimum", 0}, {"maximum", 100}}}}}, + {"required", Json::array({"num"})}}}}, + {{"name", "require_enum"}, + {"inputSchema", + {{"type", "object"}, + {"properties", {{"choice", {{"enum", Json::array({"a", "b", "c"})}}}}}, + {"required", Json::array({"choice"})}}}}})}}; + }); + + srv->route( + "tools/call", + [](const Json& in) + { + std::string name = in.at("name").get(); + Json args = in.value("arguments", Json::object()); + + if (name == "require_string") + { + return Json{{"content", Json::array({{{"type", "text"}, {"text", args["value"]}}})}, + {"isError", false}}; + } + if (name == "require_number") + { + return Json{ + {"content", Json::array({{{"type", "text"}, + {"text", std::to_string(args["num"].get())}}})}, + {"isError", false}}; + } + if (name == "require_enum") + { + return Json{ + {"content", Json::array({{{"type", "text"}, {"text", args["choice"]}}})}, + {"isError", false}}; + } + return Json{{"content", Json::array()}, {"isError", true}}; + }); + + return srv; +} + +void test_valid_string_input() +{ + std::cout << "Test: valid string input...\n"; + + auto srv = create_validation_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("require_string", Json{{"value", "hello"}}); + assert(!result.isError); + assert(result.text() == "hello"); + + std::cout << " [PASS] valid string accepted\n"; +} + +void test_valid_number_input() +{ + std::cout << "Test: valid number input...\n"; + + auto srv = create_validation_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("require_number", Json{{"num", 50}}); + assert(!result.isError); + assert(result.text() == "50"); + + std::cout << " [PASS] valid number accepted\n"; +} + +void test_valid_enum_input() +{ + std::cout << "Test: valid enum input...\n"; + + auto srv = create_validation_server(); + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("require_enum", Json{{"choice", "b"}}); + assert(!result.isError); + assert(result.text() == "b"); + + std::cout << " [PASS] valid enum accepted\n"; +} + +// ============================================================================ +// TestResourceSubscribe - Resource subscription +// ============================================================================ + +std::shared_ptr create_subscribe_server() +{ + auto srv = std::make_shared(); + static std::vector subscribed_uris; + + srv->route("resources/subscribe", + [](const Json& in) + { + subscribed_uris.push_back(in["uri"].get()); + return Json{{"subscribed", true}}; + }); + + srv->route("resources/unsubscribe", + [](const Json& in) + { + std::string uri = in["uri"].get(); + subscribed_uris.erase( + std::remove(subscribed_uris.begin(), subscribed_uris.end(), uri), + subscribed_uris.end()); + return Json{{"unsubscribed", true}}; + }); + + srv->route("get_subscriptions", + [](const Json&) + { + Json uris = Json::array(); + for (const auto& u : subscribed_uris) + uris.push_back(u); + return Json{{"subscriptions", uris}}; + }); + + return srv; +} + +void test_resource_subscribe() +{ + std::cout << "Test: resource subscribe...\n"; + + auto srv = create_subscribe_server(); + client::Client c(std::make_unique(srv)); + + auto resp = c.call("resources/subscribe", Json{{"uri", "file:///config.json"}}); + assert(resp["subscribed"] == true); + + std::cout << " [PASS] resource subscribed\n"; +} + +void test_resource_unsubscribe() +{ + std::cout << "Test: resource unsubscribe...\n"; + + auto srv = create_subscribe_server(); + client::Client c(std::make_unique(srv)); + + c.call("resources/subscribe", Json{{"uri", "file:///test.txt"}}); + auto resp = c.call("resources/unsubscribe", Json{{"uri", "file:///test.txt"}}); + assert(resp["unsubscribed"] == true); + + std::cout << " [PASS] resource unsubscribed\n"; +} + +// ============================================================================ +// TestResourceListChanged - Resource list changed notification +// ============================================================================ + +void test_resource_list_changed() +{ + std::cout << "Test: resource list changed notification...\n"; + + auto srv = std::make_shared(); + bool notified = false; + + srv->route("notifications/resources/list_changed", + [¬ified](const Json&) + { + notified = true; + return Json::object(); + }); + + client::Client c(std::make_unique(srv)); + c.call("notifications/resources/list_changed", Json::object()); + + assert(notified); + + std::cout << " [PASS] resource list changed notified\n"; +} + +void test_tool_list_changed() +{ + std::cout << "Test: tool list changed notification...\n"; + + auto srv = std::make_shared(); + bool notified = false; + + srv->route("notifications/tools/list_changed", + [¬ified](const Json&) + { + notified = true; + return Json::object(); + }); + + client::Client c(std::make_unique(srv)); + c.call("notifications/tools/list_changed", Json::object()); + + assert(notified); + + std::cout << " [PASS] tool list changed notified\n"; +} + +void test_prompt_list_changed() +{ + std::cout << "Test: prompt list changed notification...\n"; + + auto srv = std::make_shared(); + bool notified = false; + + srv->route("notifications/prompts/list_changed", + [¬ified](const Json&) + { + notified = true; + return Json::object(); + }); + + client::Client c(std::make_unique(srv)); + c.call("notifications/prompts/list_changed", Json::object()); + + assert(notified); + + std::cout << " [PASS] prompt list changed notified\n"; +} + +// ============================================================================ +// TestCompletionEdgeCases - Completion edge cases +// ============================================================================ + +std::shared_ptr create_completion_edge_server() +{ + auto srv = std::make_shared(); + + srv->route("completion/complete", + [](const Json& in) + { + Json ref = in.at("ref"); + std::string refType = ref.value("type", ""); + + if (refType == "ref/prompt") + { + return Json{ + {"completion", + {{"values", Json::array({"prompt1", "prompt2"})}, {"hasMore", false}}}}; + } + else if (refType == "ref/resource") + { + return Json{{"completion", + {{"values", Json::array({"file:///a.txt", "file:///b.txt"})}, + {"hasMore", true}, + {"total", 10}}}}; + } + return Json{{"completion", {{"values", Json::array()}, {"hasMore", false}}}}; + }); + + return srv; +} + +void test_completion_has_more() +{ + std::cout << "Test: completion hasMore...\n"; + + auto srv = create_completion_edge_server(); + client::Client c(std::make_unique(srv)); + + auto resp = + c.call("completion/complete", Json{{"ref", {{"type", "ref/resource"}, {"uri", "file:///"}}}, + {"argument", {{"name", "uri"}, {"value", "file:///"}}}}); + + assert(resp["completion"]["hasMore"] == true); + assert(resp["completion"]["total"] == 10); + + std::cout << " [PASS] completion hasMore and total\n"; +} + +void test_completion_empty() +{ + std::cout << "Test: completion empty...\n"; + + auto srv = create_completion_edge_server(); + client::Client c(std::make_unique(srv)); + + auto resp = c.call("completion/complete", Json{{"ref", {{"type", "ref/unknown"}}}, + {"argument", {{"name", "x"}, {"value", "y"}}}}); + + assert(resp["completion"]["values"].empty()); + assert(resp["completion"]["hasMore"] == false); + + std::cout << " [PASS] completion empty result\n"; +} + +// ============================================================================ +// TestBatchOperations - Multiple operations in sequence +// ============================================================================ + +void test_batch_tool_calls() +{ + std::cout << "Test: batch tool calls...\n"; + + auto srv = create_interaction_server(); + client::Client c(std::make_unique(srv)); + + // Call multiple tools in sequence (add tool uses x and y) + auto r1 = c.call_tool("add", Json{{"x", 1}, {"y", 2}}); + auto r2 = c.call_tool("add", Json{{"x", 3}, {"y", 4}}); + auto r3 = c.call_tool("add", Json{{"x", 5}, {"y", 6}}); + + assert(r1.text() == "3"); + assert(r2.text() == "7"); + assert(r3.text() == "11"); + + std::cout << " [PASS] batch tool calls succeeded\n"; +} + +void test_mixed_operation_batch() +{ + std::cout << "Test: mixed operation batch...\n"; + + auto srv = std::make_shared(); + + srv->route("tools/list", + [](const Json&) + { + return Json{{"tools", Json::array({{{"name", "echo"}, + {"inputSchema", {{"type", "object"}}}}})}}; + }); + srv->route("tools/call", + [](const Json& in) + { + return Json{{"content", Json::array({{{"type", "text"}, {"text", "echoed"}}})}, + {"isError", false}}; + }); + srv->route( + "resources/list", [](const Json&) + { return Json{{"resources", Json::array({{{"uri", "test://a"}, {"name", "a"}}})}}; }); + srv->route("prompts/list", + [](const Json&) { return Json{{"prompts", Json::array({{{"name", "p1"}}})}}; }); + + client::Client c(std::make_unique(srv)); + + auto tools = c.list_tools(); + auto resources = c.list_resources(); + auto prompts = c.list_prompts(); + auto result = c.call_tool("echo", Json::object()); + + assert(tools.size() == 1); + assert(resources.size() == 1); + assert(prompts.size() == 1); + assert(!result.isError); + + std::cout << " [PASS] mixed operation batch succeeded\n"; +} + +// ============================================================================ +// TestTransportEdgeCases - Transport-related edge cases +// ============================================================================ + +void test_empty_tool_name() +{ + std::cout << "Test: empty tool name...\n"; + + auto srv = create_interaction_server(); + client::Client c(std::make_unique(srv)); + + bool threw = false; + try + { + c.call_tool("", Json::object()); + } + catch (...) + { + threw = true; + } + assert(threw); + + std::cout << " [PASS] empty tool name throws\n"; +} + +void test_whitespace_tool_name() +{ + std::cout << "Test: whitespace tool name...\n"; + + auto srv = create_interaction_server(); + client::Client c(std::make_unique(srv)); + + bool threw = false; + try + { + c.call_tool(" ", Json::object()); + } + catch (...) + { + threw = true; + } + assert(threw); + + std::cout << " [PASS] whitespace tool name throws\n"; +} + +void test_special_chars_tool_name() +{ + std::cout << "Test: special chars in tool name...\n"; + + auto srv = std::make_shared(); + srv->route( + "tools/list", + [](const Json&) + { + return Json{ + {"tools", + Json::array( + {{{"name", "tool-with-dashes"}, {"inputSchema", {{"type", "object"}}}}, + {{"name", "tool_with_underscores"}, {"inputSchema", {{"type", "object"}}}}, + {{"name", "tool.with.dots"}, {"inputSchema", {{"type", "object"}}}}})}}; + }); + srv->route("tools/call", + [](const Json& in) + { + return Json{{"content", Json::array({{{"type", "text"}, {"text", in["name"]}}})}, + {"isError", false}}; + }); + + client::Client c(std::make_unique(srv)); + + auto r1 = c.call_tool("tool-with-dashes", Json::object()); + auto r2 = c.call_tool("tool_with_underscores", Json::object()); + auto r3 = c.call_tool("tool.with.dots", Json::object()); + + assert(r1.text() == "tool-with-dashes"); + assert(r2.text() == "tool_with_underscores"); + assert(r3.text() == "tool.with.dots"); + + std::cout << " [PASS] special chars in tool names work\n"; +} + +void test_five_level_nested_args() +{ + std::cout << "Test: five level nested arguments...\n"; + + auto srv = std::make_shared(); + srv->route("tools/list", + [](const Json&) + { + return Json{{"tools", Json::array({{{"name", "deep"}, + {"inputSchema", {{"type", "object"}}}}})}}; + }); + srv->route("tools/call", + [](const Json& in) + { + Json args = in["arguments"]; + std::string val = args["a"]["b"]["c"]["d"]["e"].get(); + return Json{{"content", Json::array({{{"type", "text"}, {"text", val}}})}, + {"isError", false}}; + }); + + client::Client c(std::make_unique(srv)); + + Json deep_args = {{"a", {{"b", {{"c", {{"d", {{"e", "found"}}}}}}}}}}; + auto result = c.call_tool("deep", deep_args); + assert(result.text() == "found"); + + std::cout << " [PASS] five level nested args handled\n"; +} + +void test_array_of_objects_argument() +{ + std::cout << "Test: array of objects as argument...\n"; + + auto srv = std::make_shared(); + srv->route("tools/list", + [](const Json&) + { + return Json{{"tools", Json::array({{{"name", "process_items"}, + {"inputSchema", {{"type", "object"}}}}})}}; + }); + srv->route("tools/call", + [](const Json& in) + { + Json items = in["arguments"]["items"]; + int sum = 0; + for (const auto& item : items) + sum += item["value"].get(); + return Json{{"content", + Json::array({{{"type", "text"}, {"text", std::to_string(sum)}}})}, + {"isError", false}}; + }); + + client::Client c(std::make_unique(srv)); + + Json items = Json::array( + {{{"id", 1}, {"value", 10}}, {{"id", 2}, {"value", 20}}, {{"id", 3}, {"value", 30}}}); + auto result = c.call_tool("process_items", {{"items", items}}); + assert(result.text() == "60"); + + std::cout << " [PASS] array of objects argument handled\n"; +} + +void test_null_argument() +{ + std::cout << "Test: null argument...\n"; + + auto srv = std::make_shared(); + srv->route("tools/list", + [](const Json&) + { + return Json{{"tools", Json::array({{{"name", "nullable"}, + {"inputSchema", {{"type", "object"}}}}})}}; + }); + srv->route("tools/call", + [](const Json& in) + { + Json args = in["arguments"]; + bool is_null = args["value"].is_null(); + return Json{ + {"content", + Json::array({{{"type", "text"}, {"text", is_null ? "null" : "not null"}}})}, + {"isError", false}}; + }); + + client::Client c(std::make_unique(srv)); + + auto result = c.call_tool("nullable", {{"value", nullptr}}); + assert(result.text() == "null"); + + std::cout << " [PASS] null argument handled\n"; +} + +void test_boolean_argument_coercion() +{ + std::cout << "Test: boolean argument coercion...\n"; + + auto srv = std::make_shared(); + srv->route("tools/list", + [](const Json&) + { + return Json{{"tools", Json::array({{{"name", "bool_tool"}, + {"inputSchema", {{"type", "object"}}}}})}}; + }); + srv->route("tools/call", + [](const Json& in) + { + bool val = in["arguments"]["flag"].get(); + return Json{{"content", Json::array({{{"type", "text"}, + {"text", val ? "true" : "false"}}})}, + {"isError", false}}; + }); + + client::Client c(std::make_unique(srv)); + + auto r1 = c.call_tool("bool_tool", {{"flag", true}}); + auto r2 = c.call_tool("bool_tool", {{"flag", false}}); + + assert(r1.text() == "true"); + assert(r2.text() == "false"); + + std::cout << " [PASS] boolean argument coercion works\n"; +} + +// ============================================================================ +// Main +// ============================================================================ + + +// ============================================================================ +// Main - Part 3 +// ============================================================================ + +int main() +{ + std::cout << "Running server interaction tests (Part 3)...\n\n"; + + try + { + test_tool_meta_custom_fields(); + test_tool_meta_absent(); + test_resource_meta_fields(); + test_call_tool_meta_roundtrip(); + test_server_throws_exception(); + test_empty_content_response(); + test_error_with_content(); + test_read_empty_resource(); + test_read_large_resource(); + test_read_binary_resource(); + test_read_multi_part_resource(); + test_tool_no_description(); + test_tool_long_description(); + test_tool_unicode_description(); + test_tool_complex_schema(); + test_server_protocol_version(); + test_server_info(); + test_server_capabilities(); + test_server_instructions(); + test_ping_response(); + test_progress_in_meta(); + test_progress_notification_route(); + test_progress_with_message(); + test_roots_list(); + test_roots_notification(); + test_multiple_roots_notifications(); + test_cancel_notification(); + test_cancel_with_reason(); + test_set_log_level(); + test_log_message_notification(); + test_image_content_type(); + test_image_data_base64(); + test_embedded_resource_content(); + test_embedded_resource_uri(); + test_embedded_resource_blob(); + test_valid_string_input(); + test_valid_number_input(); + test_valid_enum_input(); + test_resource_subscribe(); + test_resource_unsubscribe(); + test_resource_list_changed(); + test_tool_list_changed(); + test_prompt_list_changed(); + test_completion_has_more(); + test_completion_empty(); + test_batch_tool_calls(); + test_mixed_operation_batch(); + test_empty_tool_name(); + test_whitespace_tool_name(); + test_special_chars_tool_name(); + test_five_level_nested_args(); + test_array_of_objects_argument(); + test_null_argument(); + test_boolean_argument_coercion(); + + std::cout << "\n[OK] Part 3 tests passed! (54 tests)\n"; + return 0; + } + catch (const std::exception& e) + { + std::cerr << "\n[FAIL] Test failed: " << e.what() << "\n"; + return 1; + } +} diff --git a/tests/server/streaming_sse.cpp b/tests/server/streaming_sse.cpp index 8a1bf88..523483d 100644 --- a/tests/server/streaming_sse.cpp +++ b/tests/server/streaming_sse.cpp @@ -13,11 +13,16 @@ using fastmcpp::Json; using fastmcpp::server::SseServerWrapper; +struct ParsedEndpoint +{ + std::string path; + bool seen = false; +}; + int main() { // Echo handler: returns posted JSON unchanged auto handler = [](const Json& request) -> Json { return request; }; - // Choose port with fallback range int port = -1; std::unique_ptr server; @@ -44,6 +49,7 @@ int main() // Start SSE receiver std::atomic sse_connected{false}; + ParsedEndpoint endpoint; std::vector seen; std::mutex seen_mutex; @@ -58,29 +64,44 @@ int main() { sse_connected = true; std::string chunk(data, len); + + // Parse endpoint event + auto evt_pos = chunk.find("event: endpoint"); + if (evt_pos != std::string::npos) + { + auto data_pos = chunk.find("data: ", evt_pos); + if (data_pos != std::string::npos) + { + auto end_line = chunk.find('\n', data_pos); + endpoint.path = chunk.substr(data_pos + 6, end_line - (data_pos + 6)); + endpoint.seen = true; + } + } + // Parse "data: {json}\n\n" blocks - if (chunk.find("data: ") == 0) + auto data_only_pos = chunk.find("data: "); + while (data_only_pos != std::string::npos) { - size_t start = 6; - size_t end = chunk.find("\n\n"); - if (end != std::string::npos) + size_t start = data_only_pos + 6; + size_t end = chunk.find("\n\n", start); + if (end == std::string::npos) + break; + std::string json_str = chunk.substr(start, end - start); + try { - std::string json_str = chunk.substr(start, end - start); - try - { - Json j = Json::parse(json_str); - if (j.contains("n")) - { - std::lock_guard lock(seen_mutex); - seen.push_back(j["n"].get()); - if (seen.size() >= 3) - return false; // stop after 3 - } - } - catch (...) + Json j = Json::parse(json_str); + if (j.contains("n")) { + std::lock_guard lock(seen_mutex); + seen.push_back(j["n"].get()); + if (seen.size() >= 3) + return false; // stop after 3 } } + catch (...) + { + } + data_only_pos = chunk.find("data: ", end + 2); } return true; }; @@ -111,10 +132,11 @@ int main() // Post three messages httplib::Client post("127.0.0.1", port); + std::string target_path = endpoint.seen ? endpoint.path : "/messages"; for (int i = 1; i <= 3; ++i) { Json j = Json{{"n", i}}; - auto res = post.Post("/messages", j.dump(), "application/json"); + auto res = post.Post(target_path.c_str(), j.dump(), "application/json"); if (!res || res->status != 200) { server->stop();