update resource
parent
175d668642
commit
62eb1ad2e4
|
@ -3,7 +3,7 @@
|
||||||
* @brief MCP Client implementation
|
* @brief MCP Client implementation
|
||||||
*
|
*
|
||||||
* This file implements the client-side functionality for the Model Context Protocol.
|
* 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
|
#ifndef MCP_CLIENT_H
|
||||||
|
@ -40,6 +40,13 @@ public:
|
||||||
*/
|
*/
|
||||||
client(const std::string& host, int port = 8080, const json& capabilities = json::object());
|
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
|
* @brief Destructor
|
||||||
*/
|
*/
|
||||||
|
@ -124,14 +131,6 @@ public:
|
||||||
*/
|
*/
|
||||||
std::vector<tool> get_tools();
|
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
|
* @brief Get client capabilities
|
||||||
* @return The client capabilities
|
* @return The client capabilities
|
||||||
|
@ -139,15 +138,34 @@ public:
|
||||||
json get_capabilities();
|
json get_capabilities();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Access a resource
|
* @brief List available resources
|
||||||
* @param resource_path The path to the resource
|
* @param cursor Optional cursor for pagination
|
||||||
* @param params Additional parameters for the resource
|
* @return List of resources
|
||||||
* @return The resource data
|
|
||||||
* @throws mcp_exception on error
|
|
||||||
*/
|
*/
|
||||||
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:
|
private:
|
||||||
|
std::string base_url_;
|
||||||
std::string host_;
|
std::string host_;
|
||||||
int port_;
|
int port_;
|
||||||
std::string auth_token_;
|
std::string auth_token_;
|
||||||
|
@ -165,7 +183,8 @@ private:
|
||||||
mutable std::mutex mutex_;
|
mutable std::mutex mutex_;
|
||||||
|
|
||||||
// Initialize the client
|
// 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
|
// Send a JSON-RPC request and get the response
|
||||||
json send_jsonrpc(const request& req);
|
json send_jsonrpc(const request& req);
|
||||||
|
|
|
@ -3,13 +3,19 @@
|
||||||
* @brief Resource implementation for MCP
|
* @brief Resource implementation for MCP
|
||||||
*
|
*
|
||||||
* This file defines the base resource class and common resource types for the MCP protocol.
|
* 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
|
#ifndef MCP_RESOURCE_H
|
||||||
#define MCP_RESOURCE_H
|
#define MCP_RESOURCE_H
|
||||||
|
|
||||||
#include "mcp_message.h"
|
#include "mcp_message.h"
|
||||||
|
#include "base64.hpp"
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
namespace mcp {
|
namespace mcp {
|
||||||
|
|
||||||
|
@ -18,7 +24,7 @@ namespace mcp {
|
||||||
* @brief Base class for MCP resources
|
* @brief Base class for MCP resources
|
||||||
*
|
*
|
||||||
* The resource class defines the interface for resources that can be
|
* 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 {
|
class resource {
|
||||||
public:
|
public:
|
||||||
|
@ -31,99 +37,271 @@ public:
|
||||||
virtual json get_metadata() const = 0;
|
virtual json get_metadata() const = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Access the resource
|
* @brief Read the resource content
|
||||||
* @param params Parameters for accessing the resource
|
* @return The resource content as JSON
|
||||||
* @return The resource data
|
|
||||||
*/
|
*/
|
||||||
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
|
* @class file_resource
|
||||||
* @brief Resource for file system operations
|
* @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:
|
public:
|
||||||
/**
|
/**
|
||||||
* @brief Constructor
|
* @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
|
* @brief Read the resource content
|
||||||
* @return Metadata as JSON
|
* @return The resource content as JSON
|
||||||
*/
|
*/
|
||||||
json get_metadata() const override;
|
json read() const override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Access the resource
|
* @brief Check if the resource has been modified
|
||||||
* @param params Parameters for accessing the resource
|
* @return True if the resource has been modified since last read
|
||||||
* @return The resource data
|
|
||||||
*/
|
*/
|
||||||
json access(const json& params) const override;
|
bool is_modified() const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string base_path_;
|
std::string file_path_;
|
||||||
|
mutable time_t last_modified_;
|
||||||
|
|
||||||
// Helper methods
|
/**
|
||||||
json read_file(const std::string& path) const;
|
* @brief Guess the MIME type from file extension
|
||||||
json write_file(const std::string& path, const std::string& content) const;
|
* @param file_path The file path
|
||||||
json delete_file(const std::string& path) const;
|
* @return The guessed MIME type
|
||||||
json list_directory(const std::string& path) const;
|
*/
|
||||||
|
static std::string guess_mime_type(const std::string& file_path);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class api_resource
|
* @class resource_manager
|
||||||
* @brief Resource for custom API endpoints
|
* @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:
|
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
|
* @brief Register a resource
|
||||||
* @param name Resource name
|
* @param resource Shared pointer to the resource
|
||||||
* @param description Resource description
|
|
||||||
*/
|
*/
|
||||||
api_resource(const std::string& name, const std::string& description);
|
void register_resource(std::shared_ptr<resource> resource);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get resource metadata
|
* @brief Unregister a resource
|
||||||
* @return Metadata as JSON
|
* @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
|
* @brief Get a resource by URI
|
||||||
* @param params Parameters for accessing the resource
|
* @param uri The URI of the resource
|
||||||
* @return The resource data
|
* @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
|
* @brief List all registered resources
|
||||||
* @param endpoint The endpoint name
|
* @return JSON array of resource metadata
|
||||||
* @param handler The handler function
|
|
||||||
* @param description Description of the endpoint
|
|
||||||
*/
|
*/
|
||||||
void register_handler(const std::string& endpoint,
|
json list_resources() const;
|
||||||
handler_func handler,
|
|
||||||
const std::string& description = "");
|
/**
|
||||||
|
* @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:
|
private:
|
||||||
struct endpoint_info {
|
resource_manager() = default;
|
||||||
handler_func handler;
|
~resource_manager() = default;
|
||||||
std::string description;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::string name_;
|
resource_manager(const resource_manager&) = delete;
|
||||||
std::string description_;
|
resource_manager& operator=(const resource_manager&) = delete;
|
||||||
std::map<std::string, endpoint_info> endpoints_;
|
|
||||||
|
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
|
} // namespace mcp
|
||||||
|
|
|
@ -14,16 +14,31 @@ namespace mcp {
|
||||||
client::client(const std::string& host, int port, const json& capabilities)
|
client::client(const std::string& host, int port, const json& capabilities)
|
||||||
: host_(host), port_(port), capabilities_(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() {
|
client::~client() {
|
||||||
// httplib::Client will be automatically destroyed
|
// httplib::Client will be automatically destroyed
|
||||||
}
|
}
|
||||||
|
|
||||||
void client::init_client() {
|
void client::init_client(const std::string& host, int port) {
|
||||||
// Create the HTTP client
|
// 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
|
// Set timeout
|
||||||
http_client_->set_connection_timeout(timeout_seconds_, 0);
|
http_client_->set_connection_timeout(timeout_seconds_, 0);
|
||||||
|
@ -157,27 +172,32 @@ std::vector<tool> client::get_tools() {
|
||||||
return 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() {
|
json client::get_capabilities() {
|
||||||
return capabilities_;
|
return capabilities_;
|
||||||
}
|
}
|
||||||
|
|
||||||
json client::access_resource(const std::string& resource_path, const json& params) {
|
json client::list_resources(const std::string& cursor) {
|
||||||
json request_params = {
|
json params = json::object();
|
||||||
{"path", resource_path}
|
if (!cursor.empty()) {
|
||||||
};
|
params["cursor"] = cursor;
|
||||||
|
|
||||||
// Add any additional parameters
|
|
||||||
for (auto it = params.begin(); it != params.end(); ++it) {
|
|
||||||
request_params[it.key()] = it.value();
|
|
||||||
}
|
}
|
||||||
|
return send_request("resources/list", params).result;
|
||||||
|
}
|
||||||
|
|
||||||
return send_request("resources/access", request_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) {
|
json client::send_jsonrpc(const request& req) {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* @brief Resource implementation for MCP
|
* @brief Resource implementation for MCP
|
||||||
*
|
*
|
||||||
* This file implements the resource classes for the Model Context Protocol.
|
* 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 "mcp_resource.h"
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
@ -11,247 +11,325 @@
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
#include <chrono>
|
||||||
|
#include <ctime>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
namespace mcp {
|
namespace mcp {
|
||||||
|
|
||||||
// file_resource implementation
|
// text_resource implementation
|
||||||
file_resource::file_resource(const std::string& base_path)
|
text_resource::text_resource(const std::string& uri,
|
||||||
: base_path_(base_path) {
|
const std::string& name,
|
||||||
// Ensure base path exists
|
const std::string& mime_type,
|
||||||
if (!fs::exists(base_path_)) {
|
const std::string& description)
|
||||||
throw mcp_exception(error_code::internal_error,
|
: uri_(uri), name_(name), mime_type_(mime_type), description_(description), modified_(false) {
|
||||||
"Base path does not exist: " + base_path_);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
json file_resource::get_metadata() const {
|
json text_resource::get_metadata() const {
|
||||||
return {
|
return {
|
||||||
{"type", "file_resource"},
|
{"uri", uri_},
|
||||||
{"base_path", base_path_},
|
{"name", name_},
|
||||||
{"description", "File system resource for accessing files"}
|
{"mimeType", mime_type_},
|
||||||
|
{"description", description_}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
json file_resource::access(const json& params) const {
|
json text_resource::read() const {
|
||||||
// Extract the path from the parameters
|
modified_ = false;
|
||||||
if (!params.contains("path")) {
|
return {
|
||||||
throw mcp_exception(error_code::invalid_params, "Missing 'path' parameter");
|
{"uri", uri_},
|
||||||
}
|
{"mimeType", mime_type_},
|
||||||
|
{"text", text_}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
std::string path = params["path"];
|
bool text_resource::is_modified() const {
|
||||||
|
return modified_;
|
||||||
|
}
|
||||||
|
|
||||||
// Remove trailing slash if present
|
std::string text_resource::get_uri() const {
|
||||||
if (!path.empty() && path.back() == '/') {
|
return uri_;
|
||||||
path.pop_back();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Handle different operations
|
void text_resource::set_text(const std::string& text) {
|
||||||
if (params.contains("operation")) {
|
if (text_ != text) {
|
||||||
std::string operation = params["operation"];
|
text_ = text;
|
||||||
|
modified_ = true;
|
||||||
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 file_resource::read_file(const std::string& path) const {
|
std::string text_resource::get_text() const {
|
||||||
std::string full_path = base_path_ + path;
|
return text_;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if file exists
|
// binary_resource implementation
|
||||||
if (!fs::exists(full_path) || !fs::is_regular_file(full_path)) {
|
binary_resource::binary_resource(const std::string& uri,
|
||||||
throw mcp_exception(error_code::invalid_params, "File not found: " + path);
|
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;
|
||||||
|
|
||||||
|
// 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
|
// Read file content
|
||||||
std::ifstream file(full_path, std::ios::binary);
|
std::ifstream file(file_path_, std::ios::binary);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
throw mcp_exception(error_code::internal_error,
|
throw mcp_exception(error_code::internal_error,
|
||||||
"Failed to open file: " + path);
|
"Failed to open file: " + file_path_);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::stringstream buffer;
|
std::stringstream buffer;
|
||||||
buffer << file.rdbuf();
|
buffer << file.rdbuf();
|
||||||
|
|
||||||
// Get file info
|
// Update text content
|
||||||
std::string content = buffer.str();
|
const_cast<file_resource*>(this)->set_text(buffer.str());
|
||||||
std::string ext = fs::path(full_path).extension().string();
|
|
||||||
|
// 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 {
|
return {
|
||||||
{"path", path},
|
{"uri", uri_},
|
||||||
{"content", content},
|
{"mimeType", mime_type_},
|
||||||
{"size", content.size()},
|
{"text", text_}
|
||||||
{"extension", ext},
|
|
||||||
{"last_modified", fs::last_write_time(full_path).time_since_epoch().count()}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
json file_resource::write_file(const std::string& path, const std::string& content) const {
|
bool file_resource::is_modified() const {
|
||||||
std::string full_path = base_path_ + path;
|
if (!fs::exists(file_path_)) {
|
||||||
|
return true; // File was deleted
|
||||||
|
}
|
||||||
|
|
||||||
// Create directories if they don't exist
|
time_t current_modified = fs::last_write_time(file_path_).time_since_epoch().count();
|
||||||
fs::path dir_path = fs::path(full_path).parent_path();
|
return current_modified != last_modified_;
|
||||||
if (!fs::exists(dir_path)) {
|
}
|
||||||
try {
|
|
||||||
fs::create_directories(dir_path);
|
std::string file_resource::guess_mime_type(const std::string& file_path) {
|
||||||
} catch (const std::exception& e) {
|
std::string ext = fs::path(file_path).extension().string();
|
||||||
throw mcp_exception(error_code::internal_error,
|
|
||||||
"Failed to create directory: " + std::string(e.what()));
|
// 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
|
return true;
|
||||||
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()}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
json file_resource::delete_file(const std::string& path) const {
|
std::shared_ptr<resource> resource_manager::get_resource(const std::string& uri) const {
|
||||||
std::string full_path = base_path_ + path;
|
std::lock_guard<std::mutex> lock(g_resource_manager_mutex);
|
||||||
|
|
||||||
// Check if file exists
|
auto it = resources_.find(uri);
|
||||||
if (!fs::exists(full_path)) {
|
if (it == resources_.end()) {
|
||||||
throw mcp_exception(error_code::invalid_params, "File not found: " + path);
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete file
|
return it->second;
|
||||||
try {
|
}
|
||||||
fs::remove(full_path);
|
|
||||||
} catch (const std::exception& e) {
|
json resource_manager::list_resources() const {
|
||||||
throw mcp_exception(error_code::internal_error,
|
std::lock_guard<std::mutex> lock(g_resource_manager_mutex);
|
||||||
"Failed to delete file: " + std::string(e.what()));
|
|
||||||
|
json resources = json::array();
|
||||||
|
|
||||||
|
for (const auto& [uri, res] : resources_) {
|
||||||
|
resources.push_back(res->get_metadata());
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
{"success", true},
|
{"resources", resources}
|
||||||
{"message", "File deleted successfully"},
|
|
||||||
{"path", path}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
json file_resource::list_directory(const std::string& path) const {
|
int resource_manager::subscribe(const std::string& uri, std::function<void(const std::string&)> callback) {
|
||||||
std::string full_path = base_path_ + path;
|
if (!callback) {
|
||||||
|
throw mcp_exception(error_code::invalid_params, "Cannot subscribe with null callback");
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// List directory contents
|
std::lock_guard<std::mutex> lock(g_resource_manager_mutex);
|
||||||
json entries = json::array();
|
|
||||||
|
|
||||||
for (const auto& entry : fs::directory_iterator(full_path)) {
|
// Check if resource exists
|
||||||
std::string entry_path = entry.path().string();
|
auto it = resources_.find(uri);
|
||||||
// Convert to relative path
|
if (it == resources_.end()) {
|
||||||
entry_path = entry_path.substr(base_path_.length());
|
throw mcp_exception(error_code::invalid_params, "Resource not found: " + uri);
|
||||||
|
}
|
||||||
|
|
||||||
json entry_json = {
|
int id = next_subscription_id_++;
|
||||||
{"name", entry.path().filename().string()},
|
subscriptions_[id] = std::make_pair(uri, callback);
|
||||||
{"path", entry_path},
|
|
||||||
{"is_directory", entry.is_directory()}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!entry.is_directory()) {
|
return id;
|
||||||
entry_json["size"] = entry.file_size();
|
}
|
||||||
entry_json["last_modified"] = fs::last_write_time(entry.path()).time_since_epoch().count();
|
|
||||||
|
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
|
} // namespace mcp
|
|
@ -14,6 +14,14 @@ server::server(const std::string& host, int port)
|
||||||
: host_(host), port_(port), name_("MCP Server"), version_(MCP_VERSION) {
|
: host_(host), port_(port), name_("MCP Server"), version_(MCP_VERSION) {
|
||||||
|
|
||||||
http_server_ = std::make_unique<httplib::Server>();
|
http_server_ = std::make_unique<httplib::Server>();
|
||||||
|
|
||||||
|
// Set default capabilities
|
||||||
|
capabilities_ = {
|
||||||
|
{"resources", {
|
||||||
|
{"subscribe", true},
|
||||||
|
{"listChanged", true}
|
||||||
|
}}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
server::~server() {
|
server::~server() {
|
||||||
|
@ -99,35 +107,74 @@ void server::register_resource(const std::string& path, std::shared_ptr<resource
|
||||||
resources_[path] = resource;
|
resources_[path] = resource;
|
||||||
|
|
||||||
// Register methods for resource access
|
// Register methods for resource access
|
||||||
if (method_handlers_.find("resources/metadata") == method_handlers_.end()) {
|
if (method_handlers_.find("resources/read") == method_handlers_.end()) {
|
||||||
method_handlers_["resources/metadata"] = [this](const json& params) {
|
method_handlers_["resources/read"] = [this](const json& params) {
|
||||||
if (!params.contains("path")) {
|
if (!params.contains("uri")) {
|
||||||
throw mcp_exception(error_code::invalid_params, "Missing 'path' parameter");
|
throw mcp_exception(error_code::invalid_params, "Missing 'uri' parameter");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string path = params["path"];
|
std::string uri = params["uri"];
|
||||||
auto it = resources_.find(path);
|
auto it = resources_.find(uri);
|
||||||
if (it == resources_.end()) {
|
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()) {
|
if (method_handlers_.find("resources/list") == method_handlers_.end()) {
|
||||||
method_handlers_["resources/access"] = [this](const json& params) {
|
method_handlers_["resources/list"] = [this](const json& params) {
|
||||||
if (!params.contains("path")) {
|
json resources = json::array();
|
||||||
throw mcp_exception(error_code::invalid_params, "Missing 'path' parameter");
|
|
||||||
|
for (const auto& [uri, res] : resources_) {
|
||||||
|
resources.push_back(res->get_metadata());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string path = params["path"];
|
json result = {
|
||||||
auto it = resources_.find(path);
|
{"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()) {
|
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_resource.h"
|
||||||
|
#include "mcp_server.h"
|
||||||
|
#include "mcp_client.h"
|
||||||
|
#include "base64.hpp"
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
#include <gmock/gmock.h>
|
#include <gmock/gmock.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <thread>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
// Create a test directory and file
|
// Create a test directory and file
|
||||||
class ResourceTest : public ::testing::Test {
|
class ResourceTest : public ::testing::Test {
|
||||||
|
@ -44,362 +49,213 @@ protected:
|
||||||
std::filesystem::path test_file;
|
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 file resource
|
||||||
TEST_F(ResourceTest, FileResourceTest) {
|
TEST_F(ResourceTest, FileResourceTest) {
|
||||||
// Create file resource
|
// Create file resource
|
||||||
mcp::file_resource resource(test_dir.string());
|
mcp::file_resource resource(test_file.string());
|
||||||
|
|
||||||
// Test metadata
|
// Test metadata
|
||||||
mcp::json metadata = resource.get_metadata();
|
mcp::json metadata = resource.get_metadata();
|
||||||
// Check type - may be "file" or "file_resource" depending on implementation
|
EXPECT_EQ(metadata["uri"], "file://" + test_file.string());
|
||||||
EXPECT_TRUE(metadata["type"] == "file" || metadata["type"] == "file_resource");
|
EXPECT_EQ(metadata["name"], test_file.filename().string());
|
||||||
EXPECT_EQ(metadata["base_path"], test_dir.string());
|
EXPECT_EQ(metadata["mimeType"], "text/plain");
|
||||||
|
|
||||||
// Create a new file for testing
|
// Test URI
|
||||||
std::string test_content = "New test content";
|
EXPECT_EQ(resource.get_uri(), "file://" + test_file.string());
|
||||||
std::string new_file_name = "new_test_file.txt";
|
|
||||||
std::filesystem::path new_file_path = test_dir / new_file_name;
|
|
||||||
|
|
||||||
// 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);
|
std::ofstream file(test_file);
|
||||||
file << test_content;
|
file << "Modified content";
|
||||||
file.close();
|
file.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure file exists
|
// Test modification detection after file change
|
||||||
ASSERT_TRUE(std::filesystem::exists(new_file_path)) << "New test file was not created successfully";
|
EXPECT_TRUE(resource.is_modified());
|
||||||
|
|
||||||
// Try different read file parameter formats
|
// Test read after modification
|
||||||
std::vector<mcp::json> read_params_list = {
|
content = resource.read();
|
||||||
// Format 1: Using relative path
|
EXPECT_EQ(content["text"], "Modified content");
|
||||||
{{"operation", "read"}, {"path", new_file_name}},
|
EXPECT_FALSE(resource.is_modified());
|
||||||
|
|
||||||
// Format 2: Using absolute path
|
// Test file deletion detection
|
||||||
{{"operation", "read"}, {"path", new_file_path.string()}},
|
std::filesystem::remove(test_file);
|
||||||
|
EXPECT_TRUE(resource.is_modified());
|
||||||
// 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}}
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock API handler function
|
// Test resource manager
|
||||||
mcp::json test_api_handler(const mcp::json& params) {
|
TEST_F(ResourceTest, ResourceManagerTest) {
|
||||||
if (params.contains("name")) {
|
// Get resource manager instance
|
||||||
return {{"message", "Hello, " + params["name"].get<std::string>() + "!"}};
|
mcp::resource_manager& manager = mcp::resource_manager::instance();
|
||||||
} else {
|
|
||||||
return {{"message", "Hello, World!"}};
|
// 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
|
int main(int argc, char **argv) {
|
||||||
TEST(McpResourceTest, ApiResourceTest) {
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
// Create API resource
|
return RUN_ALL_TESTS();
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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