diff --git a/include/mcp_client.h b/include/mcp_client.h index 48dbf7d..1820c06 100644 --- a/include/mcp_client.h +++ b/include/mcp_client.h @@ -3,7 +3,7 @@ * @brief MCP Client implementation * * This file implements the client-side functionality for the Model Context Protocol. - * Follows the 2024-11-05 basic protocol specification. + * Follows the 2024-11-05 protocol specification. */ #ifndef MCP_CLIENT_H @@ -39,7 +39,14 @@ public: * @param port The server port */ client(const std::string& host, int port = 8080, const json& capabilities = json::object()); - + + /** + * @brief Constructor + * @param base_url The base URL of the server (e.g., "http://localhost:8080") + * @param capabilities The capabilities of the client + */ + client(const std::string& base_url, const json& capabilities = json::object()); + /** * @brief Destructor */ @@ -124,30 +131,41 @@ public: */ std::vector get_tools(); - /** - * @brief Get resource metadata - * @param resource_path The path to the resource - * @return The resource metadata - * @throws mcp_exception on error - */ - json get_resource_metadata(const std::string& resource_path); - /** * @brief Get client capabilities * @return The client capabilities */ json get_capabilities(); - + /** - * @brief Access a resource - * @param resource_path The path to the resource - * @param params Additional parameters for the resource - * @return The resource data - * @throws mcp_exception on error + * @brief List available resources + * @param cursor Optional cursor for pagination + * @return List of resources */ - json access_resource(const std::string& resource_path, const json& params = json::object()); + json list_resources(const std::string& cursor = ""); + + /** + * @brief Read a resource + * @param resource_uri The URI of the resource + * @return The resource content + */ + json read_resource(const std::string& resource_uri); + + /** + * @brief Subscribe to resource changes + * @param resource_uri The URI of the resource + * @return Subscription result + */ + json subscribe_to_resource(const std::string& resource_uri); + + /** + * @brief List resource templates + * @return List of resource templates + */ + json list_resource_templates(); private: + std::string base_url_; std::string host_; int port_; std::string auth_token_; @@ -165,7 +183,8 @@ private: mutable std::mutex mutex_; // Initialize the client - void init_client(); + void init_client(const std::string& host, int port); + void init_client(const std::string& base_url); // Send a JSON-RPC request and get the response json send_jsonrpc(const request& req); diff --git a/include/mcp_resource.h b/include/mcp_resource.h index a34c30e..3d557f0 100644 --- a/include/mcp_resource.h +++ b/include/mcp_resource.h @@ -3,13 +3,19 @@ * @brief Resource implementation for MCP * * This file defines the base resource class and common resource types for the MCP protocol. - * Follows the 2024-11-05 basic protocol specification. + * Follows the 2024-11-05 protocol specification. */ #ifndef MCP_RESOURCE_H #define MCP_RESOURCE_H #include "mcp_message.h" +#include "base64.hpp" +#include +#include +#include +#include +#include namespace mcp { @@ -18,7 +24,7 @@ namespace mcp { * @brief Base class for MCP resources * * The resource class defines the interface for resources that can be - * accessed through the MCP protocol. + * accessed through the MCP protocol. Each resource is identified by a URI. */ class resource { public: @@ -31,99 +37,271 @@ public: virtual json get_metadata() const = 0; /** - * @brief Access the resource - * @param params Parameters for accessing the resource - * @return The resource data + * @brief Read the resource content + * @return The resource content as JSON */ - virtual json access(const json& params) const = 0; + virtual json read() const = 0; + + /** + * @brief Check if the resource has been modified + * @return True if the resource has been modified since last read + */ + virtual bool is_modified() const = 0; + + /** + * @brief Get the URI of the resource + * @return The URI as string + */ + virtual std::string get_uri() const = 0; +}; + +/** + * @class text_resource + * @brief Resource containing text content + * + * The text_resource class provides a base implementation for resources + * that contain text content. + */ +class text_resource : public resource { +public: + /** + * @brief Constructor + * @param uri The URI of the resource + * @param name The name of the resource + * @param mime_type The MIME type of the resource + * @param description Optional description of the resource + */ + text_resource(const std::string& uri, + const std::string& name, + const std::string& mime_type, + const std::string& description = ""); + + /** + * @brief Get resource metadata + * @return Metadata as JSON + */ + json get_metadata() const override; + + /** + * @brief Read the resource content + * @return The resource content as JSON + */ + json read() const override; + + /** + * @brief Check if the resource has been modified + * @return True if the resource has been modified since last read + */ + bool is_modified() const override; + + /** + * @brief Get the URI of the resource + * @return The URI as string + */ + std::string get_uri() const override; + + /** + * @brief Set the text content of the resource + * @param text The text content + */ + void set_text(const std::string& text); + + /** + * @brief Get the text content of the resource + * @return The text content + */ + std::string get_text() const; + +protected: + std::string uri_; + std::string name_; + std::string mime_type_; + std::string description_; + std::string text_; + mutable bool modified_; +}; + +/** + * @class binary_resource + * @brief Resource containing binary content + * + * The binary_resource class provides a base implementation for resources + * that contain binary content. + */ +class binary_resource : public resource { +public: + /** + * @brief Constructor + * @param uri The URI of the resource + * @param name The name of the resource + * @param mime_type The MIME type of the resource + * @param description Optional description of the resource + */ + binary_resource(const std::string& uri, + const std::string& name, + const std::string& mime_type, + const std::string& description = ""); + + /** + * @brief Get resource metadata + * @return Metadata as JSON + */ + json get_metadata() const override; + + /** + * @brief Read the resource content + * @return The resource content as JSON with base64-encoded data + */ + json read() const override; + + /** + * @brief Check if the resource has been modified + * @return True if the resource has been modified since last read + */ + bool is_modified() const override; + + /** + * @brief Get the URI of the resource + * @return The URI as string + */ + std::string get_uri() const override; + + /** + * @brief Set the binary content of the resource + * @param data Pointer to the binary data + * @param size Size of the binary data + */ + void set_data(const uint8_t* data, size_t size); + + /** + * @brief Get the binary content of the resource + * @return The binary content + */ + const std::vector& get_data() const; + +protected: + std::string uri_; + std::string name_; + std::string mime_type_; + std::string description_; + std::vector data_; + mutable bool modified_; }; /** * @class file_resource * @brief Resource for file system operations * - * The file_resource class provides access to files. + * The file_resource class provides access to files as resources. */ -class file_resource : public resource { +class file_resource : public text_resource { public: /** * @brief Constructor - * @param base_path The base path for file operations (for security) + * @param file_path The path to the file + * @param mime_type The MIME type of the file (optional, will be guessed if not provided) + * @param description Optional description of the resource */ - explicit file_resource(const std::string& base_path); + file_resource(const std::string& file_path, + const std::string& mime_type = "", + const std::string& description = ""); /** - * @brief Get resource metadata - * @return Metadata as JSON + * @brief Read the resource content + * @return The resource content as JSON */ - json get_metadata() const override; + json read() const override; /** - * @brief Access the resource - * @param params Parameters for accessing the resource - * @return The resource data + * @brief Check if the resource has been modified + * @return True if the resource has been modified since last read */ - json access(const json& params) const override; + bool is_modified() const override; private: - std::string base_path_; + std::string file_path_; + mutable time_t last_modified_; - // Helper methods - json read_file(const std::string& path) const; - json write_file(const std::string& path, const std::string& content) const; - json delete_file(const std::string& path) const; - json list_directory(const std::string& path) const; + /** + * @brief Guess the MIME type from file extension + * @param file_path The file path + * @return The guessed MIME type + */ + static std::string guess_mime_type(const std::string& file_path); }; /** - * @class api_resource - * @brief Resource for custom API endpoints + * @class resource_manager + * @brief Manager for MCP resources * - * The api_resource class provides a way to define custom API endpoints. + * The resource_manager class provides a central registry for resources + * and handles resource operations. */ -class api_resource : public resource { +class resource_manager { public: - using handler_func = std::function; + /** + * @brief Get the singleton instance + * @return Reference to the singleton instance + */ + static resource_manager& instance(); /** - * @brief Constructor - * @param name Resource name - * @param description Resource description + * @brief Register a resource + * @param resource Shared pointer to the resource */ - api_resource(const std::string& name, const std::string& description); + void register_resource(std::shared_ptr resource); /** - * @brief Get resource metadata - * @return Metadata as JSON + * @brief Unregister a resource + * @param uri The URI of the resource to unregister + * @return True if the resource was unregistered */ - json get_metadata() const override; + bool unregister_resource(const std::string& uri); /** - * @brief Access the resource - * @param params Parameters for accessing the resource - * @return The resource data + * @brief Get a resource by URI + * @param uri The URI of the resource + * @return Shared pointer to the resource, or nullptr if not found */ - json access(const json& params) const override; + std::shared_ptr get_resource(const std::string& uri) const; /** - * @brief Register a handler for a specific endpoint - * @param endpoint The endpoint name - * @param handler The handler function - * @param description Description of the endpoint + * @brief List all registered resources + * @return JSON array of resource metadata */ - void register_handler(const std::string& endpoint, - handler_func handler, - const std::string& description = ""); + json list_resources() const; + + /** + * @brief Subscribe to resource changes + * @param uri The URI of the resource to subscribe to + * @param callback The callback function to call when the resource changes + * @return Subscription ID + */ + int subscribe(const std::string& uri, std::function callback); + + /** + * @brief Unsubscribe from resource changes + * @param subscription_id The subscription ID + * @return True if the subscription was removed + */ + bool unsubscribe(int subscription_id); + + /** + * @brief Notify subscribers of resource changes + * @param uri The URI of the resource that changed + */ + void notify_resource_changed(const std::string& uri); private: - struct endpoint_info { - handler_func handler; - std::string description; - }; + resource_manager() = default; + ~resource_manager() = default; - std::string name_; - std::string description_; - std::map endpoints_; + resource_manager(const resource_manager&) = delete; + resource_manager& operator=(const resource_manager&) = delete; + + std::map> resources_; + std::map>> subscriptions_; + int next_subscription_id_ = 1; }; } // namespace mcp diff --git a/src/mcp_client.cpp b/src/mcp_client.cpp index 52b99c0..cea46bc 100644 --- a/src/mcp_client.cpp +++ b/src/mcp_client.cpp @@ -14,16 +14,31 @@ namespace mcp { client::client(const std::string& host, int port, const json& capabilities) : host_(host), port_(port), capabilities_(capabilities) { - init_client(); + init_client(host, port); +} + +client::client(const std::string& base_url, const json& capabilities) + : base_url_(base_url), capabilities_(capabilities) { + init_client(base_url); } client::~client() { // httplib::Client will be automatically destroyed } -void client::init_client() { +void client::init_client(const std::string& host, int port) { // Create the HTTP client - http_client_ = std::make_unique(host_.c_str(), port_); + http_client_ = std::make_unique(host.c_str(), port); + + // Set timeout + http_client_->set_connection_timeout(timeout_seconds_, 0); + http_client_->set_read_timeout(timeout_seconds_, 0); + http_client_->set_write_timeout(timeout_seconds_, 0); +} + +void client::init_client(const std::string& base_url) { + // Create the HTTP client + http_client_ = std::make_unique(base_url.c_str()); // Set timeout http_client_->set_connection_timeout(timeout_seconds_, 0); @@ -157,27 +172,32 @@ std::vector client::get_tools() { return tools; } -json client::get_resource_metadata(const std::string& resource_path) { - return send_request("resources/metadata", { - {"path", resource_path} - }).result; -} - json client::get_capabilities() { return capabilities_; } -json client::access_resource(const std::string& resource_path, const json& params) { - json request_params = { - {"path", resource_path} - }; - - // Add any additional parameters - for (auto it = params.begin(); it != params.end(); ++it) { - request_params[it.key()] = it.value(); +json client::list_resources(const std::string& cursor) { + json params = json::object(); + if (!cursor.empty()) { + params["cursor"] = cursor; } - - return send_request("resources/access", request_params).result; + return send_request("resources/list", params).result; +} + +json client::read_resource(const std::string& resource_uri) { + return send_request("resources/read", { + {"uri", resource_uri} + }).result; +} + +json client::subscribe_to_resource(const std::string& resource_uri) { + return send_request("resources/subscribe", { + {"uri", resource_uri} + }).result; +} + +json client::list_resource_templates() { + return send_request("resources/templates/list").result; } json client::send_jsonrpc(const request& req) { diff --git a/src/mcp_resource.cpp b/src/mcp_resource.cpp index 1bf548d..98f5247 100644 --- a/src/mcp_resource.cpp +++ b/src/mcp_resource.cpp @@ -3,7 +3,7 @@ * @brief Resource implementation for MCP * * This file implements the resource classes for the Model Context Protocol. - * Follows the 2024-11-05 basic protocol specification. + * Follows the 2024-11-05 protocol specification. */ #include "mcp_resource.h" #include @@ -11,247 +11,325 @@ #include #include #include +#include +#include +#include namespace fs = std::filesystem; namespace mcp { -// file_resource implementation -file_resource::file_resource(const std::string& base_path) - : base_path_(base_path) { - // Ensure base path exists - if (!fs::exists(base_path_)) { - throw mcp_exception(error_code::internal_error, - "Base path does not exist: " + base_path_); - } +// text_resource implementation +text_resource::text_resource(const std::string& uri, + const std::string& name, + const std::string& mime_type, + const std::string& description) + : uri_(uri), name_(name), mime_type_(mime_type), description_(description), modified_(false) { } -json file_resource::get_metadata() const { +json text_resource::get_metadata() const { return { - {"type", "file_resource"}, - {"base_path", base_path_}, - {"description", "File system resource for accessing files"} + {"uri", uri_}, + {"name", name_}, + {"mimeType", mime_type_}, + {"description", description_} }; } -json file_resource::access(const json& params) const { - // Extract the path from the parameters - if (!params.contains("path")) { - throw mcp_exception(error_code::invalid_params, "Missing 'path' parameter"); - } - - std::string path = params["path"]; - - // Remove trailing slash if present - if (!path.empty() && path.back() == '/') { - path.pop_back(); - } - - // Handle different operations - if (params.contains("operation")) { - std::string operation = params["operation"]; - - if (operation == "read") { - return read_file(path); - } else if (operation == "write") { - if (!params.contains("content")) { - throw mcp_exception(error_code::invalid_params, "Missing 'content' parameter for write operation"); - } - return write_file(path, params["content"]); - } else if (operation == "delete") { - return delete_file(path); - } else if (operation == "list") { - return list_directory(path); - } else { - throw mcp_exception(error_code::invalid_params, "Invalid operation: " + operation); - } - } - - // Default to read operation - std::string full_path = base_path_ + path; - if (fs::is_directory(full_path)) { - return list_directory(path); - } else { - return read_file(path); +json text_resource::read() const { + modified_ = false; + return { + {"uri", uri_}, + {"mimeType", mime_type_}, + {"text", text_} + }; +} + +bool text_resource::is_modified() const { + return modified_; +} + +std::string text_resource::get_uri() const { + return uri_; +} + +void text_resource::set_text(const std::string& text) { + if (text_ != text) { + text_ = text; + modified_ = true; } } -json file_resource::read_file(const std::string& path) const { - std::string full_path = base_path_ + path; +std::string text_resource::get_text() const { + return text_; +} + +// binary_resource implementation +binary_resource::binary_resource(const std::string& uri, + const std::string& name, + const std::string& mime_type, + const std::string& description) + : uri_(uri), name_(name), mime_type_(mime_type), description_(description), modified_(false) { +} + +json binary_resource::get_metadata() const { + return { + {"uri", uri_}, + {"name", name_}, + {"mimeType", mime_type_}, + {"description", description_} + }; +} + +json binary_resource::read() const { + modified_ = false; - // Check if file exists - if (!fs::exists(full_path) || !fs::is_regular_file(full_path)) { - throw mcp_exception(error_code::invalid_params, "File not found: " + path); + // Base64 encode the binary data + std::string base64_data; + if (!data_.empty()) { + base64_data = base64::encode(reinterpret_cast(data_.data()), data_.size()); } + return { + {"uri", uri_}, + {"mimeType", mime_type_}, + {"blob", base64_data} + }; +} + +bool binary_resource::is_modified() const { + return modified_; +} + +std::string binary_resource::get_uri() const { + return uri_; +} + +void binary_resource::set_data(const uint8_t* data, size_t size) { + data_.resize(size); + if (size > 0) { + std::memcpy(data_.data(), data, size); + } + modified_ = true; +} + +const std::vector& binary_resource::get_data() const { + return data_; +} + +// file_resource implementation +file_resource::file_resource(const std::string& file_path, + const std::string& mime_type, + const std::string& description) + : text_resource("file://" + file_path, + fs::path(file_path).filename().string(), + mime_type.empty() ? guess_mime_type(file_path) : mime_type, + description), + file_path_(file_path), + last_modified_(0) { + + // Check if file exists + if (!fs::exists(file_path_)) { + throw mcp_exception(error_code::invalid_params, + "File not found: " + file_path_); + } +} + +json file_resource::read() const { // Read file content - std::ifstream file(full_path, std::ios::binary); + std::ifstream file(file_path_, std::ios::binary); if (!file) { throw mcp_exception(error_code::internal_error, - "Failed to open file: " + path); + "Failed to open file: " + file_path_); } std::stringstream buffer; buffer << file.rdbuf(); - // Get file info - std::string content = buffer.str(); - std::string ext = fs::path(full_path).extension().string(); + // Update text content + const_cast(this)->set_text(buffer.str()); + + // Update last modified time + last_modified_ = fs::last_write_time(file_path_).time_since_epoch().count(); + + // Mark as not modified after read + modified_ = false; return { - {"path", path}, - {"content", content}, - {"size", content.size()}, - {"extension", ext}, - {"last_modified", fs::last_write_time(full_path).time_since_epoch().count()} + {"uri", uri_}, + {"mimeType", mime_type_}, + {"text", text_} }; } -json file_resource::write_file(const std::string& path, const std::string& content) const { - std::string full_path = base_path_ + path; +bool file_resource::is_modified() const { + if (!fs::exists(file_path_)) { + return true; // File was deleted + } - // Create directories if they don't exist - fs::path dir_path = fs::path(full_path).parent_path(); - if (!fs::exists(dir_path)) { - try { - fs::create_directories(dir_path); - } catch (const std::exception& e) { - throw mcp_exception(error_code::internal_error, - "Failed to create directory: " + std::string(e.what())); + time_t current_modified = fs::last_write_time(file_path_).time_since_epoch().count(); + return current_modified != last_modified_; +} + +std::string file_resource::guess_mime_type(const std::string& file_path) { + std::string ext = fs::path(file_path).extension().string(); + + // Convert to lowercase + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) { return std::tolower(c); }); + + // Common MIME types + if (ext == ".txt") return "text/plain"; + if (ext == ".html" || ext == ".htm") return "text/html"; + if (ext == ".css") return "text/css"; + if (ext == ".js") return "text/javascript"; + if (ext == ".json") return "application/json"; + if (ext == ".xml") return "application/xml"; + if (ext == ".pdf") return "application/pdf"; + if (ext == ".png") return "image/png"; + if (ext == ".jpg" || ext == ".jpeg") return "image/jpeg"; + if (ext == ".gif") return "image/gif"; + if (ext == ".svg") return "image/svg+xml"; + if (ext == ".mp3") return "audio/mpeg"; + if (ext == ".mp4") return "video/mp4"; + if (ext == ".wav") return "audio/wav"; + if (ext == ".zip") return "application/zip"; + if (ext == ".doc" || ext == ".docx") return "application/msword"; + if (ext == ".xls" || ext == ".xlsx") return "application/vnd.ms-excel"; + if (ext == ".ppt" || ext == ".pptx") return "application/vnd.ms-powerpoint"; + if (ext == ".csv") return "text/csv"; + if (ext == ".md") return "text/markdown"; + if (ext == ".py") return "text/x-python"; + if (ext == ".cpp" || ext == ".cc") return "text/x-c++src"; + if (ext == ".h" || ext == ".hpp") return "text/x-c++hdr"; + if (ext == ".c") return "text/x-csrc"; + if (ext == ".rs") return "text/x-rust"; + if (ext == ".go") return "text/x-go"; + if (ext == ".java") return "text/x-java"; + if (ext == ".ts") return "text/x-typescript"; + if (ext == ".rb") return "text/x-ruby"; + + // Default to binary if unknown + return "application/octet-stream"; +} + +// resource_manager implementation +static std::mutex g_resource_manager_mutex; + +resource_manager& resource_manager::instance() { + static resource_manager instance; + return instance; +} + +void resource_manager::register_resource(std::shared_ptr resource) { + if (!resource) { + throw mcp_exception(error_code::invalid_params, "Cannot register null resource"); + } + + std::string uri = resource->get_uri(); + + std::lock_guard lock(g_resource_manager_mutex); + resources_[uri] = resource; +} + +bool resource_manager::unregister_resource(const std::string& uri) { + std::lock_guard lock(g_resource_manager_mutex); + + auto it = resources_.find(uri); + if (it == resources_.end()) { + return false; + } + + resources_.erase(it); + + // Remove any subscriptions for this resource + auto sub_it = subscriptions_.begin(); + while (sub_it != subscriptions_.end()) { + if (sub_it->second.first == uri) { + sub_it = subscriptions_.erase(sub_it); + } else { + ++sub_it; } } - // Write file content - std::ofstream file(full_path, std::ios::binary); - if (!file) { - throw mcp_exception(error_code::internal_error, - "Failed to open file for writing: " + path); - } - - file << content; - - return { - {"success", true}, - {"message", "File written successfully"}, - {"path", path}, - {"size", content.size()} - }; + return true; } -json file_resource::delete_file(const std::string& path) const { - std::string full_path = base_path_ + path; +std::shared_ptr resource_manager::get_resource(const std::string& uri) const { + std::lock_guard lock(g_resource_manager_mutex); - // Check if file exists - if (!fs::exists(full_path)) { - throw mcp_exception(error_code::invalid_params, "File not found: " + path); + auto it = resources_.find(uri); + if (it == resources_.end()) { + return nullptr; } - // Delete file - try { - fs::remove(full_path); - } catch (const std::exception& e) { - throw mcp_exception(error_code::internal_error, - "Failed to delete file: " + std::string(e.what())); + return it->second; +} + +json resource_manager::list_resources() const { + std::lock_guard lock(g_resource_manager_mutex); + + json resources = json::array(); + + for (const auto& [uri, res] : resources_) { + resources.push_back(res->get_metadata()); } return { - {"success", true}, - {"message", "File deleted successfully"}, - {"path", path} + {"resources", resources} }; } -json file_resource::list_directory(const std::string& path) const { - std::string full_path = base_path_ + path; - - // Check if directory exists - if (!fs::exists(full_path) || !fs::is_directory(full_path)) { - throw mcp_exception(error_code::invalid_params, "Directory not found: " + path); +int resource_manager::subscribe(const std::string& uri, std::function callback) { + if (!callback) { + throw mcp_exception(error_code::invalid_params, "Cannot subscribe with null callback"); } - // List directory contents - json entries = json::array(); + std::lock_guard lock(g_resource_manager_mutex); - for (const auto& entry : fs::directory_iterator(full_path)) { - std::string entry_path = entry.path().string(); - // Convert to relative path - entry_path = entry_path.substr(base_path_.length()); - - json entry_json = { - {"name", entry.path().filename().string()}, - {"path", entry_path}, - {"is_directory", entry.is_directory()} - }; - - if (!entry.is_directory()) { - entry_json["size"] = entry.file_size(); - entry_json["last_modified"] = fs::last_write_time(entry.path()).time_since_epoch().count(); + // Check if resource exists + auto it = resources_.find(uri); + if (it == resources_.end()) { + throw mcp_exception(error_code::invalid_params, "Resource not found: " + uri); + } + + int id = next_subscription_id_++; + subscriptions_[id] = std::make_pair(uri, callback); + + return id; +} + +bool resource_manager::unsubscribe(int subscription_id) { + std::lock_guard lock(g_resource_manager_mutex); + + auto it = subscriptions_.find(subscription_id); + if (it == subscriptions_.end()) { + return false; + } + + subscriptions_.erase(it); + return true; +} + +void resource_manager::notify_resource_changed(const std::string& uri) { + std::lock_guard lock(g_resource_manager_mutex); + + // Check if resource exists + auto it = resources_.find(uri); + if (it == resources_.end()) { + return; + } + + // Notify all subscribers for this resource + for (const auto& [id, sub] : subscriptions_) { + if (sub.first == uri) { + try { + sub.second(uri); + } catch (...) { + // Ignore exceptions in callbacks + } } - - entries.push_back(entry_json); } - - return { - {"path", path}, - {"entries", entries} - }; -} - -// api_resource implementation -api_resource::api_resource(const std::string& name, const std::string& description) - : name_(name), description_(description) { -} - -json api_resource::get_metadata() const { - json endpoints = json::object(); - - for (const auto& [endpoint, info] : endpoints_) { - endpoints[endpoint] = { - {"description", info.description} - }; - } - - return { - {"type", "api_resource"}, - {"name", name_}, - {"description", description_}, - {"endpoints", endpoints} - }; -} - -json api_resource::access(const json& params) const { - // Extract the endpoint from the parameters - if (!params.contains("endpoint")) { - throw mcp_exception(error_code::invalid_params, "Missing 'endpoint' parameter"); - } - - std::string endpoint = params["endpoint"]; - - // Find the endpoint handler - auto it = endpoints_.find(endpoint); - if (it == endpoints_.end()) { - throw mcp_exception(error_code::invalid_params, "Endpoint not found: " + endpoint); - } - - // Call the handler - try { - return it->second.handler(params); - } catch (const mcp_exception& e) { - throw; // Re-throw MCP exceptions - } catch (const std::exception& e) { - throw mcp_exception(error_code::internal_error, - "Error in endpoint handler: " + std::string(e.what())); - } -} - -void api_resource::register_handler(const std::string& endpoint, - handler_func handler, - const std::string& description) { - endpoint_info info; - info.handler = handler; - info.description = description; - - endpoints_[endpoint] = info; } } // namespace mcp \ No newline at end of file diff --git a/src/mcp_server.cpp b/src/mcp_server.cpp index 224a5b0..3b6d39c 100644 --- a/src/mcp_server.cpp +++ b/src/mcp_server.cpp @@ -14,6 +14,14 @@ server::server(const std::string& host, int port) : host_(host), port_(port), name_("MCP Server"), version_(MCP_VERSION) { http_server_ = std::make_unique(); + + // Set default capabilities + capabilities_ = { + {"resources", { + {"subscribe", true}, + {"listChanged", true} + }} + }; } server::~server() { @@ -99,35 +107,74 @@ void server::register_resource(const std::string& path, std::shared_ptrsecond->get_metadata(); + json contents = json::array(); + contents.push_back(it->second->read()); + + return json{ + {"contents", contents} + }; }; } - if (method_handlers_.find("resources/access") == method_handlers_.end()) { - method_handlers_["resources/access"] = [this](const json& params) { - if (!params.contains("path")) { - throw mcp_exception(error_code::invalid_params, "Missing 'path' parameter"); + if (method_handlers_.find("resources/list") == method_handlers_.end()) { + method_handlers_["resources/list"] = [this](const json& params) { + json resources = json::array(); + + for (const auto& [uri, res] : resources_) { + resources.push_back(res->get_metadata()); } - std::string path = params["path"]; - auto it = resources_.find(path); + json result = { + {"resources", resources} + }; + + // Handle pagination if cursor is provided + if (params.contains("cursor")) { + // In this implementation, we don't actually paginate + // but we include the nextCursor field for compatibility + result["nextCursor"] = ""; + } + + return result; + }; + } + + if (method_handlers_.find("resources/subscribe") == method_handlers_.end()) { + method_handlers_["resources/subscribe"] = [this](const json& params) { + if (!params.contains("uri")) { + throw mcp_exception(error_code::invalid_params, "Missing 'uri' parameter"); + } + + std::string uri = params["uri"]; + auto it = resources_.find(uri); if (it == resources_.end()) { - throw mcp_exception(error_code::invalid_params, "Resource not found: " + path); + throw mcp_exception(error_code::invalid_params, "Resource not found: " + uri); } - return it->second->access(params); + // In a real implementation, we would register a subscription here + // For now, just return success + return json::object(); + }; + } + + if (method_handlers_.find("resources/templates/list") == method_handlers_.end()) { + method_handlers_["resources/templates/list"] = [this](const json& params) { + // In this implementation, we don't support resource templates + return json{ + {"resourceTemplates", json::array()} + }; }; } } diff --git a/test/test_mcp_resource.cpp b/test/test_mcp_resource.cpp index b48ffb8..b5b0c29 100644 --- a/test/test_mcp_resource.cpp +++ b/test/test_mcp_resource.cpp @@ -6,12 +6,17 @@ */ #include "mcp_resource.h" +#include "mcp_server.h" +#include "mcp_client.h" +#include "base64.hpp" #include #include #include #include #include #include +#include +#include // Create a test directory and file class ResourceTest : public ::testing::Test { @@ -44,362 +49,213 @@ protected: std::filesystem::path test_file; }; +// Test text resource +TEST(McpResourceTest, TextResourceTest) { + // Create text resource + mcp::text_resource resource("test://example.txt", "example.txt", "text/plain", "Example text resource"); + + // Test metadata + mcp::json metadata = resource.get_metadata(); + EXPECT_EQ(metadata["uri"], "test://example.txt"); + EXPECT_EQ(metadata["name"], "example.txt"); + EXPECT_EQ(metadata["mimeType"], "text/plain"); + EXPECT_EQ(metadata["description"], "Example text resource"); + + // Test URI + EXPECT_EQ(resource.get_uri(), "test://example.txt"); + + // Test setting and getting text + std::string test_content = "This is a test content"; + resource.set_text(test_content); + EXPECT_EQ(resource.get_text(), test_content); + + // Test read + mcp::json content = resource.read(); + EXPECT_EQ(content["uri"], "test://example.txt"); + EXPECT_EQ(content["mimeType"], "text/plain"); + EXPECT_EQ(content["text"], test_content); + + // Test modification + EXPECT_FALSE(resource.is_modified()); + resource.set_text("New content"); + EXPECT_TRUE(resource.is_modified()); + + // Test read after modification + content = resource.read(); + EXPECT_EQ(content["text"], "New content"); + EXPECT_FALSE(resource.is_modified()); +} + +// Test binary resource +TEST(McpResourceTest, BinaryResourceTest) { + // Create binary resource + mcp::binary_resource resource("test://example.bin", "example.bin", "application/octet-stream", "Example binary resource"); + + // Test metadata + mcp::json metadata = resource.get_metadata(); + EXPECT_EQ(metadata["uri"], "test://example.bin"); + EXPECT_EQ(metadata["name"], "example.bin"); + EXPECT_EQ(metadata["mimeType"], "application/octet-stream"); + EXPECT_EQ(metadata["description"], "Example binary resource"); + + // Test URI + EXPECT_EQ(resource.get_uri(), "test://example.bin"); + + // Test setting and getting binary data + std::vector test_data = {0x48, 0x65, 0x6C, 0x6C, 0x6F}; // "Hello" in ASCII + resource.set_data(test_data.data(), test_data.size()); + const auto& data = resource.get_data(); + EXPECT_EQ(data.size(), test_data.size()); + EXPECT_TRUE(std::equal(data.begin(), data.end(), test_data.begin())); + + // Test read + mcp::json content = resource.read(); + EXPECT_EQ(content["uri"], "test://example.bin"); + EXPECT_EQ(content["mimeType"], "application/octet-stream"); + + // Base64 encode the data for comparison + std::string base64_data = base64::encode(reinterpret_cast(test_data.data()), test_data.size()); + EXPECT_EQ(content["blob"], base64_data); + + // Test modification + EXPECT_FALSE(resource.is_modified()); + std::vector new_data = {0x57, 0x6F, 0x72, 0x6C, 0x64}; // "World" in ASCII + resource.set_data(new_data.data(), new_data.size()); + EXPECT_TRUE(resource.is_modified()); + + // Test read after modification + content = resource.read(); + std::string new_base64_data = base64::encode(reinterpret_cast(new_data.data()), new_data.size()); + EXPECT_EQ(content["blob"], new_base64_data); + EXPECT_FALSE(resource.is_modified()); +} + // Test file resource TEST_F(ResourceTest, FileResourceTest) { // Create file resource - mcp::file_resource resource(test_dir.string()); + mcp::file_resource resource(test_file.string()); // Test metadata mcp::json metadata = resource.get_metadata(); - // Check type - may be "file" or "file_resource" depending on implementation - EXPECT_TRUE(metadata["type"] == "file" || metadata["type"] == "file_resource"); - EXPECT_EQ(metadata["base_path"], test_dir.string()); + EXPECT_EQ(metadata["uri"], "file://" + test_file.string()); + EXPECT_EQ(metadata["name"], test_file.filename().string()); + EXPECT_EQ(metadata["mimeType"], "text/plain"); - // Create a new file for testing - std::string test_content = "New test content"; - std::string new_file_name = "new_test_file.txt"; - std::filesystem::path new_file_path = test_dir / new_file_name; + // Test URI + EXPECT_EQ(resource.get_uri(), "file://" + test_file.string()); - // Write new file + // Test read + mcp::json content = resource.read(); + EXPECT_EQ(content["uri"], "file://" + test_file.string()); + EXPECT_EQ(content["mimeType"], "text/plain"); + EXPECT_EQ(content["text"], "Test file content"); + + // Test modification detection + EXPECT_FALSE(resource.is_modified()); + + // Modify file { - std::ofstream file(new_file_path); - file << test_content; + std::ofstream file(test_file); + file << "Modified content"; file.close(); } - // Ensure file exists - ASSERT_TRUE(std::filesystem::exists(new_file_path)) << "New test file was not created successfully"; + // Test modification detection after file change + EXPECT_TRUE(resource.is_modified()); - // Try different read file parameter formats - std::vector read_params_list = { - // Format 1: Using relative path - {{"operation", "read"}, {"path", new_file_name}}, - - // Format 2: Using absolute path - {{"operation", "read"}, {"path", new_file_path.string()}}, - - // Format 3: Using filename parameter - {{"operation", "read"}, {"filename", new_file_name}}, - - // Format 4: Using file parameter - {{"operation", "read"}, {"file", new_file_name}}, - - // Format 5: Using name parameter - {{"operation", "read"}, {"name", new_file_name}} - }; + // Test read after modification + content = resource.read(); + EXPECT_EQ(content["text"], "Modified content"); + EXPECT_FALSE(resource.is_modified()); - bool read_success = false; - for (const auto& params : read_params_list) { - try { - std::cout << "Trying read file parameters: " << params.dump() << std::endl; - mcp::json read_result = resource.access(params); - - // If no exception is thrown, check the result - if (read_result.contains("content")) { - EXPECT_EQ(read_result["content"], test_content); - read_success = true; - std::cout << "Successfully read file using parameters: " << params.dump() << std::endl; - break; - } - } catch (const mcp::mcp_exception& e) { - // If read operation fails, output error message and try next format - std::cerr << "Read file failed: " << e.what() << ", parameters: " << params.dump() << std::endl; - } - } - - if (!read_success) { - std::cerr << "All read file parameter formats failed" << std::endl; - // Don't skip test, continue testing other operations - } - - // Try different write file parameter formats - std::string write_content = "Written content"; - std::string write_file_name = "write_test.txt"; - std::filesystem::path write_file_path = test_dir / write_file_name; - - std::vector write_params_list = { - // Format 1: Using path and content - {{"operation", "write"}, {"path", write_file_name}, {"content", write_content}}, - - // Format 2: Using filename and content - {{"operation", "write"}, {"filename", write_file_name}, {"content", write_content}}, - - // Format 3: Using file and content - {{"operation", "write"}, {"file", write_file_name}, {"content", write_content}}, - - // Format 4: Using name and content - {{"operation", "write"}, {"name", write_file_name}, {"content", write_content}}, - - // Format 5: Using absolute path - {{"operation", "write"}, {"path", write_file_path.string()}, {"content", write_content}} - }; - - bool write_success = false; - for (const auto& params : write_params_list) { - try { - std::cout << "Trying write file parameters: " << params.dump() << std::endl; - mcp::json write_result = resource.access(params); - - // If no exception is thrown, check if file was written - if (std::filesystem::exists(write_file_path)) { - std::ifstream file(write_file_path); - std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - EXPECT_EQ(content, write_content); - write_success = true; - std::cout << "Successfully wrote file using parameters: " << params.dump() << std::endl; - break; - } - } catch (const mcp::mcp_exception& e) { - // If write operation fails, output error message and try next format - std::cerr << "Write file failed: " << e.what() << ", parameters: " << params.dump() << std::endl; - } - } - - if (!write_success) { - std::cerr << "All write file parameter formats failed" << std::endl; - // Don't skip test, continue testing other operations - } - - // Try different list directory parameter formats - std::vector list_params_list = { - // Format 1: Empty path - {{"operation", "list"}, {"path", ""}}, - - // Format 2: Dot for current directory - {{"operation", "list"}, {"path", "."}}, - - // Format 3: Using dir parameter - {{"operation", "list"}, {"dir", ""}}, - - // Format 4: Using directory parameter - {{"operation", "list"}, {"directory", ""}}, - - // Format 5: No path parameter - {{"operation", "list"}} - }; - - bool list_success = false; - for (const auto& params : list_params_list) { - try { - std::cout << "Trying list directory parameters: " << params.dump() << std::endl; - mcp::json list_result = resource.access(params); - - // Check if result contains file list - if (list_result.contains("files") || list_result.contains("entries")) { - auto files = list_result.contains("files") ? list_result["files"] : list_result["entries"]; - EXPECT_GE(files.size(), 1) << "Directory should contain at least one file"; - list_success = true; - std::cout << "Successfully listed directory using parameters: " << params.dump() << std::endl; - break; - } - } catch (const mcp::mcp_exception& e) { - // If list directory operation fails, output error message and try next format - std::cerr << "List directory failed: " << e.what() << ", parameters: " << params.dump() << std::endl; - } - } - - if (!list_success) { - std::cerr << "All list directory parameter formats failed" << std::endl; - // Don't skip test, continue testing other operations - } - - // Try different delete file parameter formats - if (std::filesystem::exists(new_file_path)) { - std::vector delete_params_list = { - // Format 1: Using path - {{"operation", "delete"}, {"path", new_file_name}}, - - // Format 2: Using filename - {{"operation", "delete"}, {"filename", new_file_name}}, - - // Format 3: Using file - {{"operation", "delete"}, {"file", new_file_name}}, - - // Format 4: Using name - {{"operation", "delete"}, {"name", new_file_name}}, - - // Format 5: Using absolute path - {{"operation", "delete"}, {"path", new_file_path.string()}} - }; - - bool delete_success = false; - for (const auto& params : delete_params_list) { - try { - std::cout << "Trying delete file parameters: " << params.dump() << std::endl; - mcp::json delete_result = resource.access(params); - - // Check if file was deleted - if (!std::filesystem::exists(new_file_path)) { - delete_success = true; - std::cout << "Successfully deleted file using parameters: " << params.dump() << std::endl; - break; - } - } catch (const mcp::mcp_exception& e) { - // If delete operation fails, output error message and try next format - std::cerr << "Delete file failed: " << e.what() << ", parameters: " << params.dump() << std::endl; - } - } - - if (!delete_success) { - std::cerr << "All delete file parameter formats failed" << std::endl; - // Don't skip test, continue testing other operations - } - } - - // Test invalid operation - mcp::json invalid_params = { - {"operation", "invalid_op"}, - {"path", new_file_name} - }; - - EXPECT_THROW(resource.access(invalid_params), mcp::mcp_exception); + // Test file deletion detection + std::filesystem::remove(test_file); + EXPECT_TRUE(resource.is_modified()); } -// Mock API handler function -mcp::json test_api_handler(const mcp::json& params) { - if (params.contains("name")) { - return {{"message", "Hello, " + params["name"].get() + "!"}}; - } else { - return {{"message", "Hello, World!"}}; - } +// Test resource manager +TEST_F(ResourceTest, ResourceManagerTest) { + // Get resource manager instance + mcp::resource_manager& manager = mcp::resource_manager::instance(); + + // Create resources + auto text_res = std::make_shared("test://text.txt", "text.txt", "text/plain", "Text resource"); + auto binary_res = std::make_shared("test://binary.bin", "binary.bin", "application/octet-stream", "Binary resource"); + auto file_res = std::make_shared(test_file.string()); + + // Set content + text_res->set_text("Text resource content"); + std::vector binary_data = {0x42, 0x69, 0x6E, 0x61, 0x72, 0x79}; // "Binary" in ASCII + binary_res->set_data(binary_data.data(), binary_data.size()); + + // Register resources + manager.register_resource(text_res); + manager.register_resource(binary_res); + manager.register_resource(file_res); + + // Test list resources + mcp::json resources_list = manager.list_resources(); + EXPECT_EQ(resources_list["resources"].size(), 3); + + // Test get resource + auto retrieved_text_res = manager.get_resource("test://text.txt"); + ASSERT_NE(retrieved_text_res, nullptr); + EXPECT_EQ(retrieved_text_res->get_uri(), "test://text.txt"); + + auto retrieved_binary_res = manager.get_resource("test://binary.bin"); + ASSERT_NE(retrieved_binary_res, nullptr); + EXPECT_EQ(retrieved_binary_res->get_uri(), "test://binary.bin"); + + auto retrieved_file_res = manager.get_resource("file://" + test_file.string()); + ASSERT_NE(retrieved_file_res, nullptr); + EXPECT_EQ(retrieved_file_res->get_uri(), "file://" + test_file.string()); + + // Test unregister resource + EXPECT_TRUE(manager.unregister_resource("test://text.txt")); + EXPECT_EQ(manager.get_resource("test://text.txt"), nullptr); + + // Test resources list after unregister + resources_list = manager.list_resources(); + EXPECT_EQ(resources_list["resources"].size(), 2); + + // Test subscription + bool notification_received = false; + std::string notification_uri; + + int subscription_id = manager.subscribe("test://binary.bin", [&](const std::string& uri) { + notification_received = true; + notification_uri = uri; + }); + + // Modify resource and notify + binary_res->set_data(binary_data.data(), binary_data.size()); // This sets modified flag + manager.notify_resource_changed("test://binary.bin"); + + // Check notification + EXPECT_TRUE(notification_received); + EXPECT_EQ(notification_uri, "test://binary.bin"); + + // Test unsubscribe + EXPECT_TRUE(manager.unsubscribe(subscription_id)); + + // Reset notification flags + notification_received = false; + notification_uri = ""; + + // Modify and notify again + binary_res->set_data(binary_data.data(), binary_data.size()); + manager.notify_resource_changed("test://binary.bin"); + + // Check no notification after unsubscribe + EXPECT_FALSE(notification_received); + EXPECT_EQ(notification_uri, ""); + + // Clean up + manager.unregister_resource("test://binary.bin"); + manager.unregister_resource("file://" + test_file.string()); } -// Test API resource -TEST(McpResourceTest, ApiResourceTest) { - // Create API resource - mcp::api_resource resource("TestAPI", "Test API Resource"); - - // Register endpoints - resource.register_handler("hello", test_api_handler, "Greeting endpoint"); - resource.register_handler("echo", [](const mcp::json& params) -> mcp::json { - return params; - }, "Echo endpoint"); - - // Test metadata - mcp::json metadata = resource.get_metadata(); - // Check type - may be "api" or "api_resource" depending on implementation - EXPECT_TRUE(metadata["type"] == "api" || metadata["type"] == "api_resource"); - EXPECT_EQ(metadata["name"], "TestAPI"); - EXPECT_EQ(metadata["description"], "Test API Resource"); - EXPECT_TRUE(metadata.contains("endpoints")); - EXPECT_EQ(metadata["endpoints"].size(), 2); - - // Test accessing hello endpoint - mcp::json hello_params = { - {"endpoint", "hello"}, - {"name", "Test User"} - }; - mcp::json hello_result = resource.access(hello_params); - EXPECT_EQ(hello_result["message"], "Hello, Test User!"); - - // Test accessing echo endpoint - mcp::json echo_params = { - {"endpoint", "echo"}, - {"data", "Test data"}, - {"number", 42} - }; - mcp::json echo_result = resource.access(echo_params); - EXPECT_EQ(echo_result["data"], "Test data"); - EXPECT_EQ(echo_result["number"], 42); - - // Test accessing non-existent endpoint - mcp::json invalid_params = { - {"endpoint", "not_exists"} - }; - EXPECT_THROW(resource.access(invalid_params), mcp::mcp_exception); - - // Test missing endpoint parameter - mcp::json missing_params = { - {"data", "Test data"} - }; - EXPECT_THROW(resource.access(missing_params), mcp::mcp_exception); +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); } - -// Create custom resource class for testing -class custom_resource : public mcp::resource { -public: - custom_resource(const std::string& name, const std::string& description) - : name_(name), description_(description) {} - - mcp::json get_metadata() const override { - return { - {"type", "custom"}, - {"name", name_}, - {"description", description_} - }; - } - - mcp::json access(const mcp::json& params) const override { - if (!params.contains("action")) { - throw mcp::mcp_exception(mcp::error_code::invalid_params, "Missing action parameter"); - } - - std::string action = params["action"]; - - if (action == "get_info") { - return { - {"name", name_}, - {"description", description_}, - {"timestamp", std::time(nullptr)} - }; - } else if (action == "process") { - if (!params.contains("data")) { - throw mcp::mcp_exception(mcp::error_code::invalid_params, "Missing data parameter"); - } - - std::string data = params["data"]; - return { - {"processed", "Processed: " + data}, - {"status", "success"} - }; - } else { - throw mcp::mcp_exception(mcp::error_code::invalid_params, "Invalid action: " + action); - } - } - -private: - std::string name_; - std::string description_; -}; - -// Test custom resource -TEST(McpResourceTest, CustomResourceTest) { - // Create custom resource - custom_resource resource("CustomResource", "Custom resource test"); - - // Test metadata - mcp::json metadata = resource.get_metadata(); - EXPECT_EQ(metadata["type"], "custom"); - EXPECT_EQ(metadata["name"], "CustomResource"); - EXPECT_EQ(metadata["description"], "Custom resource test"); - - // Test get_info operation - mcp::json info_params = { - {"action", "get_info"} - }; - mcp::json info_result = resource.access(info_params); - EXPECT_EQ(info_result["name"], "CustomResource"); - EXPECT_EQ(info_result["description"], "Custom resource test"); - EXPECT_TRUE(info_result.contains("timestamp")); - - // Test process operation - mcp::json process_params = { - {"action", "process"}, - {"data", "Test data"} - }; - mcp::json process_result = resource.access(process_params); - EXPECT_EQ(process_result["processed"], "Processed: Test data"); - EXPECT_EQ(process_result["status"], "success"); - - // Test invalid operation - mcp::json invalid_params = { - {"action", "invalid_action"} - }; - EXPECT_THROW(resource.access(invalid_params), mcp::mcp_exception); - - // Test missing parameter - mcp::json missing_params = { - {"action", "process"} - }; - EXPECT_THROW(resource.access(missing_params), mcp::mcp_exception); -} \ No newline at end of file