update resource
parent
175d668642
commit
62eb1ad2e4
|
@ -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<tool> 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);
|
||||
|
|
|
@ -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 <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
|
||||
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<uint8_t>& get_data() const;
|
||||
|
||||
protected:
|
||||
std::string uri_;
|
||||
std::string name_;
|
||||
std::string mime_type_;
|
||||
std::string description_;
|
||||
std::vector<uint8_t> 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<json(const json&)>;
|
||||
/**
|
||||
* @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> 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<resource> 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<void(const std::string&)> 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<std::string, endpoint_info> endpoints_;
|
||||
resource_manager(const resource_manager&) = delete;
|
||||
resource_manager& operator=(const resource_manager&) = delete;
|
||||
|
||||
std::map<std::string, std::shared_ptr<resource>> resources_;
|
||||
std::map<int, std::pair<std::string, std::function<void(const std::string&)>>> subscriptions_;
|
||||
int next_subscription_id_ = 1;
|
||||
};
|
||||
|
||||
} // namespace mcp
|
||||
|
|
|
@ -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<httplib::Client>(host_.c_str(), port_);
|
||||
http_client_ = std::make_unique<httplib::Client>(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<httplib::Client>(base_url.c_str());
|
||||
|
||||
// Set timeout
|
||||
http_client_->set_connection_timeout(timeout_seconds_, 0);
|
||||
|
@ -157,27 +172,32 @@ std::vector<tool> 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) {
|
||||
|
|
|
@ -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 <filesystem>
|
||||
|
@ -11,247 +11,325 @@
|
|||
#include <sstream>
|
||||
#include <algorithm>
|
||||
#include <stdexcept>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <mutex>
|
||||
|
||||
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<const char*>(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<uint8_t>& 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<file_resource*>(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> resource) {
|
||||
if (!resource) {
|
||||
throw mcp_exception(error_code::invalid_params, "Cannot register null resource");
|
||||
}
|
||||
|
||||
std::string uri = resource->get_uri();
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_resource_manager_mutex);
|
||||
resources_[uri] = resource;
|
||||
}
|
||||
|
||||
bool resource_manager::unregister_resource(const std::string& uri) {
|
||||
std::lock_guard<std::mutex> 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> resource_manager::get_resource(const std::string& uri) const {
|
||||
std::lock_guard<std::mutex> 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<std::mutex> 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<void(const std::string&)> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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
|
|
@ -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<httplib::Server>();
|
||||
|
||||
// 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_ptr<resource
|
|||
resources_[path] = resource;
|
||||
|
||||
// Register methods for resource access
|
||||
if (method_handlers_.find("resources/metadata") == method_handlers_.end()) {
|
||||
method_handlers_["resources/metadata"] = [this](const json& params) {
|
||||
if (!params.contains("path")) {
|
||||
throw mcp_exception(error_code::invalid_params, "Missing 'path' parameter");
|
||||
if (method_handlers_.find("resources/read") == method_handlers_.end()) {
|
||||
method_handlers_["resources/read"] = [this](const json& params) {
|
||||
if (!params.contains("uri")) {
|
||||
throw mcp_exception(error_code::invalid_params, "Missing 'uri' parameter");
|
||||
}
|
||||
|
||||
std::string path = params["path"];
|
||||
auto it = resources_.find(path);
|
||||
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->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()}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,12 +6,17 @@
|
|||
*/
|
||||
|
||||
#include "mcp_resource.h"
|
||||
#include "mcp_server.h"
|
||||
#include "mcp_client.h"
|
||||
#include "base64.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
// 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<uint8_t> 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<const char*>(test_data.data()), test_data.size());
|
||||
EXPECT_EQ(content["blob"], base64_data);
|
||||
|
||||
// Test modification
|
||||
EXPECT_FALSE(resource.is_modified());
|
||||
std::vector<uint8_t> 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<const char*>(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<mcp::json> 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<mcp::json> 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<char>(file)), std::istreambuf_iterator<char>());
|
||||
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<mcp::json> 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<mcp::json> 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<std::string>() + "!"}};
|
||||
} 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<mcp::text_resource>("test://text.txt", "text.txt", "text/plain", "Text resource");
|
||||
auto binary_res = std::make_shared<mcp::binary_resource>("test://binary.bin", "binary.bin", "application/octet-stream", "Binary resource");
|
||||
auto file_res = std::make_shared<mcp::file_resource>(test_file.string());
|
||||
|
||||
// Set content
|
||||
text_res->set_text("Text resource content");
|
||||
std::vector<uint8_t> 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);
|
||||
}
|
Loading…
Reference in New Issue