OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-cogserver / opencog / cogserver / server / MCPServer.cc

153 lines · cc ref:
/*
 * opencog/cogserver/server/MCPServer.cc
 *
 * Copyright (C) 2025 Linas Vepstas
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

#ifdef HAVE_MCP

#include <cstddef>
#include <string>
#include <cctype>

#include <opencog/util/exceptions.h>
#include <opencog/util/Logger.h>

#include <opencog/cogserver/server/MCPServer.h>
#include <opencog/cogserver/mcp-eval/McpEval.h>

using namespace opencog;

MCPServer::MCPServer(const Handle& hcsn, SocketManager* mgr) :
	ConsoleSocket(mgr),
	_hcsn(hcsn)
{
	_eval = nullptr;
}

MCPServer::~MCPServer()
{
	logger().info("MCP Client disconnected");
}

// ==================================================================

// Called before any data is sent/received.
// We arrive here if MCP is NOT being started from a shell.
// This is in fact the usual or intended usage, as the only MCP shell
// users will be coders who are debugging stuff. But there's a catch:
// the shell automatically provides an evaluator. No shell means no
// evaluator, so we have to make one for ourself.
//
void MCPServer::OnConnection(void)
{
	logger().info("MCP Client connected");

	// If there's no shell, then set up an evaluator for ourself.
	if (nullptr == _shell)
		_eval = McpEval::get_evaluator(AtomSpaceCast(_hcsn->getAtomSpace()));
}

// Called for each newline-terminated line received.
void MCPServer::OnLine(const std::string& line)
{
	// If there's a shell, just use the shell evaluator.
	if (_shell)
	{
		_shell->eval(line);
		return;
	}

	// No shell? Do it ourself.
#ifdef ONE_JSON_OBJECT_PER_LINE
	_eval->begin_eval();
	_eval->eval_expr(buffer.substr(start));
	Send(_eval->poll_result());
	return;
#endif // ONE_JSON_OBJECT_PER_LINE

	// The above assumes one JSON object per line, which is a reasonable
	// assumption for the base design. However, the MCP proxying breaks
	// this assumption, so the code below loops, handing off each object
	// that it finds. Its ... necessarily slow, because it passes over
	// all the data character by character, and then does some string
	// copies. So taht's pretty dumb. For for now, I simply don't care.
	// If you need a high-speed interface, use the sexpr API, not the
	// JSON API.

	// Handle multiple JSON objects on a single line
	std::string buffer = line;
	size_t pos = 0;

	while (pos < buffer.length())
	{
		// Skip whitespace
		while (pos < buffer.length() && std::isspace(buffer[pos]))
			pos++;

		if (pos >= buffer.length())
			break;

		// Find the end of this JSON object by counting braces
		size_t start = pos;
		int brace_count = 0;
		bool in_string = false;
		bool escape_next = false;

		while (pos < buffer.length())
		{
			char c = buffer[pos];

			if (!escape_next && c == '\\')
			{
				escape_next = true;
			}
			else if (!escape_next && c == '"')
			{
				in_string = !in_string;
			}
			else if (!in_string && !escape_next)
			{
				if (c == '{')
					brace_count++;
				else if (c == '}')
				{
					brace_count--;
					if (brace_count == 0)
					{
						pos++;
						break;
					}
				}
			}
			else
			{
				escape_next = false;
			}

			pos++;
		}

		// Extract and process this JSON object
		if (brace_count == 0 && pos > start)
		{
			std::string json_obj = buffer.substr(start, pos - start);
			_eval->begin_eval();
			_eval->eval_expr(json_obj);
			Send(_eval->poll_result());
		}
		else
		{
			// Malformed JSON, process the whole remaining string
			_eval->begin_eval();
			_eval->eval_expr(buffer.substr(start));
			Send(_eval->poll_result());
			return;
		}
	}
}

#endif // HAVE_MCP
// ==================================================================