OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-cogserver / examples / mcp / mcp-checker.cc

458 lines · cc ref:
/**
 * @file mcp-checker.cc
 * @brief Demo MCP client connecting to server via TCP
 *
 * This implements a simple MCP test client that can be used to verify
 * that the MCP network server is available, and is responding to
 * commands.  It will list the tools and resources provided by the
 * MCP server. Aim it at a running instance of the CogServer.
 *
 * (This code was created by Claude.)
 */

#include <json/json.h>
#include <iostream>
#include <string>
#include <sstream>
#include <cstring>
#include <thread>
#include <chrono>
#include <atomic>

// Socket includes
#ifdef _WIN32
    #include <winsock2.h>
    #include <ws2tcpip.h>
    #pragma comment(lib, "ws2_32.lib")
    typedef int socklen_t;
#else
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <netinet/in.h>
    #include <unistd.h>
    #include <netdb.h>
    #define INVALID_SOCKET -1
    #define SOCKET_ERROR -1
    #define closesocket close
    typedef int SOCKET;
#endif

// Using jsoncpp's Json namespace

// Helper function to convert Json::Value to string with optional indentation
static inline std::string dump_result(const Json::Value& value, bool pretty = true) {
    Json::StreamWriterBuilder builder;
    builder["indentation"] = pretty ? "  " : "";
    std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
    std::ostringstream oss;
    writer->write(value, &oss);
    return oss.str();
}

class SimpleJsonRpcClient {
private:
    SOCKET sock;
    std::atomic<int> message_id{0};
    bool connected{false};
    Json::StreamWriterBuilder writer_builder;
    Json::CharReaderBuilder reader_builder;

public:
    SimpleJsonRpcClient() : sock(INVALID_SOCKET) {
#ifdef _WIN32
        WSADATA wsaData;
        WSAStartup(MAKEWORD(2, 2), &wsaData);
#endif
        writer_builder["indentation"] = "";
        writer_builder["commentStyle"] = "None";
    }

    ~SimpleJsonRpcClient() {
        disconnect();
#ifdef _WIN32
        WSACleanup();
#endif
    }

    bool connect(const std::string& host, int port) {
        // Create socket
        sock = socket(AF_INET, SOCK_STREAM, 0);
        if (sock == INVALID_SOCKET) {
            std::cerr << "Failed to create socket" << std::endl;
            return false;
        }

        // Resolve hostname
        struct hostent* server = gethostbyname(host.c_str());
        if (server == nullptr) {
            std::cerr << "Failed to resolve hostname: " << host << std::endl;
            closesocket(sock);
            sock = INVALID_SOCKET;
            return false;
        }

        // Setup address structure
        struct sockaddr_in serv_addr;
        memset(&serv_addr, 0, sizeof(serv_addr));
        serv_addr.sin_family = AF_INET;
        memcpy(&serv_addr.sin_addr.s_addr, server->h_addr, server->h_length);
        serv_addr.sin_port = htons(port);

        // Connect to server
        if (::connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
            std::cerr << "Failed to connect to " << host << ":" << port << std::endl;
            closesocket(sock);
            sock = INVALID_SOCKET;
            return false;
        }

        connected = true;
        return true;
    }

    void disconnect() {
        if (sock != INVALID_SOCKET) {
            closesocket(sock);
            sock = INVALID_SOCKET;
        }
        connected = false;
    }

    bool sendMessage(const Json::Value& message) {
        if (!connected) return false;

        std::string msg_str = dump_result(message, false) + "\n";
        int total_sent = 0;
        int msg_len = msg_str.length();

        while (total_sent < msg_len) {
            int sent = send(sock, msg_str.c_str() + total_sent, msg_len - total_sent, 0);
            if (sent <= 0) {
                std::cerr << "Failed to send message" << std::endl;
                return false;
            }
            total_sent += sent;
        }

        return true;
    }

    Json::Value receiveMessage() {
        if (!connected) return Json::Value();

        static std::string buffer; // Make buffer static to preserve data between calls
        char recv_buf[4096];

        while (true) {
            // First check if we already have a complete line in the buffer
            size_t newline_pos = buffer.find('\n');
            if (newline_pos != std::string::npos) {
                std::string line = buffer.substr(0, newline_pos);
                buffer = buffer.substr(newline_pos + 1);

                Json::Value root;
                std::unique_ptr<Json::CharReader> reader(reader_builder.newCharReader());
                std::string errors;

                if (!reader->parse(line.c_str(), line.c_str() + line.length(), &root, &errors)) {
                    std::cerr << "Failed to parse JSON: " << errors << std::endl;
                    return Json::Value();
                }
                return root;
            }

            // Read more data if we don't have a complete line
            int received = recv(sock, recv_buf, sizeof(recv_buf) - 1, 0);
            if (received <= 0) {
                std::cerr << "Failed to receive message" << std::endl;
                return Json::Value();
            }

            recv_buf[received] = '\0';
            buffer += recv_buf;
        }
    }

