cpp-mcp/examples/host_example.cpp

429 lines
14 KiB
C++

/**
* @file host_example.cpp
* @brief Example MCP host implementation combining server and client capabilities
*/
#include "mcp_server.h"
#include "mcp_client.h"
#include "mcp_resource.h"
#include "mcp_tools.h"
#include "mcp_workflow_resource.h"
#include "custom_agent.h"
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <chrono>
#include <thread>
#include <functional>
#include <mutex>
#include <condition_variable>
#include <atomic>
/**
* @class mcp_host
* @brief A host that combines MCP server and client capabilities
*
* This class provides an example of a host application that can act both
* as an MCP server (exposing resources and tools) and an MCP client
* (connecting to other MCP servers).
*/
class mcp_host {
public:
/**
* @brief Constructor
* @param name Host name
* @param host Host address to bind to
* @param port Port to listen on
*/
mcp_host(const std::string& name, const std::string& host = "localhost", int port = 8080)
: name_(name), running_(false) {
// Create the server
server_ = std::make_unique<mcp::server>(host, port);
server_->set_name(name);
// Set up workflow resource
workflow_resource_ = std::make_shared<mcp::workflow_resource>();
server_->register_resource("/workflows", workflow_resource_);
// Set up agent resource
agent_resource_ = std::make_shared<mcp::agent_resource>();
server_->register_resource("/agents", agent_resource_);
// Register host management API
host_api_ = std::make_shared<mcp::api_resource>("Host API", "Host management API");
// Register GET /status endpoint
host_api_->register_handler(
mcp::http_method::get,
"/status",
[this](const mcp::request& req) -> mcp::response {
mcp::response res;
res.set_json_body({
{"name", name_},
{"running", running_.load()},
{"connections", connections_.size()},
{"agents", agent_resource_->get_agent_names()},
{"workflows", workflow_resource_->get_workflow_names()}
});
return res;
},
"Get host status"
);
// Register POST /connect endpoint
host_api_->register_handler(
mcp::http_method::post,
"/connect",
[this](const mcp::request& req) -> mcp::response {
mcp::response res;
try {
mcp::json body = req.json_body();
if (!body.contains("host") || !body["host"].is_string() ||
!body.contains("port") || !body["port"].is_number_integer()) {
res.set_error(mcp::error_code::invalid_request,
"Missing required fields: host (string) and port (integer)");
return res;
}
std::string host = body["host"];
int port = body["port"];
std::string connection_id = connect_to_server(host, port);
res.set_json_body({
{"connection_id", connection_id},
{"status", "connected"}
});
} catch (const std::exception& e) {
res.set_error(mcp::error_code::internal_server_error, e.what());
}
return res;
},
"Connect to another MCP server"
);
// Register GET /connections endpoint
host_api_->register_handler(
mcp::http_method::get,
"/connections",
[this](const mcp::request& req) -> mcp::response {
mcp::response res;
mcp::json connections = mcp::json::array();
std::lock_guard<std::mutex> lock(connections_mutex_);
for (const auto& [id, conn] : connections_) {
connections.push_back({
{"id", id},
{"host", conn.host},
{"port", conn.port}
});
}
res.set_json_body(connections);
return res;
},
"List all connections"
);
// Register DELETE /connections/{id} endpoint
host_api_->register_handler(
mcp::http_method::delete_,
"/connections/:id",
[this](const mcp::request& req) -> mcp::response {
mcp::response res;
// Extract connection ID from path
std::string path = req.path;
size_t pos = path.find_last_of('/');
if (pos == std::string::npos) {
res.set_error(mcp::error_code::invalid_request, "Invalid path");
return res;
}
std::string connection_id = path.substr(pos + 1);
bool success = disconnect_from_server(connection_id);
if (success) {
res.status_code = 204; // No content
} else {
res.set_error(mcp::error_code::not_found,
"Connection not found: " + connection_id);
}
return res;
},
"Disconnect from a server"
);
server_->register_resource("/host", host_api_);
}
/**
* @brief Destructor
*/
~mcp_host() {
stop();
}
/**
* @brief Start the host
* @param blocking If true, this call blocks until the host stops
*/
void start(bool blocking = false) {
if (running_) {
return;
}
running_ = true;
// Start the server in a separate thread
server_thread_ = std::thread([this]() {
try {
server_->start(true); // Blocking call
} catch (const std::exception& e) {
std::cerr << "Server error: " << e.what() << std::endl;
running_ = false;
}
});
if (blocking) {
// Wait for the server to exit
if (server_thread_.joinable()) {
server_thread_.join();
}
}
}
/**
* @brief Stop the host
*/
void stop() {
if (!running_) {
return;
}
running_ = false;
// Disconnect from all servers
{
std::lock_guard<std::mutex> lock(connections_mutex_);
connections_.clear();
}
// Stop the server
server_->stop();
// Wait for the server thread to exit
if (server_thread_.joinable()) {
server_thread_.join();
}
}
/**
* @brief Register a tool
* @param tool The tool to register
* @param handler The function to call when the tool is invoked
*/
void register_tool(const mcp::tool& tool, mcp::tool_handler handler) {
server_->register_tool(tool, handler);
}
/**
* @brief Register a resource
* @param path The path to mount the resource at
* @param resource The resource to register
*/
void register_resource(const std::string& path, std::shared_ptr<mcp::resource> resource) {
server_->register_resource(path, resource);
}
/**
* @brief Register an agent
* @param agent The agent to register
*/
void register_agent(std::shared_ptr<mcp::agent> agent) {
agent_resource_->register_agent(agent);
}
/**
* @brief Register a workflow
* @param workflow The workflow to register
*/
void register_workflow(const mcp::workflow& workflow) {
workflow_resource_->register_workflow(workflow);
}
/**
* @brief Connect to another MCP server
* @param host The server host
* @param port The server port
* @return Connection ID
*/
std::string connect_to_server(const std::string& host, int port) {
// Create a unique connection ID
static int next_id = 1;
std::string connection_id = "conn_" + std::to_string(next_id++);
// Create the client
auto client = std::make_shared<mcp::client>(host, port);
// Test the connection by getting server info
try {
mcp::json server_info = client->get_server_info();
std::cout << "Connected to server: " << server_info["name"].get<std::string>()
<< " (version " << server_info["version"].get<std::string>() << ")"
<< std::endl;
} catch (const std::exception& e) {
throw std::runtime_error(
"Failed to connect to server at " + host + ":" + std::to_string(port) +
" - " + e.what()
);
}
// Store the connection
{
std::lock_guard<std::mutex> lock(connections_mutex_);
connections_[connection_id] = {host, port, client};
}
return connection_id;
}
/**
* @brief Disconnect from a server
* @param connection_id The connection ID
* @return True if the connection was found and removed
*/
bool disconnect_from_server(const std::string& connection_id) {
std::lock_guard<std::mutex> lock(connections_mutex_);
return connections_.erase(connection_id) > 0;
}
/**
* @brief Get a client by connection ID
* @param connection_id The connection ID
* @return The client, or nullptr if not found
*/
std::shared_ptr<mcp::client> get_client(const std::string& connection_id) {
std::lock_guard<std::mutex> lock(connections_mutex_);
auto it = connections_.find(connection_id);
if (it != connections_.end()) {
return it->second.client;
}
return nullptr;
}
/**
* @brief Call a tool on a connected server
* @param connection_id The connection ID
* @param tool_name The name of the tool to call
* @param parameters The parameters to pass to the tool
* @return The result of the tool call
*/
mcp::json call_remote_tool(const std::string& connection_id,
const std::string& tool_name,
const mcp::json& parameters = mcp::json::object()) {
auto client = get_client(connection_id);
if (!client) {
throw std::runtime_error("Connection not found: " + connection_id);
}
return client->call_tool(tool_name, parameters);
}
private:
struct connection {
std::string host;
int port;
std::shared_ptr<mcp::client> client;
};
std::string name_;
std::atomic<bool> running_;
std::unique_ptr<mcp::server> server_;
std::thread server_thread_;
std::shared_ptr<mcp::workflow_resource> workflow_resource_;
std::shared_ptr<mcp::agent_resource> agent_resource_;
std::shared_ptr<mcp::api_resource> host_api_;
std::map<std::string, connection> connections_;
std::mutex connections_mutex_;
};
// Example tool handler for system information
mcp::json system_info_handler(const mcp::json& params) {
mcp::json info = {
{"hostname", "example-host"},
{"platform", "example-platform"},
{"uptime_seconds", 3600}
};
// This would be populated with actual system info in a real implementation
return info;
}
int main() {
// Create the host
mcp_host host("Example MCP Host", "localhost", 8081);
// Register a system info tool
mcp::tool system_info_tool = mcp::tool_builder("system_info")
.with_description("Get system information")
.build();
host.register_tool(system_info_tool, system_info_handler);
// Create an example file resource
auto file_resource = std::make_shared<mcp::file_resource>("./files");
host.register_resource("/files", file_resource);
// Create a chain agent for processing
auto chain_agent_ptr = std::make_shared<examples::chain_agent>("processor");
// Set up a chain that calls system_info, then does text processing
chain_agent_ptr->initialize({
{"chain", mcp::json::array({
{
{"tool", "system_info"}
},
{
{"tool", "text_processor"},
{"mappings", {
{"hostname", "text"} // Map system_info.hostname to text_processor.text
}}
}
})}
});
host.register_agent(chain_agent_ptr);
// Create a workflow that combines local and remote tools (when connected)
mcp::workflow combined_workflow("combined", "Combined workflow using local and remote tools");
combined_workflow.add_tool_call("system_info");
host.register_workflow(combined_workflow);
std::cout << "Starting Host on http://localhost:8081" << std::endl;
std::cout << "The host combines server and client capabilities." << std::endl;
std::cout << "You can interact with it via its API at /host" << std::endl;
std::cout << "Press Ctrl+C to stop" << std::endl;
try {
// Start the host in blocking mode
host.start(true);
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
return 0;
}