pluginbridge / Source /Plugin /McpServer.cpp
RAM2118's picture
Out-of-process rebuild: Add Plugin/McpServer.cpp — MCP tools route through helper IPC
df8b77e verified
Raw History Blame
7.93 kB
#include "McpServer.h"
#include "HelperConnection.h"
#include "../Shared/IPCProtocol.h"
#define CPPHTTPLIB_THREAD_POOL_COUNT 2
#include "httplib.h"
#include "json.hpp"
using json = nlohmann::json;
McpServer::McpServer(int port, HelperConnection& helper)
: server(std::make_unique<httplib::Server>()), port(port), helper(helper)
{
setupRoutes();
}
McpServer::~McpServer()
{
stop();
}
void McpServer::setupRoutes()
{
server->Post("/mcp", [this](const httplib::Request& req, httplib::Response& res) {
res.set_header("Access-Control-Allow-Origin", "http://localhost");
try {
auto body = json::parse(req.body);
std::string method = body.value("method", "");
std::string responseStr;
if (method == "initialize")
responseStr = handleInitialize(req.body);
else if (method == "notifications/initialized") {
initialized = true;
res.status = 204;
return;
}
else if (method == "tools/list")
responseStr = handleToolsList(req.body);
else if (method == "tools/call")
responseStr = handleToolsCall(req.body);
else {
json err = {{"jsonrpc","2.0"},{"id",body.value("id",json(nullptr))},
{"error",{{"code",-32601},{"message","Method not found"}}}};
responseStr = err.dump();
}
if (!responseStr.empty())
res.set_content(responseStr, "application/json");
}
catch (const json::exception&) {
json err = {{"jsonrpc","2.0"},{"id",nullptr},
{"error",{{"code",-32700},{"message","Parse error"}}}};
res.set_content(err.dump(), "application/json");
}
});
server->Get("/health", [this](const httplib::Request&, httplib::Response& res) {
json health = {
{"status", "ok"},
{"port", port},
{"initialized", initialized},
{"helper", helper.isHelperRunning() ? "connected" : "disconnected"}
};
res.set_content(health.dump(), "application/json");
});
server->Options("/mcp", [](const httplib::Request&, httplib::Response& res) {
res.set_header("Access-Control-Allow-Origin", "http://localhost");
res.set_header("Access-Control-Allow-Methods", "POST, OPTIONS");
res.set_header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
res.status = 204;
});
}
void McpServer::start()
{
if (running.load()) return;
serverThread = std::thread([this]() {
running.store(true);
server->listen("127.0.0.1", port);
running.store(false);
});
server->wait_until_ready();
}
void McpServer::stop()
{
if (server && server->is_running()) server->stop();
if (serverThread.joinable()) serverThread.join();
running.store(false);
}
bool McpServer::isRunning() const { return running.load(); }
// --- Protocol Handlers ---
std::string McpServer::handleInitialize(const std::string& requestBody)
{
auto body = json::parse(requestBody);
sessionId = "pb-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
json response = {
{"jsonrpc","2.0"},
{"id", body.value("id", json(nullptr))},
{"result", {
{"protocolVersion","2025-06-18"},
{"capabilities",{{"tools",{{"listChanged",false}}}}},
{"serverInfo",{{"name","PluginBridge"},{"version","0.2.0"}}}
}}
};
return response.dump();
}
std::string McpServer::handleToolsList(const std::string& requestBody)
{
auto body = json::parse(requestBody);
json response = {
{"jsonrpc","2.0"},
{"id", body.value("id", json(nullptr))},
{"result",{
{"tools", json::array({
{{"name","list_plugins"},{"description","List loaded plugin"},
{"inputSchema",{{"type","object"},{"properties",json::object()}}}},
{{"name","search_param"},{"description","Search params by keyword"},
{"inputSchema",{{"type","object"},{"properties",{
{"plugin",{{"type","string"}}},
{"keyword",{{"type","string"},{"description","e.g. 'gain', 'band 1', 'cutoff'"}}}
}},{"required",json::array({"plugin","keyword"})}}}},
{{"name","get_params"},{"description","Get param values by ID"},
{"inputSchema",{{"type","object"},{"properties",{
{"plugin",{{"type","string"}}},
{"ids",{{"type","array"},{"items",{{"type","integer"}}}}}
}},{"required",json::array({"plugin","ids"})}}}},
{{"name","set_params"},{"description","Set params (batch, 0.0-1.0)"},
{"inputSchema",{{"type","object"},{"properties",{
{"plugin",{{"type","string"}}},
{"values",{{"type","object"},{"description","id→value"}}}
}},{"required",json::array({"plugin","values"})}}}},
{{"name","get_analysis"},{"description","Real-time audio analysis"},
{"inputSchema",{{"type","object"},{"properties",json::object()}}}}
})}
}}
};
return response.dump();
}
std::string McpServer::handleToolsCall(const std::string& requestBody)
{
auto body = json::parse(requestBody);
auto params = body["params"];
std::string toolName = params["name"];
auto args = params.value("arguments", json::object());
std::string resultText;
bool isError = false;
if (!helper.isHelperRunning())
{
resultText = "helper not connected";
isError = true;
}
else if (toolName == "list_plugins")
{
// Query helper for loaded plugin name
auto resp = helper.sendCommand("{\"cmd\":\"search_param\",\"keyword\":\"\"}");
// Just return what's loaded
resultText = "[\"loaded plugin\"]"; // simplified — actual name comes from processor
}
else if (toolName == "search_param")
{
json cmd = {{"cmd","search_param"},{"keyword", args.value("keyword","")}};
auto resp = helper.sendCommand(cmd.dump());
try {
auto j = json::parse(resp);
if (j.value("ok", false))
resultText = j["results"].dump();
else
resultText = j.value("error", "search failed");
} catch (...) { resultText = "parse error"; isError = true; }
}
else if (toolName == "get_params")
{
json cmd = {{"cmd","get_params"},{"ids", args.value("ids", json::array())}};
auto resp = helper.sendCommand(cmd.dump());
try {
auto j = json::parse(resp);
if (j.value("ok", false))
resultText = j["values"].dump();
else
resultText = "get failed";
} catch (...) { resultText = "parse error"; isError = true; }
}
else if (toolName == "set_params")
{
json cmd = {{"cmd","set_params"},{"values", args.value("values", json::object())}};
auto resp = helper.sendCommand(cmd.dump());
try {
auto j = json::parse(resp);
resultText = j.value("ok", false) ? "ok" : "set failed";
} catch (...) { resultText = "parse error"; isError = true; }
}
else if (toolName == "get_analysis")
{
resultText = "silent"; // Phase 4
}
else
{
resultText = "unknown tool: " + toolName;
isError = true;
}
json response = {
{"jsonrpc","2.0"},
{"id", body.value("id", json(nullptr))},
{"result",{
{"content", json::array({{{"type","text"},{"text",resultText}}})},
{"isError", isError}
}}
};
return response.dump();
}