/**
* McpEval.cc
*
* MCP protocol evaluator
*
* Copyright (c) 2008, 2014, 2015, 2020, 2021, 2022 Linas Vepstas
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License v3 as
* published by the Free Software Foundation and including the exceptions
* at http://opencog.org/wiki/Licenses
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program; if not, write to:
* Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include <chrono>
#include <ctime>
#include <algorithm>
#include <fstream>
#include <json/json.h>
#include <opencog/util/Logger.h>
#include <opencog/cogserver/mcp-tools/McpPlugin.h>
#include <opencog/cogserver/mcp-tools/McpPlugAtomSpace.h>
#include <opencog/cogserver/mcp-tools/McpPlugEcho.h>
#include "McpEval.h"
using namespace opencog;
using namespace std::chrono_literals;
McpEval::McpEval(const AtomSpacePtr& asp)
: GenericEval(), _atomspace(asp)
{
_started = false;
_done = false;
}
// Helper function to convert Json::Value to compact string
static std::string json_to_string(const Json::Value& value) {
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
return Json::writeString(builder, value);
}
// Helper function to publish a resource file as MCP resource response
static bool publish_resource(const std::string& doc_base,
const std::string& uri,
const std::string& filename,
Json::Value& response)
{
std::string doc_path = doc_base + filename;
std::ifstream file(doc_path);
if (!file.is_open()) {
response["error"]["code"] = -32602;
response["error"]["message"] = "Failed to read documentation file: " + doc_path;
return false;
}
std::stringstream buffer;
buffer << file.rdbuf();
file.close();
Json::Value content;
content["uri"] = uri;
content["mimeType"] = "text/markdown";
content["text"] = buffer.str();
response["result"]["contents"] = Json::arrayValue;
response["result"]["contents"].append(content);
return true;
}
McpEval::~McpEval()
{
}
/* ============================================================== */
/**
* Evaluate MCP commands.
*/
void McpEval::eval_expr(const std::string &expr)
{
if (0 == expr.size()) return;
if (0 == expr.compare("\n")) return;
logger().debug("[McpEval] received %s", expr.c_str());
try
{
Json::Value request;
Json::CharReaderBuilder reader_builder;
std::unique_ptr<Json::CharReader> reader(reader_builder.newCharReader());
std::string errors;
if (!reader->parse(expr.c_str(), expr.c_str() + expr.length(), &request, &errors)) {
Json::Value error_response;
error_response["jsonrpc"] = "2.0";
error_response["id"] = Json::Value::null;
error_response["error"]["code"] = -32700;
error_response["error"]["message"] = "Parse error: " + errors;
_result = json_to_string(error_response) + "\n";
return;
}
if (!request.isMember("jsonrpc") || request["jsonrpc"].asString() != "2.0")
{
Json::Value error_response;
error_response["jsonrpc"] = "2.0";
error_response["id"] = Json::Value::null;
error_response["error"]["code"] = -32600;
error_response["error"]["message"] = "Invalid Request - missing jsonrpc 2.0";
_result = json_to_string(error_response) + "\n";
return;
}
std::string method = request.isMember("method") ? request["method"].asString() : "";
Json::Value params = request.isMember("params") ? request["params"] : Json::objectValue;
Json::Value id = request.isMember("id") ? request["id"] : Json::Value::null;
logger().debug("[McpEval] method %s", method.c_str());
Json::Value response;
response["jsonrpc"] = "2.0";
response["id"] = id;
if (method == "initialize") {
// Use the latest MCP protocol version
response["result"]["protocolVersion"] = "2025-06-18";
// Indicate that we support tools by including the capability
// The MCP spec states that presence of the key indicates support
// An empty object {} means basic support without advanced features
Json::Value toolsCapability;
toolsCapability["listChanged"] = false; // We don't support dynamic tool changes
response["result"]["capabilities"]["tools"] = toolsCapability;
// Indicate that we support resources for documentation
Json::Value resourcesCapability;
resourcesCapability["subscribe"] = false; // No live updates for docs
resourcesCapability["listChanged"] = false; // Static resource list
response["result"]["capabilities"]["resources"] = resourcesCapability;
response["result"]["serverInfo"]["name"] = "CogServer MCP";
response["result"]["serverInfo"]["version"] = "0.2.1";
response["result"]["serverInfo"]["instructions"] =
"The CogServer MCP provides access to a live, running instance of the "
"AtomSpace. It allows the MCP client to view and manipulate the contents "
"of the AtomSpace. This includes creating and deleting Atoms, changing "
"the Values attached to them, running the executable Atoms, and sending "
"messages to those Atoms that implement an Object interface.";
} else if (method == "notifications/initialized" or
method == "initialized") {
#define WTF_INIT
#ifdef WTF_INIT
// Notifications (requests without id) should not send a
// response per JSON-RPC spec.
// But HTTP clients need some response to be sent back.
// Return an empty result object to keep HTTP happy.
response["result"] = Json::objectValue;
response.removeMember("id");
#else
// Notification - no response
_result = "";
_done = true;
return;
#endif
} else if (method == "notifications/cancelled") {
// XXX FIXME .. the currently running tool should be killed.
// To do this, the Mcpplugin API would need to get a new
// kill method that would get called to do this. Later.
// This method is supposed to not have any response.
_result = "";
_done = true;
return;
} else if (method == "ping") {
response["result"] = Json::objectValue;
} else if (method == "tools/list") {
Json::Value all_tools(Json::arrayValue);
// Collect tools from all registered plugins
for (const auto& plugin : _plugins) {
std::string plugin_tools_json = plugin->get_tool_descriptions();
Json::Value plugin_tools;
Json::CharReaderBuilder builder;
std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
std::string errors;
if (reader->parse(plugin_tools_json.c_str(),
plugin_tools_json.c_str() + plugin_tools_json.length(),
&plugin_tools, &errors)) {
for (Json::ArrayIndex i = 0; i < plugin_tools.size(); ++i) {
all_tools.append(plugin_tools[i]);
}
}
}
response["result"]["tools"] = all_tools;
} else if (method == "resources/list") {
Json::Value resources(Json::arrayValue);
// Add the AtomSpace introduction document (shorter overview)
Json::Value intro_resource;
intro_resource["uri"] = "atomspace://docs/introduction";
intro_resource["name"] = "AtomSpace Introduction";
intro_resource["description"] = "Overview of the AtomSpace, Atoms, and basic concepts";
intro_resource["mimeType"] = "text/markdown";
resources.append(intro_resource);
// Add the detailed AtomSpace guide (longer detailed guide)
Json::Value guide_resource;
guide_resource["uri"] = "atomspace://docs/atomspace-guide";
guide_resource["name"] = "AtomSpace Detailed Guide";
guide_resource["description"] = "Comprehensive guide to Atomese, the AtomSpace, and the CogServer";
guide_resource["mimeType"] = "text/markdown";
resources.append(guide_resource);
// Add the CogServer MCP access guide
Json::Value cogserver_resource;
cogserver_resource["uri"] = "atomspace://docs/cogserver-mcp";
cogserver_resource["name"] = "CogServer and MCP Access";
cogserver_resource["description"] = "How to access the CogServer, MCP tools, port numbers, and documentation locations";
cogserver_resource["mimeType"] = "text/markdown";
resources.append(cogserver_resource);
// Add Atom Types reference
Json::Value atomtypes_resource;
atomtypes_resource["uri"] = "atomspace://docs/atom-types";
atomtypes_resource["name"] = "Atom Types Reference";
atomtypes_resource["description"] = "Comprehensive reference for 170+ Atom types organized by functional category";
atomtypes_resource["mimeType"] = "text/markdown";
resources.append(atomtypes_resource);
// Add Creating Atoms guide
Json::Value createatom_resource;
createatom_resource["uri"] = "atomspace://docs/create-atom";
createatom_resource["name"] = "Creating Atoms Guide";
createatom_resource["description"] = "Guide for creating Nodes and Links in the AtomSpace";
createatom_resource["mimeType"] = "text/markdown";
resources.append(createatom_resource);
// Add Designing Structures guide
Json::Value designing_resource;
designing_resource["uri"] = "atomspace://docs/designing-structures";
designing_resource["name"] = "Designing Structures Guide";
designing_resource["description"] = "Guide for designing data structures in Atomese: global uniqueness, avoiding IDs, Atomese vs programming languages";
designing_resource["mimeType"] = "text/markdown";
resources.append(designing_resource);
// Add Query AtomSpace guide
Json::Value query_resource;
query_resource["uri"] = "atomspace://docs/query-atom";
query_resource["name"] = "Querying the AtomSpace";
query_resource["description"] = "Guide for querying and exploring the AtomSpace effectively";
query_resource["mimeType"] = "text/markdown";
resources.append(query_resource);
// Add Working with Values guide
Json::Value values_resource;
values_resource["uri"] = "atomspace://docs/working-with-values";
values_resource["name"] = "Working with Values";
values_resource["description"] = "Guide for working with Values and key-value pairs";
values_resource["mimeType"] = "text/markdown";
resources.append(values_resource);
// Add Pattern Matching guide
Json::Value pattern_resource;
pattern_resource["uri"] = "atomspace://docs/pattern-matching";
pattern_resource["name"] = "Pattern Matching Guide";
pattern_resource["description"] = "Guide for using MeetLink and QueryLink to search the AtomSpace with patterns";
pattern_resource["mimeType"] = "text/markdown";
resources.append(pattern_resource);
// Add Advanced Pattern Matching guide
Json::Value advanced_pattern_resource;
advanced_pattern_resource["uri"] = "atomspace://docs/advanced-pattern-matching";
advanced_pattern_resource["name"] = "Advanced Pattern Matching";
advanced_pattern_resource["description"] = "Guide for using AbsentLink, ChoiceLink, AlwaysLink, and GroupLink in sophisticated queries";
advanced_pattern_resource["mimeType"] = "text/markdown";
resources.append(advanced_pattern_resource);
// Add Streams guide
Json::Value streams_resource;
streams_resource["uri"] = "atomspace://docs/streams";
streams_resource["name"] = "Working with Streams";
streams_resource["description"] = "Comprehensive guide for creating and processing data streams: FormulaStream, FutureStream, FlatStream, FilterLink, DrainLink";
streams_resource["mimeType"] = "text/markdown";
resources.append(streams_resource);
response["result"]["resources"] = resources;
} else if (method == "resources/read") {
std::string uri = params.isMember("uri") ? params["uri"].asString() : "";
// Use the CMAKE install prefix for the documentation path
std::string doc_base = std::string(PROJECT_INSTALL_PREFIX) + "/share/cogserver/mcp/";
if (uri == "atomspace://docs/introduction") {
publish_resource(doc_base, uri, "AtomSpace-Overview.md", response);
} else if (uri == "atomspace://docs/atomspace-guide") {
publish_resource(doc_base, uri, "AtomSpace-Details.md", response);
} else if (uri == "atomspace://docs/cogserver-mcp") {
publish_resource(doc_base, uri, "CogServer-Resource.md", response);
} else if (uri == "atomspace://docs/atom-types") {
publish_resource(doc_base, uri, "AtomTypes-Resource.md", response);
} else if (uri == "atomspace://docs/create-atom") {
publish_resource(doc_base, uri, "CreateAtom-Resource.md", response);
} else if (uri == "atomspace://docs/designing-structures") {
publish_resource(doc_base, uri, "DesigningStructures-Resource.md", response);
} else if (uri == "atomspace://docs/query-atom") {
publish_resource(doc_base, uri, "QueryAtom-Resource.md", response);
} else if (uri == "atomspace://docs/working-with-values") {
publish_resource(doc_base, uri, "WorkingWithValues-Resource.md", response);
} else if (uri == "atomspace://docs/pattern-matching") {
publish_resource(doc_base, uri, "PatternMatching-Resource.md", response);
} else if (uri == "atomspace://docs/advanced-pattern-matching") {
publish_resource(doc_base, uri, "AdvancedPatternMatching-Resource.md", response);
} else if (uri == "atomspace://docs/streams") {
publish_resource(doc_base, uri, "Streams-Resource.md", response);
} else {
response["error"]["code"] = -32602;
response["error"]["message"] = "Resource not found: " + uri;
}
} else if (method == "tools/call") {
std::string tool_name = params.isMember("name") ? params["name"].asString() : "";
Json::Value arguments = params.isMember("arguments") ? params["arguments"] : Json::objectValue;
// Find the plugin that handles this tool
auto it = _tool_to_plugin.find(tool_name);
if (it != _tool_to_plugin.end()) {
// Invoke the tool through the plugin
std::string arguments_json = json_to_string(arguments);
std::string tool_result_json;
// Catch exceptions from tool execution and convert to MCP error format
try {
tool_result_json = it->second->invoke_tool(tool_name, arguments_json);
} catch (const std::exception& e) {
// Convert exception to MCP content format error
// Per MCP spec: tool execution errors use {"content": [...], "isError": true}
Json::Value error_content;
Json::Value content_item;
content_item["type"] = "text";
content_item["text"] = e.what();
error_content["content"].append(content_item);
error_content["isError"] = true;
response["result"] = error_content;
logger().debug("[McpEval] replying: %s", json_to_string(response).c_str());
_result = json_to_string(response) + "\n";
_done = true;
return;
}
Json::Value tool_result;
Json::CharReaderBuilder builder;
std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
std::string errors;
if (reader->parse(tool_result_json.c_str(),
tool_result_json.c_str() + tool_result_json.length(),
&tool_result, &errors)) {
// Per MCP spec: tool execution results (including errors) go in "result"
// Tool errors use {"content": [...], "isError": true} format
// Only JSON-RPC protocol errors use "error" field
response["result"] = tool_result;
} else {
response["error"]["code"] = -32700;
response["error"]["message"] = "Failed to parse tool result";
}
} else {
response["error"]["code"] = -32601;
response["error"]["message"] = "Tool not found: " + tool_name;
}
} else {
response["error"]["code"] = -32601;
response["error"]["message"] = "Method not found: " + method;
}
logger().debug("[McpEval] replying: %s", json_to_string(response).c_str());
// Trailing newline is mandatory; jsonrpc uses line discipline.
_result = json_to_string(response) + "\n";
_done = true;
}
catch (const std::exception& e)
{
Json::Value error_response;
error_response["jsonrpc"] = "2.0";
error_response["id"] = Json::Value::null;
error_response["error"]["code"] = -32700;
error_response["error"]["message"] = "Parse error: " + std::string(e.what());
logger().debug("[McpEval] error reply: %s", json_to_string(error_response).c_str());
_result = json_to_string(error_response) + "\n";
_done = true;
}
}
std::string McpEval::poll_result()
{
if (_done) {
_done = false;
return _result;
}
return "";
}
void McpEval::begin_eval()
{
_done = false;
}
/* ============================================================== */
/**
* interrupt() - convert user's control-C at keyboard into exception.
*/
void McpEval::interrupt(void)
{
_done = true;
_started = false;
_caught_error = true;
}
/* ============================================================== */
/**
* Register a plugin to provide MCP tools
*/
void McpEval::register_plugin(std::shared_ptr<McpPlugin> plugin)
{
if (!plugin) return;
_plugins.push_back(plugin);
// Map each tool to its plugin
std::string tools_json = plugin->get_tool_descriptions();
Json::Value tools;
Json::CharReaderBuilder builder;
std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
std::string errors;
if (reader->parse(tools_json.c_str(), tools_json.c_str() + tools_json.length(), &tools, &errors)) {
for (Json::ArrayIndex i = 0; i < tools.size(); ++i) {
std::string tool_name = tools[i]["name"].asString();
_tool_to_plugin[tool_name] = plugin;
}
}
}
/**
* Unregister a plugin
*/
void McpEval::unregister_plugin(std::shared_ptr<McpPlugin> plugin)
{
if (!plugin) return;
// Remove from plugins list
_plugins.erase(
std::remove(_plugins.begin(), _plugins.end(), plugin),
_plugins.end()
);
// Remove tool mappings
auto it = _tool_to_plugin.begin();
while (it != _tool_to_plugin.end()) {
if (it->second == plugin) {
it = _tool_to_plugin.erase(it);
} else {
++it;
}
}
}
/* ============================================================== */
// One evaluator per thread. This allows multiple users to each
// have their own evaluator.
McpEval* McpEval::get_evaluator(const AtomSpacePtr& asp)
{
static thread_local McpEval* evaluator = new McpEval(asp);
// Install the plugins here, for now. XXX Hack alert, this is
// not the right way to do this long-term, but it holds water for
// now.
auto echo_plugin = std::make_shared<McpPlugEcho>();
evaluator->register_plugin(echo_plugin);
auto as_plugin = std::make_shared<McpPlugAtomSpace>(asp.get());
evaluator->register_plugin(as_plugin);
// The eval_dtor runs when this thread is destroyed.
class eval_dtor {
public:
~eval_dtor() { delete evaluator; }
};
static thread_local eval_dtor killer;
return evaluator;
}
/* ===================== END OF FILE ======================== */