diff --git a/CMakeLists.txt b/CMakeLists.txt index 5afdf7e..b09cf5d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -293,6 +293,10 @@ if(FASTMCPP_BUILD_TESTS) target_link_libraries(fastmcpp_client_api_advanced PRIVATE fastmcpp_core) add_test(NAME fastmcpp_client_api_advanced COMMAND fastmcpp_client_api_advanced) + add_executable(fastmcpp_client_api_icons tests/client/api_icons.cpp) + target_link_libraries(fastmcpp_client_api_icons PRIVATE fastmcpp_core) + add_test(NAME fastmcpp_client_api_icons COMMAND fastmcpp_client_api_icons) + add_executable(fastmcpp_server_middleware tests/server/middleware.cpp) target_link_libraries(fastmcpp_server_middleware PRIVATE fastmcpp_core) add_test(NAME fastmcpp_server_middleware COMMAND fastmcpp_server_middleware) diff --git a/examples/context_introspection.cpp b/examples/context_introspection.cpp index ce493a4..c5cc27a 100644 --- a/examples/context_introspection.cpp +++ b/examples/context_introspection.cpp @@ -27,16 +27,28 @@ int main() resources::ResourceManager resource_mgr; // Register some sample resources - resources::Resource doc1{Id{"file://docs/readme.txt"}, resources::Kind::File, - Json{{"description", "Project README"}, {"size", 1024}}}; + resources::Resource doc1; + doc1.uri = "file://docs/readme.txt"; + doc1.name = "readme.txt"; + doc1.id = Id{"file://docs/readme.txt"}; + doc1.kind = resources::Kind::File; + doc1.metadata = Json{{"description", "Project README"}, {"size", 1024}}; resource_mgr.register_resource(doc1); - resources::Resource doc2{Id{"file://docs/api.txt"}, resources::Kind::File, - Json{{"description", "API Documentation"}, {"size", 2048}}}; + resources::Resource doc2; + doc2.uri = "file://docs/api.txt"; + doc2.name = "api.txt"; + doc2.id = Id{"file://docs/api.txt"}; + doc2.kind = resources::Kind::File; + doc2.metadata = Json{{"description", "API Documentation"}, {"size", 2048}}; resource_mgr.register_resource(doc2); - resources::Resource config{Id{"config://app.json"}, resources::Kind::Json, - Json{{"description", "Application config"}}}; + resources::Resource config; + config.uri = "config://app.json"; + config.name = "app.json"; + config.id = Id{"config://app.json"}; + config.kind = resources::Kind::Json; + config.metadata = Json{{"description", "Application config"}}; resource_mgr.register_resource(config); // ============================================================================ @@ -71,9 +83,9 @@ int main() std::cout << "\n2. Listing Prompts:\n"; std::cout << " " << std::string(40, '-') << "\n"; auto prompts = ctx.list_prompts(); - for (const auto& [name, prompt] : prompts) + for (const auto& prompt : prompts) { - std::cout << " - Name: " << name << "\n"; + std::cout << " - Name: " << prompt.name << "\n"; std::cout << " Template: " << prompt.template_string() << "\n\n"; } diff --git a/examples/tool_injection_middleware.cpp b/examples/tool_injection_middleware.cpp index 5d5e98a..074abe6 100644 --- a/examples/tool_injection_middleware.cpp +++ b/examples/tool_injection_middleware.cpp @@ -28,13 +28,21 @@ int main() prompts::PromptManager prompt_mgr; // Register some resources - resource_mgr.register_resource(resources::Resource{Id{"file://docs/readme.md"}, - resources::Kind::File, - Json{{"description", "Project README"}}}); - - resource_mgr.register_resource(resources::Resource{Id{"file://docs/api.md"}, - resources::Kind::File, - Json{{"description", "API Documentation"}}}); + resources::Resource res1; + res1.uri = "file://docs/readme.md"; + res1.name = "readme.md"; + res1.id = Id{"file://docs/readme.md"}; + res1.kind = resources::Kind::File; + res1.metadata = Json{{"description", "Project README"}}; + resource_mgr.register_resource(res1); + + resources::Resource res2; + res2.uri = "file://docs/api.md"; + res2.name = "api.md"; + res2.id = Id{"file://docs/api.md"}; + res2.kind = resources::Kind::File; + res2.metadata = Json{{"description", "API Documentation"}}; + resource_mgr.register_resource(res2); // Register some prompts prompt_mgr.add("greeting", prompts::Prompt("Hello {{name}}!")); diff --git a/include/fastmcpp/client/client.hpp b/include/fastmcpp/client/client.hpp index 775106a..42825e6 100644 --- a/include/fastmcpp/client/client.hpp +++ b/include/fastmcpp/client/client.hpp @@ -731,6 +731,28 @@ class Client rt.mimeType = r["mimeType"].get(); if (r.contains("annotations")) rt.annotations = r["annotations"]; + if (r.contains("title")) + rt.title = r["title"].get(); + if (r.contains("icons")) + { + std::vector icons; + for (const auto& icon : r["icons"]) + { + fastmcpp::Icon i; + i.src = icon.at("src").get(); + if (icon.contains("mimeType")) + i.mime_type = icon["mimeType"].get(); + if (icon.contains("sizes")) + { + std::vector sizes; + for (const auto& s : icon["sizes"]) + sizes.push_back(s.get()); + i.sizes = sizes; + } + icons.push_back(i); + } + rt.icons = icons; + } result.resourceTemplates.push_back(rt); } } @@ -790,6 +812,11 @@ class Client tc.text = m["content"].get(); msg.content.push_back(tc); } + else if (m["content"].is_object()) + { + // Handle single content object (Python fastmcp format) + msg.content.push_back(parse_content_block(m["content"])); + } } result.messages.push_back(msg); } diff --git a/include/fastmcpp/client/transports.hpp b/include/fastmcpp/client/transports.hpp index 3b073a8..98ce5e0 100644 --- a/include/fastmcpp/client/transports.hpp +++ b/include/fastmcpp/client/transports.hpp @@ -2,10 +2,17 @@ #include "fastmcpp/client/client.hpp" #include "fastmcpp/types.hpp" +#include +#include #include +#include +#include +#include #include #include #include +#include +#include #include namespace fastmcpp::client @@ -82,4 +89,52 @@ class StdioTransport : public ITransport std::ostream* log_stream_ = nullptr; }; +/// SSE client transport for connecting to MCP servers using Server-Sent Events protocol. +/// This transport is compatible with Python fastmcp servers and other SSE-based MCP servers. +/// +/// The SSE protocol works as follows: +/// 1. Client connects to /sse endpoint (GET) to establish event stream +/// 2. Client sends JSON-RPC requests to /messages endpoint (POST) +/// 3. Server sends JSON-RPC responses back via the SSE stream +class SseClientTransport : public ITransport +{ + public: + /// Construct an SSE client transport + /// @param base_url The base URL of the MCP server (e.g., "http://127.0.0.1:8766") + /// Will connect to {base_url}/sse and post to {base_url}/messages + /// @param sse_path Path for SSE endpoint (default: "/sse") + /// @param messages_path Path for message endpoint (default: "/messages") + explicit SseClientTransport(std::string base_url, std::string sse_path = "/sse", + std::string messages_path = "/messages"); + + ~SseClientTransport(); + + /// Send a JSON-RPC request and wait for response + fastmcpp::Json request(const std::string& route, const fastmcpp::Json& payload) override; + + /// Check if connected to SSE stream + bool is_connected() const; + + private: + void start_sse_listener(); + void stop_sse_listener(); + void process_sse_event(const fastmcpp::Json& event); + + std::string base_url_; + std::string sse_path_; + std::string messages_path_; + std::string endpoint_path_; // Endpoint path from SSE with session_id + + // SSE listener thread and state + std::unique_ptr sse_thread_; + std::atomic running_{false}; + std::atomic connected_{false}; + + // Request/response matching + std::atomic next_id_{1}; + std::mutex pending_mutex_; + std::condition_variable pending_cv_; + std::unordered_map> pending_requests_; +}; + } // namespace fastmcpp::client diff --git a/include/fastmcpp/client/types.hpp b/include/fastmcpp/client/types.hpp index 32e065e..368d913 100644 --- a/include/fastmcpp/client/types.hpp +++ b/include/fastmcpp/client/types.hpp @@ -56,9 +56,11 @@ using ContentBlock = std::variant title; ///< Human-readable title std::optional description; - fastmcpp::Json inputSchema; ///< JSON Schema for tool input - std::optional outputSchema; ///< JSON Schema for structured output + fastmcpp::Json inputSchema; ///< JSON Schema for tool input + std::optional outputSchema; ///< JSON Schema for structured output + std::optional> icons; ///< Icons for UI display }; /// Result of tools/list request @@ -120,9 +122,11 @@ struct ResourceInfo { std::string uri; std::string name; + std::optional title; ///< Human-readable title std::optional description; std::optional mimeType; std::optional annotations; + std::optional> icons; ///< Icons for UI display }; /// Resource template information @@ -131,9 +135,11 @@ struct ResourceTemplate { std::string uriTemplate; std::string name; + std::optional title; ///< Human-readable title std::optional description; std::optional mimeType; std::optional annotations; + std::optional> icons; ///< Icons for UI display }; /// Text resource content @@ -195,8 +201,10 @@ struct PromptArgument struct PromptInfo { std::string name; + std::optional title; ///< Human-readable title std::optional description; std::optional> arguments; + std::optional> icons; ///< Icons for UI display }; /// Prompt message role @@ -309,48 +317,97 @@ inline void from_json(const fastmcpp::Json& j, ImageContent& c) inline void to_json(fastmcpp::Json& j, const ToolInfo& t) { j = fastmcpp::Json{{"name", t.name}, {"inputSchema", t.inputSchema}}; + if (t.title) + j["title"] = *t.title; if (t.description) j["description"] = *t.description; if (t.outputSchema) j["outputSchema"] = *t.outputSchema; + if (t.icons) + j["icons"] = *t.icons; } inline void from_json(const fastmcpp::Json& j, ToolInfo& t) { t.name = j.at("name").get(); + if (j.contains("title")) + t.title = j["title"].get(); if (j.contains("description")) t.description = j["description"].get(); t.inputSchema = j.value("inputSchema", fastmcpp::Json::object()); if (j.contains("outputSchema")) t.outputSchema = j["outputSchema"]; + if (j.contains("icons")) + t.icons = j["icons"].get>(); } inline void to_json(fastmcpp::Json& j, const ResourceInfo& r) { j = fastmcpp::Json{{"uri", r.uri}, {"name", r.name}}; + if (r.title) + j["title"] = *r.title; if (r.description) j["description"] = *r.description; if (r.mimeType) j["mimeType"] = *r.mimeType; if (r.annotations) j["annotations"] = *r.annotations; + if (r.icons) + j["icons"] = *r.icons; } inline void from_json(const fastmcpp::Json& j, ResourceInfo& r) { r.uri = j.at("uri").get(); r.name = j.at("name").get(); + if (j.contains("title")) + r.title = j["title"].get(); if (j.contains("description")) r.description = j["description"].get(); if (j.contains("mimeType")) r.mimeType = j["mimeType"].get(); if (j.contains("annotations")) r.annotations = j["annotations"]; + if (j.contains("icons")) + r.icons = j["icons"].get>(); +} + +inline void to_json(fastmcpp::Json& j, const ResourceTemplate& t) +{ + j = fastmcpp::Json{{"uriTemplate", t.uriTemplate}, {"name", t.name}}; + if (t.title) + j["title"] = *t.title; + if (t.description) + j["description"] = *t.description; + if (t.mimeType) + j["mimeType"] = *t.mimeType; + if (t.annotations) + j["annotations"] = *t.annotations; + if (t.icons) + j["icons"] = *t.icons; +} + +inline void from_json(const fastmcpp::Json& j, ResourceTemplate& t) +{ + t.uriTemplate = j.at("uriTemplate").get(); + t.name = j.at("name").get(); + if (j.contains("title")) + t.title = j["title"].get(); + if (j.contains("description")) + t.description = j["description"].get(); + if (j.contains("mimeType")) + t.mimeType = j["mimeType"].get(); + if (j.contains("annotations")) + t.annotations = j["annotations"]; + if (j.contains("icons")) + t.icons = j["icons"].get>(); } inline void to_json(fastmcpp::Json& j, const PromptInfo& p) { j = fastmcpp::Json{{"name", p.name}}; + if (p.title) + j["title"] = *p.title; if (p.description) j["description"] = *p.description; if (p.arguments) @@ -364,11 +421,15 @@ inline void to_json(fastmcpp::Json& j, const PromptInfo& p) j["arguments"].push_back(argJson); } } + if (p.icons) + j["icons"] = *p.icons; } inline void from_json(const fastmcpp::Json& j, PromptInfo& p) { p.name = j.at("name").get(); + if (j.contains("title")) + p.title = j["title"].get(); if (j.contains("description")) p.description = j["description"].get(); if (j.contains("arguments")) @@ -384,6 +445,8 @@ inline void from_json(const fastmcpp::Json& j, PromptInfo& p) p.arguments->push_back(arg); } } + if (j.contains("icons")) + p.icons = j["icons"].get>(); } inline void from_json(const fastmcpp::Json& j, TextResourceContent& c) diff --git a/include/fastmcpp/mcp/handler.hpp b/include/fastmcpp/mcp/handler.hpp index b03168a..b3bd469 100644 --- a/include/fastmcpp/mcp/handler.hpp +++ b/include/fastmcpp/mcp/handler.hpp @@ -1,4 +1,6 @@ #pragma once +#include "fastmcpp/prompts/manager.hpp" +#include "fastmcpp/resources/manager.hpp" #include "fastmcpp/server/server.hpp" #include "fastmcpp/tools/manager.hpp" #include "fastmcpp/types.hpp" @@ -35,4 +37,11 @@ make_mcp_handler(const std::string& server_name, const std::string& version, const server::Server& server, const tools::ToolManager& tools, const std::unordered_map& descriptions = {}); +// Full MCP handler with tools, resources, and prompts support +std::function +make_mcp_handler(const std::string& server_name, const std::string& version, + const server::Server& server, const tools::ToolManager& tools, + const resources::ResourceManager& resources, const prompts::PromptManager& prompts, + const std::unordered_map& descriptions = {}); + } // namespace fastmcpp::mcp diff --git a/include/fastmcpp/prompts/manager.hpp b/include/fastmcpp/prompts/manager.hpp index aadc200..d581c2f 100644 --- a/include/fastmcpp/prompts/manager.hpp +++ b/include/fastmcpp/prompts/manager.hpp @@ -1,4 +1,5 @@ #pragma once +#include "fastmcpp/exceptions.hpp" #include "fastmcpp/prompts/prompt.hpp" #include @@ -14,27 +15,48 @@ class PromptManager public: void add(const std::string& name, const Prompt& p) { - prompts_[name] = p; + Prompt stored = p; + stored.name = name; + prompts_[name] = stored; } + + void register_prompt(const Prompt& p) + { + prompts_[p.name] = p; + } + const Prompt& get(const std::string& name) const { - return prompts_.at(name); + auto it = prompts_.find(name); + if (it == prompts_.end()) + throw NotFoundError("Prompt not found: " + name); + return it->second; } + bool has(const std::string& name) const { return prompts_.count(name) > 0; } - // List all prompts (v2.13.0+) - std::vector> list() const + std::vector list() const { - std::vector> result; + std::vector result; result.reserve(prompts_.size()); - for (const auto& kv : prompts_) - result.push_back(kv); + for (const auto& [name, prompt] : prompts_) + result.push_back(prompt); return result; } + std::vector render(const std::string& name, + const Json& args = Json::object()) const + { + const auto& prompt = get(name); + if (prompt.generator) + return prompt.generator(args); + // Legacy: use template rendering + return {{{"user", prompt.template_string()}}}; + } + private: std::unordered_map prompts_; }; diff --git a/include/fastmcpp/prompts/prompt.hpp b/include/fastmcpp/prompts/prompt.hpp index 51a9c01..f793a1e 100644 --- a/include/fastmcpp/prompts/prompt.hpp +++ b/include/fastmcpp/prompts/prompt.hpp @@ -1,15 +1,42 @@ #pragma once +#include "fastmcpp/types.hpp" + +#include +#include #include #include +#include namespace fastmcpp::prompts { -class Prompt +/// MCP Prompt argument definition +struct PromptArgument { - public: + std::string name; + std::optional description; + bool required{false}; +}; + +/// MCP Prompt message +struct PromptMessage +{ + std::string role; // "user", "assistant", "system" + std::string content; // Message content +}; + +/// MCP Prompt definition +struct Prompt +{ + std::string name; + std::optional description; + std::vector arguments; + std::function(const Json&)> generator; // Message generator + + // Legacy constructor for backwards compatibility Prompt() = default; explicit Prompt(std::string tmpl) : tmpl_(std::move(tmpl)) {} + const std::string& template_string() const { return tmpl_; @@ -17,7 +44,7 @@ class Prompt std::string render(const std::unordered_map& vars) const; private: - std::string tmpl_; + std::string tmpl_; // Legacy template string }; } // namespace fastmcpp::prompts diff --git a/include/fastmcpp/resources/manager.hpp b/include/fastmcpp/resources/manager.hpp index 2accd4d..436d70b 100644 --- a/include/fastmcpp/resources/manager.hpp +++ b/include/fastmcpp/resources/manager.hpp @@ -12,12 +12,44 @@ namespace fastmcpp::resources class ResourceManager { public: - void register_resource(const Resource& res); - const Resource& get(const std::string& id) const; // throws NotFoundError - std::vector list() const; + void register_resource(const Resource& res) + { + by_uri_[res.uri] = res; + } + + const Resource& get(const std::string& uri) const + { + auto it = by_uri_.find(uri); + if (it == by_uri_.end()) + throw NotFoundError("Resource not found: " + uri); + return it->second; + } + + bool has(const std::string& uri) const + { + return by_uri_.count(uri) > 0; + } + + std::vector list() const + { + std::vector result; + result.reserve(by_uri_.size()); + for (const auto& [uri, res] : by_uri_) + result.push_back(res); + return result; + } + + ResourceContent read(const std::string& uri, const Json& params = Json::object()) const + { + const auto& res = get(uri); + if (res.provider) + return res.provider(params); + // Default: return empty content + return ResourceContent{uri, res.mime_type, std::string{}}; + } private: - std::unordered_map by_id_; + std::unordered_map by_uri_; }; } // namespace fastmcpp::resources diff --git a/include/fastmcpp/resources/resource.hpp b/include/fastmcpp/resources/resource.hpp index f2256d1..affa2c2 100644 --- a/include/fastmcpp/resources/resource.hpp +++ b/include/fastmcpp/resources/resource.hpp @@ -2,17 +2,35 @@ #include "fastmcpp/resources/types.hpp" #include "fastmcpp/types.hpp" +#include #include #include +#include namespace fastmcpp::resources { +/// Content returned by a resource read operation +struct ResourceContent +{ + std::string uri; + std::optional mime_type; + std::variant> data; // text or binary +}; + +/// MCP Resource definition struct Resource { + std::string uri; // e.g., "file://readme.txt" + std::string name; // Human-readable name + std::optional description; // Optional description + std::optional mime_type; // MIME type hint + std::function provider; // Content provider function + + // Legacy fields (for backwards compatibility) fastmcpp::Id id; Kind kind{Kind::Unknown}; - fastmcpp::Json metadata; // arbitrary metadata + fastmcpp::Json metadata; }; } // namespace fastmcpp::resources diff --git a/include/fastmcpp/server/context.hpp b/include/fastmcpp/server/context.hpp index 27b35e2..387bd29 100644 --- a/include/fastmcpp/server/context.hpp +++ b/include/fastmcpp/server/context.hpp @@ -67,8 +67,8 @@ class Context std::vector list_resources() const; /// List all available prompts from the server - /// @return Vector of (name, Prompt) pairs - std::vector> list_prompts() const; + /// @return Vector of Prompt objects (each contains its name) + std::vector list_prompts() const; /// Get a prompt by name and render it with optional arguments /// @param name The name of the prompt to retrieve diff --git a/include/fastmcpp/tools/tool.hpp b/include/fastmcpp/tools/tool.hpp index 20c9645..f16a2e1 100644 --- a/include/fastmcpp/tools/tool.hpp +++ b/include/fastmcpp/tools/tool.hpp @@ -2,6 +2,7 @@ #include "fastmcpp/types.hpp" #include +#include #include #include @@ -14,6 +15,8 @@ class Tool using Fn = std::function; Tool() = default; + + // Original constructor (backward compatible) Tool(std::string name, fastmcpp::Json input_schema, fastmcpp::Json output_schema, Fn fn, std::vector exclude_args = {}) : name_(std::move(name)), input_schema_(std::move(input_schema)), @@ -22,10 +25,33 @@ class Tool { } + // Extended constructor with title, description, icons + Tool(std::string name, fastmcpp::Json input_schema, fastmcpp::Json output_schema, Fn fn, + std::optional title, std::optional description, + std::optional> icons, + std::vector exclude_args = {}) + : name_(std::move(name)), title_(std::move(title)), description_(std::move(description)), + input_schema_(std::move(input_schema)), output_schema_(std::move(output_schema)), + icons_(std::move(icons)), fn_(std::move(fn)), exclude_args_(std::move(exclude_args)) + { + } + const std::string& name() const { return name_; } + const std::optional& title() const + { + return title_; + } + const std::optional& description() const + { + return description_; + } + const std::optional>& icons() const + { + return icons_; + } fastmcpp::Json input_schema() const { if (exclude_args_.empty()) @@ -41,6 +67,23 @@ class Tool return fn_(input); } + // Setters for optional fields (builder pattern) + Tool& set_title(std::string title) + { + title_ = std::move(title); + return *this; + } + Tool& set_description(std::string desc) + { + description_ = std::move(desc); + return *this; + } + Tool& set_icons(std::vector icons) + { + icons_ = std::move(icons); + return *this; + } + private: fastmcpp::Json prune_schema(const fastmcpp::Json& schema) const { @@ -76,8 +119,11 @@ class Tool } std::string name_; + std::optional title_; + std::optional description_; fastmcpp::Json input_schema_; fastmcpp::Json output_schema_; + std::optional> icons_; Fn fn_; std::vector exclude_args_; }; diff --git a/src/client/transports.cpp b/src/client/transports.cpp index a349083..6f7dc40 100644 --- a/src/client/transports.cpp +++ b/src/client/transports.cpp @@ -526,4 +526,288 @@ fastmcpp::Json StdioTransport::request(const std::string& route, const fastmcpp: #endif } +// ============================================================================= +// SseClientTransport implementation +// ============================================================================= + +SseClientTransport::SseClientTransport(std::string base_url, std::string sse_path, + std::string messages_path) + : base_url_(std::move(base_url)), sse_path_(std::move(sse_path)), + messages_path_(std::move(messages_path)) +{ + start_sse_listener(); +} + +SseClientTransport::~SseClientTransport() +{ + stop_sse_listener(); +} + +bool SseClientTransport::is_connected() const +{ + return connected_.load(std::memory_order_acquire); +} + +void SseClientTransport::start_sse_listener() +{ + running_.store(true, std::memory_order_release); + + sse_thread_ = std::make_unique( + [this]() + { + auto url = parse_url(base_url_); + + // Use two-argument constructor for better Windows compatibility + httplib::Client cli(url.host.c_str(), url.port); + cli.set_connection_timeout(10, 0); + cli.set_read_timeout(300, 0); // Long timeout for SSE stream (5 minutes) + cli.set_keep_alive(true); + + std::string buffer; + auto content_receiver = [this, &buffer](const char* data, size_t len) + { + if (!running_.load(std::memory_order_acquire)) + return false; + + buffer.append(data, len); + + // Parse SSE events (data: lines separated by double newlines) + size_t pos = 0; + while (true) + { + // Try both \n\n (Unix) and \r\n\r\n (Windows/HTTP) for compatibility + size_t sep = buffer.find("\n\n", pos); + int sep_len = 2; + if (sep == std::string::npos) + { + sep = buffer.find("\r\n\r\n", pos); + sep_len = 4; + } + if (sep == std::string::npos) + break; + + std::string chunk = buffer.substr(pos, sep - pos); + pos = sep + sep_len; + + // Extract event type and data lines + std::string event_type; + std::string aggregated; + size_t line_start = 0; + while (line_start < chunk.size()) + { + size_t line_end = chunk.find('\n', line_start); + std::string line = chunk.substr(line_start, line_end == std::string::npos + ? std::string::npos + : (line_end - line_start)); + // Strip trailing \r for CRLF line endings + if (!line.empty() && line.back() == '\r') + line.pop_back(); + + if (line.rfind("event:", 0) == 0) + { + event_type = line.substr(6); + if (!event_type.empty() && event_type[0] == ' ') + event_type.erase(0, 1); + } + else if (line.rfind("data:", 0) == 0) + { + std::string data_part = line.substr(5); + if (!data_part.empty() && data_part[0] == ' ') + data_part.erase(0, 1); + aggregated += data_part; + } + if (line_end == std::string::npos) + break; + line_start = line_end + 1; + } + + if (!aggregated.empty()) + { + // Handle endpoint event specially - it's not JSON + if (event_type == "endpoint") + { + endpoint_path_ = aggregated; + } + else + { + // Try to parse as JSON for other events + try + { + auto evt = fastmcpp::util::json::parse(aggregated); + process_sse_event(evt); + } + catch (...) + { + // Ignore parse errors + } + } + } + } + + if (pos > 0) + buffer.erase(0, pos); + + return running_.load(std::memory_order_acquire); + }; + + auto response_handler = [this](const httplib::Response& r) + { + if (r.status >= 200 && r.status < 300) + { + connected_.store(true, std::memory_order_release); + return true; + } + return false; + }; + + // Try to connect with retries + httplib::Headers headers = {{"Accept", "text/event-stream"}}; + for (int attempt = 0; attempt < 50 && running_.load(std::memory_order_acquire); + ++attempt) + { + auto res = cli.Get(sse_path_.c_str(), headers, response_handler, content_receiver); + if (res) + break; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + connected_.store(false, std::memory_order_release); + }); + + // Wait for connection to establish + for (int i = 0; i < 50 && !is_connected(); ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); +} + +void SseClientTransport::stop_sse_listener() +{ + running_.store(false, std::memory_order_release); + + // Wake up any pending requests + { + std::lock_guard lock(pending_mutex_); + for (auto& [id, promise] : pending_requests_) + { + try + { + promise.set_exception( + std::make_exception_ptr(fastmcpp::TransportError("SSE connection closed"))); + } + catch (...) + { + } + } + pending_requests_.clear(); + } + pending_cv_.notify_all(); + + if (sse_thread_ && sse_thread_->joinable()) + sse_thread_->join(); +} + +void SseClientTransport::process_sse_event(const fastmcpp::Json& event) +{ + // Check if this is a JSON-RPC response with an ID + if (!event.contains("id")) + return; + + auto id_val = event.at("id"); + int64_t id = 0; + if (id_val.is_number()) + id = id_val.get(); + else if (id_val.is_string()) + { + try + { + id = std::stoll(id_val.get()); + } + catch (...) + { + return; + } + } + else + return; + + std::lock_guard lock(pending_mutex_); + auto it = pending_requests_.find(id); + if (it != pending_requests_.end()) + { + it->second.set_value(event); + pending_requests_.erase(it); + pending_cv_.notify_all(); + } +} + +fastmcpp::Json SseClientTransport::request(const std::string& route, const fastmcpp::Json& payload) +{ + if (!is_connected()) + throw fastmcpp::TransportError("SSE client not connected"); + + // Generate unique request ID + int64_t id = next_id_.fetch_add(1, std::memory_order_relaxed); + + // Build JSON-RPC request + fastmcpp::Json rpc_request = { + {"jsonrpc", "2.0"}, {"method", route}, {"params", payload}, {"id", id}}; + + // Create promise for response + std::promise response_promise; + std::future response_future = response_promise.get_future(); + + { + std::lock_guard lock(pending_mutex_); + pending_requests_[id] = std::move(response_promise); + } + + // Send request via POST to /messages with session_id + auto url = parse_url(base_url_); + httplib::Client cli(url.host.c_str(), url.port); + cli.set_connection_timeout(5, 0); + cli.set_read_timeout(30, 0); + + // Use the endpoint path from SSE if available, otherwise use default + std::string post_path = endpoint_path_.empty() ? messages_path_ : endpoint_path_; + auto res = cli.Post(post_path.c_str(), rpc_request.dump(), "application/json"); + if (!res) + { + std::lock_guard lock(pending_mutex_); + pending_requests_.erase(id); + throw fastmcpp::TransportError("Failed to send request to " + messages_path_); + } + if (res->status < 200 || res->status >= 300) + { + std::lock_guard lock(pending_mutex_); + pending_requests_.erase(id); + throw fastmcpp::TransportError("HTTP error: " + std::to_string(res->status)); + } + + // Wait for response from SSE stream (with timeout) + auto status = response_future.wait_for(std::chrono::seconds(30)); + if (status == std::future_status::timeout) + { + std::lock_guard lock(pending_mutex_); + pending_requests_.erase(id); + throw fastmcpp::TransportError("Request timeout waiting for SSE response"); + } + + auto rpc_response = response_future.get(); + + // Unwrap JSON-RPC response envelope + // Response format: {"jsonrpc":"2.0","id":...,"result":{...}} or + // {"jsonrpc":"2.0","id":...,"error":{...}} + if (rpc_response.contains("error")) + { + auto error = rpc_response["error"]; + std::string message = error.value("message", "Unknown error"); + throw fastmcpp::TransportError("JSON-RPC error: " + message); + } + + if (rpc_response.contains("result")) + return rpc_response["result"]; + + // If no result or error, return empty object (shouldn't happen with well-formed JSON-RPC) + return fastmcpp::Json::object(); +} + } // namespace fastmcpp::client diff --git a/src/mcp/handler.cpp b/src/mcp/handler.cpp index fa41f7c..f8ca293 100644 --- a/src/mcp/handler.cpp +++ b/src/mcp/handler.cpp @@ -1,5 +1,7 @@ #include "fastmcpp/mcp/handler.hpp" +#include + namespace fastmcpp::mcp { @@ -10,18 +12,39 @@ static fastmcpp::Json jsonrpc_error(const fastmcpp::Json& id, int code, const st {"error", fastmcpp::Json{{"code", code}, {"message", message}}}}; } -static fastmcpp::Json make_tool_entry(const std::string& name, const std::string& description, - const fastmcpp::Json& schema) +static fastmcpp::Json +make_tool_entry(const std::string& name, const std::string& description, + const fastmcpp::Json& schema, + const std::optional& title = std::nullopt, + const std::optional>& icons = std::nullopt) { fastmcpp::Json entry = { {"name", name}, - {"description", description}, }; + if (title) + entry["title"] = *title; + if (!description.empty()) + entry["description"] = description; // Schema may be empty if (!schema.is_null() && !schema.empty()) entry["inputSchema"] = schema; else entry["inputSchema"] = fastmcpp::Json::object(); + // Add icons if present + if (icons && !icons->empty()) + { + fastmcpp::Json icons_json = fastmcpp::Json::array(); + for (const auto& icon : *icons) + { + fastmcpp::Json icon_obj = {{"src", icon.src}}; + if (icon.mime_type) + icon_obj["mimeType"] = *icon.mime_type; + if (icon.sizes) + icon_obj["sizes"] = *icon.sizes; + icons_json.push_back(icon_obj); + } + entry["icons"] = icons_json; + } return entry; } @@ -59,6 +82,9 @@ make_mcp_handler(const std::string& server_name, const std::string& version, fastmcpp::Json tools_array = fastmcpp::Json::array(); for (auto& name : tools.list_names()) { + // Get full tool object to access all fields + const auto& tool = tools.get(name); + fastmcpp::Json schema = fastmcpp::Json::object(); auto it = input_schemas_override.find(name); if (it != input_schemas_override.end()) @@ -69,7 +95,7 @@ make_mcp_handler(const std::string& server_name, const std::string& version, { try { - schema = tools.input_schema_for(name); + schema = tool.input_schema(); } catch (...) { @@ -77,11 +103,16 @@ make_mcp_handler(const std::string& server_name, const std::string& version, } } + // Get description from override map or from tool std::string desc = ""; auto dit = descriptions.find(name); if (dit != descriptions.end()) desc = dit->second; - tools_array.push_back(make_tool_entry(name, desc, schema)); + else if (tool.description()) + desc = *tool.description(); + + tools_array.push_back( + make_tool_entry(name, desc, schema, tool.title(), tool.icons())); } return fastmcpp::Json{{"jsonrpc", "2.0"}, @@ -382,7 +413,7 @@ make_mcp_handler(const std::string& server_name, const std::string& version, const std::unordered_map& descriptions) { // Build meta vector from ToolManager - std::vector> meta; + std::vector> tools_meta; for (const auto& name : tools.list_names()) { fastmcpp::Json schema = fastmcpp::Json::object(); @@ -398,9 +429,412 @@ make_mcp_handler(const std::string& server_name, const std::string& version, auto it = descriptions.find(name); if (it != descriptions.end()) desc = it->second; - meta.emplace_back(name, desc, schema); + tools_meta.emplace_back(name, desc, schema); } - return make_mcp_handler(server_name, version, server, meta); + + // Create handler that captures both server AND tools + // This allows tools/call to use tools.invoke() directly + return [server_name, version, &server, &tools, + tools_meta](const fastmcpp::Json& message) -> fastmcpp::Json + { + try + { + const auto id = message.contains("id") ? message.at("id") : fastmcpp::Json(); + std::string method = message.value("method", ""); + fastmcpp::Json params = message.value("params", fastmcpp::Json::object()); + + if (method == "initialize") + { + fastmcpp::Json serverInfo = {{"name", server.name()}, + {"version", server.version()}}; + if (server.website_url()) + serverInfo["websiteUrl"] = *server.website_url(); + if (server.icons()) + { + fastmcpp::Json icons_array = fastmcpp::Json::array(); + for (const auto& icon : *server.icons()) + { + fastmcpp::Json icon_json; + to_json(icon_json, icon); + icons_array.push_back(icon_json); + } + serverInfo["icons"] = icons_array; + } + return fastmcpp::Json{ + {"jsonrpc", "2.0"}, + {"id", id}, + {"result", + {{"protocolVersion", "2024-11-05"}, + {"capabilities", fastmcpp::Json{{"tools", fastmcpp::Json::object()}}}, + {"serverInfo", serverInfo}}}}; + } + + if (method == "tools/list") + { + fastmcpp::Json tools_array = fastmcpp::Json::array(); + for (const auto& name : tools.list_names()) + { + const auto& tool = tools.get(name); + fastmcpp::Json tool_json = {{"name", name}, + {"inputSchema", tool.input_schema()}}; + + // Add optional fields from Tool + if (tool.title()) + tool_json["title"] = *tool.title(); + if (tool.description()) + tool_json["description"] = *tool.description(); + if (tool.icons() && !tool.icons()->empty()) + { + fastmcpp::Json icons_json = fastmcpp::Json::array(); + for (const auto& icon : *tool.icons()) + { + fastmcpp::Json icon_obj = {{"src", icon.src}}; + if (icon.mime_type) + icon_obj["mimeType"] = *icon.mime_type; + if (icon.sizes) + icon_obj["sizes"] = *icon.sizes; + icons_json.push_back(icon_obj); + } + tool_json["icons"] = icons_json; + } + tools_array.push_back(tool_json); + } + return fastmcpp::Json{{"jsonrpc", "2.0"}, + {"id", id}, + {"result", fastmcpp::Json{{"tools", tools_array}}}}; + } + + if (method == "tools/call") + { + std::string name = params.value("name", ""); + fastmcpp::Json args = params.value("arguments", fastmcpp::Json::object()); + if (name.empty()) + return jsonrpc_error(id, -32602, "Missing tool name"); + try + { + // Use tools.invoke() directly - this is why we capture tools + auto result = tools.invoke(name, args); + fastmcpp::Json content = fastmcpp::Json::array(); + if (result.is_object() && result.contains("content")) + content = result.at("content"); + else if (result.is_array()) + content = result; + else if (result.is_string()) + content = fastmcpp::Json::array({fastmcpp::Json{ + {"type", "text"}, {"text", result.get()}}}); + else + content = fastmcpp::Json::array( + {fastmcpp::Json{{"type", "text"}, {"text", result.dump()}}}); + return fastmcpp::Json{{"jsonrpc", "2.0"}, + {"id", id}, + {"result", fastmcpp::Json{{"content", content}}}}; + } + catch (const std::exception& e) + { + return jsonrpc_error(id, -32603, e.what()); + } + } + + // Resources, prompts, etc. - route through server + if (method == "resources/list" || method == "resources/read" || + method == "prompts/list" || method == "prompts/get") + { + try + { + auto routed = server.handle(method, params); + return fastmcpp::Json{{"jsonrpc", "2.0"}, {"id", id}, {"result", routed}}; + } + catch (...) + { + // Return empty result for unimplemented + if (method == "resources/list") + return fastmcpp::Json{ + {"jsonrpc", "2.0"}, + {"id", id}, + {"result", fastmcpp::Json{{"resources", fastmcpp::Json::array()}}}}; + if (method == "resources/read") + return fastmcpp::Json{ + {"jsonrpc", "2.0"}, + {"id", id}, + {"result", fastmcpp::Json{{"contents", fastmcpp::Json::array()}}}}; + if (method == "prompts/list") + return fastmcpp::Json{ + {"jsonrpc", "2.0"}, + {"id", id}, + {"result", fastmcpp::Json{{"prompts", fastmcpp::Json::array()}}}}; + if (method == "prompts/get") + return fastmcpp::Json{ + {"jsonrpc", "2.0"}, + {"id", id}, + {"result", fastmcpp::Json{{"messages", fastmcpp::Json::array()}}}}; + } + } + + return jsonrpc_error(id, -32601, std::string("Method '") + method + "' not found"); + } + catch (const std::exception& e) + { + return jsonrpc_error(message.value("id", fastmcpp::Json()), -32603, e.what()); + } + }; +} + +// Full MCP handler with tools, resources, and prompts support +std::function +make_mcp_handler(const std::string& server_name, const std::string& version, + const server::Server& server, const tools::ToolManager& tools, + const resources::ResourceManager& resources, const prompts::PromptManager& prompts, + const std::unordered_map& descriptions) +{ + return [server_name, version, &server, &tools, &resources, &prompts, + descriptions](const fastmcpp::Json& message) -> fastmcpp::Json + { + try + { + const auto id = message.contains("id") ? message.at("id") : fastmcpp::Json(); + std::string method = message.value("method", ""); + fastmcpp::Json params = message.value("params", fastmcpp::Json::object()); + + if (method == "initialize") + { + fastmcpp::Json serverInfo = {{"name", server.name()}, + {"version", server.version()}}; + if (server.website_url()) + serverInfo["websiteUrl"] = *server.website_url(); + if (server.icons()) + { + fastmcpp::Json icons_array = fastmcpp::Json::array(); + for (const auto& icon : *server.icons()) + { + fastmcpp::Json icon_json; + to_json(icon_json, icon); + icons_array.push_back(icon_json); + } + serverInfo["icons"] = icons_array; + } + + // Advertise capabilities for tools, resources, and prompts + fastmcpp::Json capabilities = {{"tools", fastmcpp::Json::object()}}; + if (!resources.list().empty()) + capabilities["resources"] = fastmcpp::Json::object(); + if (!prompts.list().empty()) + capabilities["prompts"] = fastmcpp::Json::object(); + + return fastmcpp::Json{{"jsonrpc", "2.0"}, + {"id", id}, + {"result", + {{"protocolVersion", "2024-11-05"}, + {"capabilities", capabilities}, + {"serverInfo", serverInfo}}}}; + } + + if (method == "tools/list") + { + fastmcpp::Json tools_array = fastmcpp::Json::array(); + for (const auto& name : tools.list_names()) + { + const auto& tool = tools.get(name); + fastmcpp::Json tool_json = {{"name", name}, + {"inputSchema", tool.input_schema()}}; + if (tool.title()) + tool_json["title"] = *tool.title(); + if (tool.description()) + tool_json["description"] = *tool.description(); + if (tool.icons() && !tool.icons()->empty()) + { + fastmcpp::Json icons_json = fastmcpp::Json::array(); + for (const auto& icon : *tool.icons()) + { + fastmcpp::Json icon_obj = {{"src", icon.src}}; + if (icon.mime_type) + icon_obj["mimeType"] = *icon.mime_type; + if (icon.sizes) + icon_obj["sizes"] = *icon.sizes; + icons_json.push_back(icon_obj); + } + tool_json["icons"] = icons_json; + } + tools_array.push_back(tool_json); + } + return fastmcpp::Json{{"jsonrpc", "2.0"}, + {"id", id}, + {"result", fastmcpp::Json{{"tools", tools_array}}}}; + } + + if (method == "tools/call") + { + std::string name = params.value("name", ""); + fastmcpp::Json args = params.value("arguments", fastmcpp::Json::object()); + if (name.empty()) + return jsonrpc_error(id, -32602, "Missing tool name"); + try + { + auto result = tools.invoke(name, args); + fastmcpp::Json content = fastmcpp::Json::array(); + if (result.is_object() && result.contains("content")) + content = result.at("content"); + else if (result.is_array()) + content = result; + else if (result.is_string()) + content = fastmcpp::Json::array({fastmcpp::Json{ + {"type", "text"}, {"text", result.get()}}}); + else + content = fastmcpp::Json::array( + {fastmcpp::Json{{"type", "text"}, {"text", result.dump()}}}); + return fastmcpp::Json{{"jsonrpc", "2.0"}, + {"id", id}, + {"result", fastmcpp::Json{{"content", content}}}}; + } + catch (const std::exception& e) + { + return jsonrpc_error(id, -32603, e.what()); + } + } + + // Resources support + if (method == "resources/list") + { + fastmcpp::Json resources_array = fastmcpp::Json::array(); + for (const auto& res : resources.list()) + { + fastmcpp::Json res_json = {{"uri", res.uri}, {"name", res.name}}; + if (res.description) + res_json["description"] = *res.description; + if (res.mime_type) + res_json["mimeType"] = *res.mime_type; + resources_array.push_back(res_json); + } + return fastmcpp::Json{{"jsonrpc", "2.0"}, + {"id", id}, + {"result", fastmcpp::Json{{"resources", resources_array}}}}; + } + + if (method == "resources/read") + { + std::string uri = params.value("uri", ""); + if (uri.empty()) + return jsonrpc_error(id, -32602, "Missing resource URI"); + // Strip trailing slashes for compatibility with Python fastmcp + while (!uri.empty() && uri.back() == '/') + uri.pop_back(); + try + { + auto content = resources.read(uri, params); + fastmcpp::Json content_json = {{"uri", content.uri}}; + if (content.mime_type) + content_json["mimeType"] = *content.mime_type; + + // Handle text vs binary content + if (std::holds_alternative(content.data)) + { + content_json["text"] = std::get(content.data); + } + else + { + // Binary data - base64 encode + const auto& binary = std::get>(content.data); + static const char* b64_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string b64; + b64.reserve((binary.size() + 2) / 3 * 4); + for (size_t i = 0; i < binary.size(); i += 3) + { + uint32_t n = binary[i] << 16; + if (i + 1 < binary.size()) + n |= binary[i + 1] << 8; + if (i + 2 < binary.size()) + n |= binary[i + 2]; + b64.push_back(b64_chars[(n >> 18) & 0x3F]); + b64.push_back(b64_chars[(n >> 12) & 0x3F]); + b64.push_back((i + 1 < binary.size()) ? b64_chars[(n >> 6) & 0x3F] + : '='); + b64.push_back((i + 2 < binary.size()) ? b64_chars[n & 0x3F] : '='); + } + content_json["blob"] = b64; + } + + return fastmcpp::Json{{"jsonrpc", "2.0"}, + {"id", id}, + {"result", fastmcpp::Json{{"contents", {content_json}}}}}; + } + catch (const NotFoundError& e) + { + return jsonrpc_error(id, -32602, e.what()); + } + catch (const std::exception& e) + { + return jsonrpc_error(id, -32603, e.what()); + } + } + + // Prompts support + if (method == "prompts/list") + { + fastmcpp::Json prompts_array = fastmcpp::Json::array(); + for (const auto& prompt : prompts.list()) + { + fastmcpp::Json prompt_json = {{"name", prompt.name}}; + if (prompt.description) + prompt_json["description"] = *prompt.description; + if (!prompt.arguments.empty()) + { + fastmcpp::Json args_array = fastmcpp::Json::array(); + for (const auto& arg : prompt.arguments) + { + fastmcpp::Json arg_json = {{"name", arg.name}, + {"required", arg.required}}; + if (arg.description) + arg_json["description"] = *arg.description; + args_array.push_back(arg_json); + } + prompt_json["arguments"] = args_array; + } + prompts_array.push_back(prompt_json); + } + return fastmcpp::Json{{"jsonrpc", "2.0"}, + {"id", id}, + {"result", fastmcpp::Json{{"prompts", prompts_array}}}}; + } + + if (method == "prompts/get") + { + std::string name = params.value("name", ""); + if (name.empty()) + return jsonrpc_error(id, -32602, "Missing prompt name"); + try + { + fastmcpp::Json args = params.value("arguments", fastmcpp::Json::object()); + auto messages = prompts.render(name, args); + + fastmcpp::Json messages_array = fastmcpp::Json::array(); + for (const auto& msg : messages) + { + messages_array.push_back( + {{"role", msg.role}, + {"content", fastmcpp::Json{{"type", "text"}, {"text", msg.content}}}}); + } + + return fastmcpp::Json{{"jsonrpc", "2.0"}, + {"id", id}, + {"result", fastmcpp::Json{{"messages", messages_array}}}}; + } + catch (const NotFoundError& e) + { + return jsonrpc_error(id, -32602, e.what()); + } + catch (const std::exception& e) + { + return jsonrpc_error(id, -32603, e.what()); + } + } + + return jsonrpc_error(id, -32601, std::string("Method '") + method + "' not found"); + } + catch (const std::exception& e) + { + return jsonrpc_error(message.value("id", fastmcpp::Json()), -32603, e.what()); + } + }; } } // namespace fastmcpp::mcp diff --git a/src/resources/manager.cpp b/src/resources/manager.cpp index ccdcd28..dfb1dfb 100644 --- a/src/resources/manager.cpp +++ b/src/resources/manager.cpp @@ -1,28 +1,3 @@ #include "fastmcpp/resources/manager.hpp" -namespace fastmcpp::resources -{ - -void ResourceManager::register_resource(const Resource& res) -{ - by_id_[res.id.value] = res; -} - -const Resource& ResourceManager::get(const std::string& id) const -{ - auto it = by_id_.find(id); - if (it == by_id_.end()) - throw fastmcpp::NotFoundError("resource not found: " + id); - return it->second; -} - -std::vector ResourceManager::list() const -{ - std::vector out; - out.reserve(by_id_.size()); - for (auto& kv : by_id_) - out.push_back(kv.second); - return out; -} - -} // namespace fastmcpp::resources +// All implementations are inline in the header diff --git a/src/server/context.cpp b/src/server/context.cpp index deef3e9..54e22b7 100644 --- a/src/server/context.cpp +++ b/src/server/context.cpp @@ -28,7 +28,7 @@ std::vector Context::list_resources() const return resource_mgr_->list(); } -std::vector> Context::list_prompts() const +std::vector Context::list_prompts() const { return prompt_mgr_->list(); } diff --git a/src/server/middleware.cpp b/src/server/middleware.cpp index 99827ff..fa9b648 100644 --- a/src/server/middleware.cpp +++ b/src/server/middleware.cpp @@ -11,31 +11,38 @@ namespace fastmcpp::server void ToolInjectionMiddleware::add_prompt_tools(const prompts::PromptManager& pm) { // list_prompts tool - add_tool( - "list_prompts", "List all available prompts from the server", - Json{{"type", "object"}, {"properties", Json::object()}, {"required", Json::array()}}, - [&pm](const Json& /*args*/) -> Json - { - Context ctx(resources::ResourceManager(), pm); - auto prompts = ctx.list_prompts(); - - Json prompt_list = Json::array(); - for (const auto& [name, prompt] : prompts) - { - Json prompt_obj = { - {"name", name}, - {"description", prompt.template_string()}, - {"arguments", Json::array()}, - {"messages", - Json::array({Json{ - {"role", "user"}, - {"content", Json::array({Json{{"type", "text"}, - {"text", prompt.template_string()}}})}}})}}; - prompt_list.push_back(prompt_obj); - } - - return Json{{"prompts", prompt_list}}; - }); + add_tool("list_prompts", "List all available prompts from the server", + Json{{"type", "object"}, {"properties", Json::object()}, {"required", Json::array()}}, + [&pm](const Json& /*args*/) -> Json + { + Context ctx(resources::ResourceManager(), pm); + auto prompts_list = ctx.list_prompts(); + + Json prompt_list = Json::array(); + for (const auto& prompt : prompts_list) + { + Json prompt_obj = {{"name", prompt.name}}; + if (prompt.description) + prompt_obj["description"] = *prompt.description; + else + prompt_obj["description"] = nullptr; + + // Build arguments list + Json args_list = Json::array(); + for (const auto& arg : prompt.arguments) + { + Json arg_obj = {{"name", arg.name}, {"required", arg.required}}; + if (arg.description) + arg_obj["description"] = *arg.description; + args_list.push_back(arg_obj); + } + prompt_obj["arguments"] = args_list; + + prompt_list.push_back(prompt_obj); + } + + return Json{{"prompts", prompt_list}}; + }); // get_prompt tool add_tool( diff --git a/src/server/sse_server.cpp b/src/server/sse_server.cpp index 7d1f21c..d6c6e7d 100644 --- a/src/server/sse_server.cpp +++ b/src/server/sse_server.cpp @@ -239,10 +239,11 @@ bool SseServerWrapper::start() } res.status = 200; + // Note: Don't set Transfer-Encoding manually - set_chunked_content_provider + // handles it res.set_header("Content-Type", "text/event-stream; charset=utf-8"); res.set_header("Cache-Control", "no-cache, no-transform"); res.set_header("Connection", "keep-alive"); - res.set_header("Transfer-Encoding", "chunked"); // Security: Only set CORS header if explicitly configured if (!cors_origin_.empty()) diff --git a/tests/client/api_advanced.cpp b/tests/client/api_advanced.cpp index 64373b7..28d5f2b 100644 --- a/tests/client/api_advanced.cpp +++ b/tests/client/api_advanced.cpp @@ -294,7 +294,7 @@ void test_list_resource_templates() client::Client c(std::make_unique(srv)); auto result = c.list_resource_templates_mcp(); - assert(result.resourceTemplates.size() == 2); + assert(result.resourceTemplates.size() == 3); assert(result._meta.has_value()); assert(result._meta->value("hasMore", true) == false); diff --git a/tests/client/api_basic.cpp b/tests/client/api_basic.cpp index 1236bac..93533db 100644 --- a/tests/client/api_basic.cpp +++ b/tests/client/api_basic.cpp @@ -12,12 +12,12 @@ void test_list_tools() auto tools = c.list_tools(); - assert(tools.size() == 6); + assert(tools.size() == 7); assert(tools[0].name == "add"); assert(tools[0].description.value_or("") == "Add two numbers"); assert(tools[1].name == "greet"); - std::cout << " [PASS] list_tools() returns 6 tools\n"; + std::cout << " [PASS] list_tools() returns 7 tools\n"; } void test_list_tools_mcp() @@ -29,7 +29,7 @@ void test_list_tools_mcp() auto result = c.list_tools_mcp(); - assert(result.tools.size() == 6); + assert(result.tools.size() == 7); assert(!result.nextCursor.has_value()); // No pagination in this test std::cout << " [PASS] list_tools_mcp() returns ListToolsResult\n"; @@ -110,7 +110,7 @@ void test_list_resources() auto resources = c.list_resources(); - assert(resources.size() == 3); + assert(resources.size() == 4); assert(resources[0].uri == "file:///readme.txt"); assert(resources[0].name == "readme.txt"); assert(resources[0].mimeType.value_or("") == "text/plain"); @@ -151,12 +151,13 @@ void test_list_prompts() auto prompts = c.list_prompts(); - assert(prompts.size() == 2); + assert(prompts.size() == 3); assert(prompts[0].name == "code_review"); assert(prompts[1].name == "summarize"); assert(prompts[1].arguments.has_value()); assert(prompts[1].arguments->size() == 1); assert((*prompts[1].arguments)[0].name == "style"); + assert(prompts[2].name == "icon_prompt"); std::cout << " [PASS] list_prompts() works\n"; } diff --git a/tests/client/api_icons.cpp b/tests/client/api_icons.cpp new file mode 100644 index 0000000..4513105 --- /dev/null +++ b/tests/client/api_icons.cpp @@ -0,0 +1,146 @@ +/// @file tests/client/api_icons.cpp +/// @brief Integration tests for title and icons fields on client types + +#include "test_helpers.hpp" + +void test_tool_with_icons() +{ + std::cout << "Test: tool with title and icons...\n"; + + auto srv = create_tool_server(); + client::Client c(std::make_unique(srv)); + + auto tools = c.list_tools(); + + // Find the icon_tool + auto it = std::find_if(tools.begin(), tools.end(), + [](const auto& t) { return t.name == "icon_tool"; }); + + assert(it != tools.end()); + assert(it->title.has_value()); + assert(*it->title == "My Icon Tool"); + assert(it->icons.has_value()); + assert(it->icons->size() == 2); + + // Check first icon (URL) + assert((*it->icons)[0].src == "https://example.com/icon.png"); + assert((*it->icons)[0].mime_type.has_value()); + assert(*(*it->icons)[0].mime_type == "image/png"); + assert(!(*it->icons)[0].sizes.has_value()); + + // Check second icon (data URI with sizes) + assert((*it->icons)[1].src == "data:image/svg+xml;base64,PHN2Zz48L3N2Zz4="); + assert((*it->icons)[1].mime_type.has_value()); + assert(*(*it->icons)[1].mime_type == "image/svg+xml"); + assert((*it->icons)[1].sizes.has_value()); + assert((*it->icons)[1].sizes->size() == 2); + assert((*(*it->icons)[1].sizes)[0] == "48x48"); + assert((*(*it->icons)[1].sizes)[1] == "any"); + + std::cout << " [PASS] Tool with title and icons round-trips correctly\n"; +} + +void test_tool_without_icons() +{ + std::cout << "Test: tool without icons...\n"; + + auto srv = create_tool_server(); + client::Client c(std::make_unique(srv)); + + auto tools = c.list_tools(); + + // Find the add tool (has no icons) + auto it = + std::find_if(tools.begin(), tools.end(), [](const auto& t) { return t.name == "add"; }); + + assert(it != tools.end()); + assert(!it->title.has_value()); + assert(!it->icons.has_value()); + + std::cout << " [PASS] Tool without icons has nullopt fields\n"; +} + +void test_resource_with_icons() +{ + std::cout << "Test: resource with title and icons...\n"; + + auto srv = create_resource_server(); + client::Client c(std::make_unique(srv)); + + auto resources = c.list_resources(); + + // Find the icon_resource + auto it = std::find_if(resources.begin(), resources.end(), + [](const auto& r) { return r.name == "icon_resource"; }); + + assert(it != resources.end()); + assert(it->title.has_value()); + assert(*it->title == "Resource With Icons"); + assert(it->icons.has_value()); + assert(it->icons->size() == 1); + assert((*it->icons)[0].src == "https://example.com/res.png"); + + std::cout << " [PASS] Resource with title and icons round-trips correctly\n"; +} + +void test_resource_template_with_icons() +{ + std::cout << "Test: resource template with title and icons...\n"; + + auto srv = create_resource_server(); + client::Client c(std::make_unique(srv)); + + auto templates = c.list_resource_templates(); + + // Find the icon_template + auto it = std::find_if(templates.begin(), templates.end(), + [](const auto& t) { return t.name == "icon_template"; }); + + assert(it != templates.end()); + assert(it->title.has_value()); + assert(*it->title == "Template With Icons"); + assert(it->icons.has_value()); + assert(it->icons->size() == 1); + assert((*it->icons)[0].src == "https://example.com/tpl.svg"); + assert((*it->icons)[0].mime_type.has_value()); + assert(*(*it->icons)[0].mime_type == "image/svg+xml"); + + std::cout << " [PASS] Resource template with title and icons round-trips correctly\n"; +} + +void test_prompt_with_icons() +{ + std::cout << "Test: prompt with title and icons...\n"; + + auto srv = create_prompt_server(); + client::Client c(std::make_unique(srv)); + + auto prompts = c.list_prompts(); + + // Find the icon_prompt + auto it = std::find_if(prompts.begin(), prompts.end(), + [](const auto& p) { return p.name == "icon_prompt"; }); + + assert(it != prompts.end()); + assert(it->title.has_value()); + assert(*it->title == "Prompt With Icons"); + assert(it->icons.has_value()); + assert(it->icons->size() == 1); + assert((*it->icons)[0].src == "https://example.com/prompt.png"); + + std::cout << " [PASS] Prompt with title and icons round-trips correctly\n"; +} + +int main() +{ + std::cout << "\n=== Client API Icons Integration Tests ===\n\n"; + + test_tool_with_icons(); + test_tool_without_icons(); + test_resource_with_icons(); + test_resource_template_with_icons(); + test_prompt_with_icons(); + + std::cout << "\n=== All Icon Integration Tests Passed ===\n\n"; + return 0; +} diff --git a/tests/client/test_helpers.hpp b/tests/client/test_helpers.hpp index 7ad0306..255cd2b 100644 --- a/tests/client/test_helpers.hpp +++ b/tests/client/test_helpers.hpp @@ -52,6 +52,23 @@ class CallbackTransport : public client::ITransport client::Client* client_; }; +// Helper to create ToolInfo (C++17 compatible) +inline client::ToolInfo +make_tool(const std::string& name, const std::string& desc, const Json& inputSchema, + const std::optional& outputSchema = std::nullopt, + const std::optional& title = std::nullopt, + const std::optional>& icons = std::nullopt) +{ + client::ToolInfo t; + t.name = name; + t.title = title; + t.description = desc; + t.inputSchema = inputSchema; + t.outputSchema = outputSchema; + t.icons = icons; + return t; +} + // Helper: Create a server with tools/list and tools/call routes std::shared_ptr create_tool_server() { @@ -59,45 +76,56 @@ std::shared_ptr create_tool_server() // Register some tools static std::vector registered_tools = { - {"add", "Add two numbers", - Json{{"type", "object"}, - {"properties", {{"a", {{"type", "number"}}}, {"b", {{"type", "number"}}}}}}, - std::nullopt}, - {"greet", "Greet a person", - Json{{"type", "object"}, {"properties", {{"name", {{"type", "string"}}}}}}, std::nullopt}, - {"structured", "Return structured content", Json{{"type", "object"}}, - Json{{"type", "object"}, - {"x-fastmcp-wrap-result", true}, - {"properties", {{"result", {{"type", "integer"}}}}}, - {"required", Json::array({"result"})}}}, - {"mixed", "Mixed content", Json{{"type", "object"}}, std::nullopt}, - {"typed", "Nested typed result", Json{{"type", "object"}}, - Json{{"type", "object"}, - {"properties", - {{"items", - Json{{"type", "array"}, - {"items", - Json{{"type", "object"}, - {"properties", - {{"id", Json{{"type", "integer"}}}, - {"name", Json{{"type", "string"}}}, - {"active", Json{{"type", "boolean"}, {"default", true}}}, - {"timestamp", Json{{"type", "string"}, {"format", "date-time"}}}}}, - {"required", Json::array({"id", "name", "timestamp"})}}}}}, - {"mode", Json{{"enum", Json::array({"fast", "slow"})}}}}}, - {"required", Json::array({"items", "mode"})}}}, - {"typed_invalid", "Invalid typed result", Json{{"type", "object"}}, - Json{{"type", "object"}, - {"properties", - {{"items", Json{{"type", "array"}, - {"items", Json{{"type", "object"}, - {"properties", - {{"id", Json{{"type", "integer"}}}, - {"timestamp", Json{{"type", "string"}, - {"format", "date-time"}}}}}, - {"required", Json::array({"id", "timestamp"})}}}}}, - {"mode", Json{{"enum", Json::array({"fast", "slow"})}}}}}, - {"required", Json::array({"items", "mode"})}}}}; + make_tool("add", "Add two numbers", + Json{{"type", "object"}, + {"properties", {{"a", {{"type", "number"}}}, {"b", {{"type", "number"}}}}}}), + make_tool("greet", "Greet a person", + Json{{"type", "object"}, {"properties", {{"name", {{"type", "string"}}}}}}), + make_tool("structured", "Return structured content", Json{{"type", "object"}}, + Json{{"type", "object"}, + {"x-fastmcp-wrap-result", true}, + {"properties", {{"result", {{"type", "integer"}}}}}, + {"required", Json::array({"result"})}}), + make_tool("mixed", "Mixed content", Json{{"type", "object"}}), + make_tool( + "typed", "Nested typed result", Json{{"type", "object"}}, + Json{{"type", "object"}, + {"properties", + {{"items", + Json{{"type", "array"}, + {"items", + Json{{"type", "object"}, + {"properties", + {{"id", Json{{"type", "integer"}}}, + {"name", Json{{"type", "string"}}}, + {"active", Json{{"type", "boolean"}, {"default", true}}}, + {"timestamp", Json{{"type", "string"}, {"format", "date-time"}}}}}, + {"required", Json::array({"id", "name", "timestamp"})}}}}}, + {"mode", Json{{"enum", Json::array({"fast", "slow"})}}}}}, + {"required", Json::array({"items", "mode"})}}), + make_tool( + "typed_invalid", "Invalid typed result", Json{{"type", "object"}}, + Json{{"type", "object"}, + {"properties", + {{"items", Json{{"type", "array"}, + {"items", Json{{"type", "object"}, + {"properties", + {{"id", Json{{"type", "integer"}}}, + {"timestamp", Json{{"type", "string"}, + {"format", "date-time"}}}}}, + {"required", Json::array({"id", "timestamp"})}}}}}, + {"mode", Json{{"enum", Json::array({"fast", "slow"})}}}}}, + {"required", Json::array({"items", "mode"})}}), + // Tool with icons for icon tests + make_tool( + "icon_tool", "Tool with icons", Json{{"type", "object"}}, + std::nullopt, // outputSchema + std::string("My Icon Tool"), // title + std::vector{fastmcpp::Icon{"https://example.com/icon.png", + std::string("image/png"), std::nullopt}, + fastmcpp::Icon{"data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=", + std::string("image/svg+xml"), + std::vector{"48x48", "any"}}})}; // Store last received meta for testing static Json last_received_meta = nullptr; @@ -109,10 +137,26 @@ std::shared_ptr create_tool_server() for (const auto& t : registered_tools) { Json tool = {{"name", t.name}, {"inputSchema", t.inputSchema}}; + if (t.title) + tool["title"] = *t.title; if (t.description) tool["description"] = *t.description; if (t.outputSchema) tool["outputSchema"] = *t.outputSchema; + if (t.icons) + { + Json icons_json = Json::array(); + for (const auto& icon : *t.icons) + { + Json icon_obj = {{"src", icon.src}}; + if (icon.mime_type) + icon_obj["mimeType"] = *icon.mime_type; + if (icon.sizes) + icon_obj["sizes"] = *icon.sizes; + icons_json.push_back(icon_obj); + } + tool["icons"] = icons_json; + } tools.push_back(tool); } return Json{{"tools", tools}}; @@ -239,29 +283,41 @@ std::shared_ptr create_resource_server() [](const Json&) { return Json{ - {"resources", Json::array({{{"uri", "file:///readme.txt"}, - {"name", "readme.txt"}, - {"mimeType", "text/plain"}}, - {{"uri", "file:///data.json"}, - {"name", "data.json"}, - {"mimeType", "application/json"}}, - {{"uri", "file:///blob.bin"}, - {"name", "blob.bin"}, - {"mimeType", "application/octet-stream"}}})}, + {"resources", + Json::array({{{"uri", "file:///readme.txt"}, + {"name", "readme.txt"}, + {"mimeType", "text/plain"}}, + {{"uri", "file:///data.json"}, + {"name", "data.json"}, + {"mimeType", "application/json"}}, + {{"uri", "file:///blob.bin"}, + {"name", "blob.bin"}, + {"mimeType", "application/octet-stream"}}, + {{"uri", "file:///icon-resource"}, + {"name", "icon_resource"}, + {"title", "Resource With Icons"}, + {"icons", + Json::array({{{"src", "https://example.com/res.png"}}})}}})}, {"_meta", Json{{"page", 1}}}}; }); - srv->route("resources/templates/list", - [](const Json&) - { - return Json{ - {"resourceTemplates", Json::array({{{"uriTemplate", "file:///{name}"}, - {"name", "file template"}, - {"description", "files"}}, - {{"uriTemplate", "mem:///{key}"}, - {"name", "memory template"}}})}, - {"_meta", Json{{"hasMore", false}}}}; - }); + srv->route( + "resources/templates/list", + [](const Json&) + { + return Json{ + {"resourceTemplates", + Json::array({{{"uriTemplate", "file:///{name}"}, + {"name", "file template"}, + {"description", "files"}}, + {{"uriTemplate", "mem:///{key}"}, {"name", "memory template"}}, + {{"uriTemplate", "icon:///{id}"}, + {"name", "icon_template"}, + {"title", "Template With Icons"}, + {"icons", Json::array({{{"src", "https://example.com/tpl.svg"}, + {"mimeType", "image/svg+xml"}}})}}})}, + {"_meta", Json{{"hasMore", false}}}}; + }); srv->route("resources/read", [](const Json& in) @@ -297,12 +353,17 @@ std::shared_ptr create_prompt_server() { return Json{ {"prompts", - Json::array({{{"name", "code_review"}, {"description", "Review code for issues"}}, - {{"name", "summarize"}, - {"description", "Summarize text"}, - {"arguments", Json::array({{{"name", "style"}, - {"description", "Summary style"}, - {"required", false}}})}}})}}; + Json::array( + {{{"name", "code_review"}, {"description", "Review code for issues"}}, + {{"name", "summarize"}, + {"description", "Summarize text"}, + {"arguments", Json::array({{{"name", "style"}, + {"description", "Summary style"}, + {"required", false}}})}}, + {{"name", "icon_prompt"}, + {"title", "Prompt With Icons"}, + {"description", "A prompt with icons"}, + {"icons", Json::array({{{"src", "https://example.com/prompt.png"}}})}}})}}; }); srv->route("prompts/get", diff --git a/tests/json_types.cpp b/tests/json_types.cpp index 105cb2c..1e9de3b 100644 --- a/tests/json_types.cpp +++ b/tests/json_types.cpp @@ -1,7 +1,9 @@ +#include "fastmcpp/client/types.hpp" #include "fastmcpp/types.hpp" #include "fastmcpp/util/json.hpp" #include +#include #include using fastmcpp::util::json::dump; @@ -9,22 +11,152 @@ using fastmcpp::util::json::dump_pretty; using fastmcpp::util::json::json; using fastmcpp::util::json::parse; +void test_icon_serialization() +{ + // Icon round-trip + fastmcpp::Icon icon; + icon.src = "https://example.com/icon.png"; + icon.mime_type = "image/png"; + icon.sizes = std::vector{"48x48", "96x96"}; + + json j = icon; + assert(j["src"] == "https://example.com/icon.png"); + assert(j["mimeType"] == "image/png"); + assert(j["sizes"].size() == 2); + + auto icon2 = j.get(); + assert(icon2.src == icon.src); + assert(icon2.mime_type == icon.mime_type); + assert(icon2.sizes->size() == 2); + std::cout << " Icon serialization: PASS\n"; +} + +void test_toolinfo_title_icons() +{ + fastmcpp::client::ToolInfo tool; + tool.name = "my_tool"; + tool.title = "My Tool Title"; + tool.description = "A test tool"; + tool.inputSchema = json{{"type", "object"}}; + + fastmcpp::Icon icon; + icon.src = "https://example.com/tool.png"; + tool.icons = std::vector{icon}; + + json j = tool; + assert(j["name"] == "my_tool"); + assert(j["title"] == "My Tool Title"); + assert(j.contains("icons")); + + auto tool2 = j.get(); + assert(tool2.title.has_value()); + assert(*tool2.title == "My Tool Title"); + assert(tool2.icons.has_value()); + std::cout << " ToolInfo title/icons: PASS\n"; +} + +void test_resourceinfo_title_icons() +{ + fastmcpp::client::ResourceInfo res; + res.uri = "file:///test.txt"; + res.name = "test.txt"; + res.title = "Test File"; + + fastmcpp::Icon icon; + icon.src = "data:image/png;base64,abc"; + res.icons = std::vector{icon}; + + json j = res; + assert(j["title"] == "Test File"); + assert(j.contains("icons")); + + auto res2 = j.get(); + assert(res2.title.has_value()); + assert(res2.icons.has_value()); + std::cout << " ResourceInfo title/icons: PASS\n"; +} + +void test_resourcetemplate_title_icons() +{ + fastmcpp::client::ResourceTemplate tmpl; + tmpl.uriTemplate = "file:///{name}"; + tmpl.name = "file_template"; + tmpl.title = "File Template"; + + fastmcpp::Icon icon; + icon.src = "/icons/file.svg"; + tmpl.icons = std::vector{icon}; + + json j = tmpl; + assert(j["title"] == "File Template"); + assert(j.contains("icons")); + + auto tmpl2 = j.get(); + assert(tmpl2.title.has_value()); + assert(tmpl2.icons.has_value()); + std::cout << " ResourceTemplate title/icons: PASS\n"; +} + +void test_promptinfo_title_icons() +{ + fastmcpp::client::PromptInfo prompt; + prompt.name = "code_review"; + prompt.title = "Code Review Prompt"; + + fastmcpp::Icon icon; + icon.src = "https://example.com/review.png"; + prompt.icons = std::vector{icon}; + + json j = prompt; + assert(j["title"] == "Code Review Prompt"); + assert(j.contains("icons")); + + auto prompt2 = j.get(); + assert(prompt2.title.has_value()); + assert(prompt2.icons.has_value()); + std::cout << " PromptInfo title/icons: PASS\n"; +} + +void test_types_without_optional_fields() +{ + fastmcpp::client::ToolInfo tool; + tool.name = "simple"; + tool.inputSchema = json{{"type", "object"}}; + + json j = tool; + assert(!j.contains("title")); + assert(!j.contains("icons")); + + auto tool2 = j.get(); + assert(!tool2.title.has_value()); + assert(!tool2.icons.has_value()); + std::cout << " Types without optional fields: PASS\n"; +} + int main() { - // Parse and dump + std::cout << "Testing JSON type serialization:\n"; + auto j = parse("{\"a\":1,\"b\":[true,\"x\"]}"); assert(j["a"].get() == 1); - assert(j["b"][0].get() == true); auto s = dump(j); auto sp = dump_pretty(j, 2); assert(!s.empty()); assert(!sp.empty()); - // Custom type round-trip fastmcpp::Id id{"abc"}; - json jid = id; // to_json - auto id2 = jid.get(); // from_json + json jid = id; + auto id2 = jid.get(); assert(id2.value == "abc"); + std::cout << " Basic JSON operations: PASS\n"; + + test_icon_serialization(); + test_toolinfo_title_icons(); + test_resourceinfo_title_icons(); + test_resourcetemplate_title_icons(); + test_promptinfo_title_icons(); + test_types_without_optional_fields(); + std::cout << "All JSON type tests PASSED!\n"; return 0; } diff --git a/tests/prompts/basic.cpp b/tests/prompts/basic.cpp index 7a8208b..a4d1b2f 100644 --- a/tests/prompts/basic.cpp +++ b/tests/prompts/basic.cpp @@ -1,4 +1,5 @@ #include "fastmcpp/client/types.hpp" +#include "fastmcpp/exceptions.hpp" #include "fastmcpp/prompts/manager.hpp" #include "fastmcpp/prompts/prompt.hpp" @@ -216,7 +217,7 @@ void test_manager_get_nonexistent() { pm.get("nonexistent"); } - catch (const std::out_of_range&) + catch (const NotFoundError&) { threw = true; } diff --git a/tests/resources/advanced.cpp b/tests/resources/advanced.cpp index 2cf95c1..b93dd71 100644 --- a/tests/resources/advanced.cpp +++ b/tests/resources/advanced.cpp @@ -13,6 +13,19 @@ using namespace fastmcpp; +// Helper to create resources with legacy fields for backward compatibility tests +resources::Resource make_legacy_resource(const std::string& id, resources::Kind kind, + const Json& metadata) +{ + resources::Resource r; + r.uri = id; + r.name = id; + r.id = Id{id}; + r.kind = kind; + r.metadata = metadata; + return r; +} + void test_multiple_resource_kinds() { std::cout << "Test 1: Multiple resource kinds...\n"; @@ -20,19 +33,19 @@ void test_multiple_resource_kinds() resources::ResourceManager rm; // File resource - resources::Resource file_res{Id{"file1"}, resources::Kind::File, - Json{{"path", "/data/file.txt"}, {"size", 1024}}}; + auto file_res = make_legacy_resource("file1", resources::Kind::File, + Json{{"path", "/data/file.txt"}, {"size", 1024}}); // Text resource - resources::Resource text_res{Id{"text1"}, resources::Kind::Text, - Json{{"content", "Hello World"}, {"encoding", "utf-8"}}}; + auto text_res = make_legacy_resource("text1", resources::Kind::Text, + Json{{"content", "Hello World"}, {"encoding", "utf-8"}}); // JSON resource - resources::Resource json_res{Id{"json1"}, resources::Kind::Json, - Json{{"data", Json{{"key", "value"}}}}}; + auto json_res = make_legacy_resource("json1", resources::Kind::Json, + Json{{"data", Json{{"key", "value"}}}}); // Unknown kind resource - resources::Resource unknown_res{Id{"unknown1"}, resources::Kind::Unknown, Json::object()}; + auto unknown_res = make_legacy_resource("unknown1", resources::Kind::Unknown, Json::object()); rm.register_resource(file_res); rm.register_resource(text_res); @@ -69,13 +82,13 @@ void test_resource_metadata() resources::ResourceManager rm; // Resource with rich metadata - resources::Resource rich_res{ - Id{"rich1"}, resources::Kind::File, + auto rich_res = make_legacy_resource( + "rich1", resources::Kind::File, Json{{"name", "document.pdf"}, {"size_bytes", 2048}, {"created_at", "2025-01-01T00:00:00Z"}, {"tags", Json::array({"important", "draft"})}, - {"author", Json{{"name", "Alice"}, {"email", "alice@example.com"}}}}}; + {"author", Json{{"name", "Alice"}, {"email", "alice@example.com"}}}}); rm.register_resource(rich_res); @@ -95,15 +108,15 @@ void test_resource_update() resources::ResourceManager rm; // Register initial version - resources::Resource v1{Id{"doc1"}, resources::Kind::Text, - Json{{"version", 1}, {"content", "Version 1"}}}; + auto v1 = make_legacy_resource("doc1", resources::Kind::Text, + Json{{"version", 1}, {"content", "Version 1"}}); rm.register_resource(v1); assert(rm.get("doc1").metadata["version"] == 1); // Update with new version (same ID) - resources::Resource v2{Id{"doc1"}, resources::Kind::Text, - Json{{"version", 2}, {"content", "Version 2"}}}; + auto v2 = make_legacy_resource("doc1", resources::Kind::Text, + Json{{"version", 2}, {"content", "Version 2"}}); rm.register_resource(v2); @@ -150,8 +163,8 @@ void test_resource_list_ordering() // Add multiple resources for (int i = 0; i < 5; ++i) { - resources::Resource res{Id{"res_" + std::to_string(i)}, resources::Kind::Text, - Json{{"index", i}}}; + auto res = make_legacy_resource("res_" + std::to_string(i), resources::Kind::Text, + Json{{"index", i}}); rm.register_resource(res); } @@ -178,7 +191,7 @@ void test_empty_metadata() resources::ResourceManager rm; // Resource with empty metadata - resources::Resource empty_meta{Id{"empty1"}, resources::Kind::Text, Json::object()}; + auto empty_meta = make_legacy_resource("empty1", resources::Kind::Text, Json::object()); rm.register_resource(empty_meta); @@ -200,7 +213,7 @@ void test_large_metadata() for (int i = 0; i < 100; ++i) large_meta["field_" + std::to_string(i)] = "value_" + std::to_string(i); - resources::Resource large_res{Id{"large1"}, resources::Kind::Json, large_meta}; + auto large_res = make_legacy_resource("large1", resources::Kind::Json, large_meta); rm.register_resource(large_res); @@ -224,7 +237,7 @@ void test_special_characters_in_id() for (const auto& id : special_ids) { - resources::Resource res{Id{id}, resources::Kind::Text, Json{{"id", id}}}; + auto res = make_legacy_resource(id, resources::Kind::Text, Json{{"id", id}}); rm.register_resource(res); } @@ -264,9 +277,10 @@ void test_many_resources() // Register many resources for (int i = 0; i < num_resources; ++i) { - resources::Resource res{Id{"bulk_" + std::to_string(i)}, - static_cast((i % 4)), // Cycle through kinds - Json{{"index", i}}}; + auto res = + make_legacy_resource("bulk_" + std::to_string(i), + static_cast((i % 4)), // Cycle through kinds + Json{{"index", i}}); rm.register_resource(res); } diff --git a/tests/resources/basic.cpp b/tests/resources/basic.cpp index f8e3058..91370dc 100644 --- a/tests/resources/basic.cpp +++ b/tests/resources/basic.cpp @@ -7,9 +7,18 @@ int main() { using namespace fastmcpp; resources::ResourceManager rm; - resources::Resource r{Id{"r1"}, resources::Kind::Text, Json{{"title", "hello"}}}; + + // Create resource using new struct format + resources::Resource r; + r.uri = "r1"; + r.name = "r1"; + r.id = Id{"r1"}; + r.kind = resources::Kind::Text; + r.metadata = Json{{"title", "hello"}}; + rm.register_resource(r); auto got = rm.get("r1"); + assert(got.uri == "r1"); assert(got.id.value == "r1"); auto list = rm.list(); assert(list.size() == 1); diff --git a/tests/server/context_meta.cpp b/tests/server/context_meta.cpp index 9a05f7a..bb3b3a2 100644 --- a/tests/server/context_meta.cpp +++ b/tests/server/context_meta.cpp @@ -11,7 +11,13 @@ int main() resources::ResourceManager rm; prompts::PromptManager pm; pm.add("hello", prompts::Prompt{"Hello"}); - rm.register_resource(resources::Resource{Id{"file://test"}, resources::Kind::File, Json{}}); + resources::Resource res; + res.uri = "file://test"; + res.name = "test"; + res.id = Id{"file://test"}; + res.kind = resources::Kind::File; + res.metadata = Json{}; + rm.register_resource(res); // Context without MCP session: request metadata unavailable server::Context ctx_no_session(rm, pm); diff --git a/tests/server/middleware.cpp b/tests/server/middleware.cpp index 6f1ee63..43399b7 100644 --- a/tests/server/middleware.cpp +++ b/tests/server/middleware.cpp @@ -53,9 +53,13 @@ int main() fastmcpp::prompts::PromptManager pm; pm.add("hello", fastmcpp::prompts::Prompt{"Hello, {{name}}"}); fastmcpp::resources::ResourceManager rm; - rm.register_resource(fastmcpp::resources::Resource{fastmcpp::Id{"file://test.txt"}, - fastmcpp::resources::Kind::File, - Json{{"mimeType", "text/plain"}}}); + fastmcpp::resources::Resource test_res; + test_res.uri = "file://test.txt"; + test_res.name = "test.txt"; + test_res.id = fastmcpp::Id{"file://test.txt"}; + test_res.kind = fastmcpp::resources::Kind::File; + test_res.metadata = Json{{"mimeType", "text/plain"}}; + rm.register_resource(test_res); server::ToolInjectionMiddleware mw; mw.add_prompt_tools(pm);