    Json::Value sendRequest(const std::string& method, const Json::Value& params = Json::Value()) {
        int id = message_id++;

        Json::Value request;
        request["jsonrpc"] = "2.0";
        request["method"] = method;
        request["id"] = id;

        if (!params.isNull()) {
            request["params"] = params;
        }

        if (!sendMessage(request)) {
            throw std::runtime_error("Failed to send request");
        }

        Json::Value response = receiveMessage();
        if (response.empty()) {
            throw std::runtime_error("Failed to receive response");
        }

        if (response.isMember("error")) {
            throw std::runtime_error("RPC error: " + dump_result(response["error"], false));
        }

        if (response.isMember("result")) {
            return response["result"];
        }

        return Json::Value();
    }

    bool initialize(const std::string& name, const std::string& version) {
        Json::Value params;
        params["protocolVersion"] = "2025-06-18";
        params["capabilities"] = Json::objectValue;
        params["clientInfo"]["name"] = name;
        params["clientInfo"]["version"] = version;

        try {
            Json::Value result = sendRequest("initialize", params);
            std::cout << "Initialize response: " << dump_result(result) << std::endl;

            // Send initialized notification
            Json::Value notification;
            notification["jsonrpc"] = "2.0";
            notification["method"] = "notifications/initialized";
            sendMessage(notification);

            // Per JSON RPC spec, there should be no response to
            // notifications. But we have to hack this, to keep
            // Claude HTTP happy. So read a response, and discard it.
            Json::Value init_response = receiveMessage();
            std::cout << "DEBUG: initialize notification response: "
                  << dump_result(init_response) << std::endl;
            return true;
        } catch (const std::exception& e) {
            std::cerr << "Initialize failed: " << e.what() << std::endl;
            return false;
        }
    }

    bool ping() {
        try {
            sendRequest("ping");
            return true;
        } catch (...) {
            return false;
        }
    }

    Json::Value listTools() {
        return sendRequest("tools/list");
    }

    Json::Value callTool(const std::string& name, const Json::Value& arguments = Json::Value()) {
        Json::Value params;
        params["name"] = name;

        if (!arguments.isNull()) {
            params["arguments"] = arguments;
        }

        return sendRequest("tools/call", params);
    }

    Json::Value listResources() {
        return sendRequest("resources/list");
    }

    Json::Value readResource(const std::string& uri) {
        Json::Value params;
        params["uri"] = uri;
        return sendRequest("resources/read", params);
    }
};

void print_usage(const char* program_name) {
    std::cout << "Usage: " << program_name << " [options]\n"
              << "Options:\n"
              << "  -h, --host <host>     Server host (default: localhost)\n"
              << "  -p, --port <port>     Server port (default: 18888)\n"
              << "  --help               Show this help message\n"
              << "\n"
              << "Example:\n"
              << "  " << program_name << " --host localhost --port 18888\n"
              << "\n"
              << "This client connects to an existing MCP server running on the specified\n"
              << "host and port, and lists the available tools and resources.\n";
}

int main(int argc, char* argv[]) {
    std::string host = "localhost";
    int port = 18888;

    // Parse command line arguments
    for (int i = 1; i < argc; i++) {
        std::string arg = argv[i];

        if (arg == "--help") {
            print_usage(argv[0]);
            return 0;
        } else if ((arg == "-h" || arg == "--host") && i + 1 < argc) {
            host = argv[++i];
        } else if ((arg == "-p" || arg == "--port") && i + 1 < argc) {
            port = std::stoi(argv[++i]);
        } else {
            std::cerr << "Unknown argument: " << arg << std::endl;
            print_usage(argv[0]);
            return 1;
        }
    }

    std::cout << "MCP Network Client Example\n";
    std::cout << "Connecting to: " << host << ":" << port << std::endl;

    try {
        SimpleJsonRpcClient client;

        // Connect to server
        if (!client.connect(host, port)) {
            std::cerr << "Failed to connect to server" << std::endl;
            return 1;
        }

        std::cout << "✓ Connected to server" << std::endl;

        // Initialize the client
        std::cout << "\nInitializing client..." << std::endl;
        if (!client.initialize("NetworkClientJsonOnly", "1.0.0")) {
            std::cerr << "Failed to initialize client" << std::endl;
            return 1;
        }

        std::cout << "✓ Client initialized successfully" << std::endl;

        // Test ping
        std::cout << "\nTesting ping..." << std::endl;
        if (client.ping()) {
            std::cout << "✓ Ping successful" << std::endl;
        } else {
            std::cout << "✗ Ping failed" << std::endl;
        }

        // List available tools
        std::cout << "\nListing available tools..." << std::endl;
        try {
            Json::Value tools_response = client.listTools();
            std::cout << "DEBUG: Raw tools response: " << dump_result(tools_response) << std::endl;

            if (tools_response.isMember("tools") && tools_response["tools"].isArray()) {
                Json::Value tools = tools_response["tools"];
                std::cout << "✓ Found " << tools.size() << " tools:" << std::endl;

                for (Json::ArrayIndex i = 0; i < tools.size(); ++i) {
                    const Json::Value& tool = tools[i];
                    std::cout << "  - " << tool["name"].asString()
                              << ": " << tool["description"].asString() << std::endl;
                }

                // Call a tool if available
                if (tools.size() > 0) {
                    const Json::Value& first_tool = tools[0];
                    std::string tool_name = first_tool["name"].asString();
                    std::cout << "\nCalling tool '" << tool_name << "'..." << std::endl;

                    Json::Value tool_args;
                    // Add some example arguments based on common tool types
                    if (tool_name == "echo") {
                        tool_args["text"] = "Hello from network client!";
                    } else if (tool_name == "greeting") {
                        tool_args["name"] = "NetworkClient";
                    } else if (tool_name == "time") {
                        // No arguments needed for time tool
                    }

                    try {
                        Json::Value result = client.callTool(tool_name, tool_args);
                        std::cout << "✓ Tool result: " << dump_result(result) << std::endl;
                    } catch (const std::exception& e) {
                        std::cout << "✗ Tool call failed: " << e.what() << std::endl;
                    }
                }
            }
        } catch (const std::exception& e) {
            std::cout << "✗ Failed to list tools: " << e.what() << std::endl;
        }

        // List available resources
        std::cout << "\nListing available resources..." << std::endl;
        try {
            Json::Value resources_response = client.listResources();
            std::cout << "DEBUG: Raw resources response: " << dump_result(resources_response) << std::endl;

            if (resources_response.isMember("resources") && resources_response["resources"].isArray()) {
                Json::Value resources = resources_response["resources"];
                std::cout << "✓ Found " << resources.size() << " resources:" << std::endl;

                for (Json::ArrayIndex i = 0; i < resources.size(); ++i) {
                    const Json::Value& resource = resources[i];
                    std::cout << "  - URI: " << resource["uri"].asString() << std::endl;
                    if (resource.isMember("name")) {
                        std::cout << "    Name: " << resource["name"].asString() << std::endl;
                    }
                    if (resource.isMember("description")) {
                        std::cout << "    Description: " << resource["description"].asString() << std::endl;
                    }
                    if (resource.isMember("mimeType")) {
                        std::cout << "    MIME Type: " << resource["mimeType"].asString() << std::endl;
                    }
                }

                // Try to read the first resource if available
                if (resources.size() > 0) {
                    const Json::Value& first_resource = resources[0];
                    if (first_resource.isMember("uri")) {
                        std::string uri = first_resource["uri"].asString();
                        std::cout << "\nReading first resource: " << uri << std::endl;

                        try {
                            Json::Value read_response = client.readResource(uri);
                            if (read_response.isMember("contents")) {
                                Json::Value contents = read_response["contents"];
                                if (contents.isArray() && contents.size() > 0) {
                                    const Json::Value& first_content = contents[0];
                                    if (first_content.isMember("text")) {
                                        std::string text = first_content["text"].asString();
                                        // Print first 200 chars of content
                                        if (text.length() > 200) {
                                            std::cout << "✓ Resource content (first 200 chars):\n"
                                                      << text.substr(0, 200) << "..." << std::endl;
                                        } else {
                                            std::cout << "✓ Resource content:\n" << text << std::endl;
                                        }
                                    } else if (first_content.isMember("uri")) {
                                        std::cout << "✓ Resource references URI: "
                                                  << first_content["uri"].asString() << std::endl;
                                    }
                                }
                            } else {
                                std::cout << "✓ Read response: " << dump_result(read_response) << std::endl;
                            }
                        } catch (const std::exception& e) {
                            std::cout << "✗ Failed to read resource: " << e.what() << std::endl;
                        }
                    }
                }
            } else {
                std::cout << "✓ No resources available" << std::endl;
            }
        } catch (const std::exception& e) {
            std::cout << "✗ Failed to list resources: " << e.what() << std::endl;
        }

        std::cout << "\n✓ All operations completed successfully!" << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
        return 1;
    }

    return 0;
}