pluginbridge / Source /McpServer.cpp
RAM2118's picture
Sprint 1: Add McpServer.cpp — working HTTP server with MCP JSON-RPC
90c2369 verified
Raw History Blame
8.17 kB
#include "McpServer.h"
// httplib must be included in exactly ONE .cpp file
#define CPPHTTPLIB_THREAD_POOL_COUNT 2
#include "httplib.h"
#include "json.hpp"
using json = nlohmann::json;
McpServer::McpServer(int port)
: server(std::make_unique<httplib::Server>()), port(port)
{
setupRoutes();
}
McpServer::~McpServer()
{
stop();
}
void McpServer::setupRoutes()
{
// Single MCP endpoint — all JSON-RPC goes through here
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") {
// Notification — no response needed
initialized = true;
res.status = 204;
return;
}
else if (method == "tools/list") {
responseStr = handleToolsList();
}
else if (method == "tools/call") {
responseStr = handleToolsCall(req.body);
}
else {
json errResponse = {
{"jsonrpc", "2.0"},
{"id", body.value("id", json(nullptr))},
{"error", {{"code", -32601}, {"message", "Method not found"}}}
};
responseStr = errResponse.dump();
}
if (!responseStr.empty()) {
res.set_content(responseStr, "application/json");
}
}
catch (const json::exception& e) {
json errResponse = {
{"jsonrpc", "2.0"},
{"id", nullptr},
{"error", {{"code", -32700}, {"message", "Parse error"}}}
};
res.set_content(errResponse.dump(), "application/json");
}
});
// Health check — quick way to verify server is alive
server->Get("/health", [this](const httplib::Request&, httplib::Response& res) {
json health = {{"status", "ok"}, {"port", port}, {"initialized", initialized}};
res.set_content(health.dump(), "application/json");
});
// CORS preflight
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);
});
// Wait until server is actually ready
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();
}
void McpServer::serverThreadFunc()
{
server->listen("127.0.0.1", port);
}
// --- MCP Protocol Handlers ---
std::string McpServer::handleInitialize(const std::string& requestBody)
{
auto body = json::parse(requestBody);
// Generate session ID
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.1.0"}
}}
}}
};
return response.dump();
}
std::string McpServer::handleToolsList()
{
json response = {
{"jsonrpc", "2.0"},
{"id", 1},
{"result", {
{"tools", json::array({
{
{"name", "list_plugins"},
{"description", "List all loaded plugin instances"},
{"inputSchema", {{"type", "object"}, {"properties", json::object()}}}
},
{
{"name", "search_param"},
{"description", "Search plugin parameters by keyword"},
{"inputSchema", {
{"type", "object"},
{"properties", {
{"plugin", {{"type", "string"}, {"description", "Plugin name"}}},
{"keyword", {{"type", "string"}, {"description", "Search term"}}}
}},
{"required", json::array({"plugin", "keyword"})}
}}
},
{
{"name", "get_params"},
{"description", "Get current values for parameter IDs"},
{"inputSchema", {
{"type", "object"},
{"properties", {
{"plugin", {{"type", "string"}}},
{"ids", {{"type", "array"}, {"items", {{"type", "integer"}}}}}
}},
{"required", json::array({"plugin", "ids"})}
}}
},
{
{"name", "set_params"},
{"description", "Set parameter values by ID (batch)"},
{"inputSchema", {
{"type", "object"},
{"properties", {
{"plugin", {{"type", "string"}}},
{"values", {{"type", "object"}, {"description", "param_id → value (0.0-1.0)"}}}
}},
{"required", json::array({"plugin", "values"})}
}}
},
{
{"name", "get_analysis"},
{"description", "Get real-time audio analysis (LUFS, bands, stereo)"},
{"inputSchema", {{"type", "object"}, {"properties", json::object()}}}
}
})}
}}
};
return response.dump();
}
std::string McpServer::handleToolsCall(const std::string& requestBody)
{
auto body = json::parse(requestBody);
std::string toolName = body["params"]["name"];
json result;
if (toolName == "list_plugins") {
// Sprint 1: no plugins loaded yet
result = {
{"content", json::array({{{"type", "text"}, {"text", "[]"}}})},
{"isError", false}
};
}
else if (toolName == "search_param") {
result = {
{"content", json::array({{{"type", "text"}, {"text", "no plugin loaded"}}})},
{"isError", true}
};
}
else if (toolName == "get_params") {
result = {
{"content", json::array({{{"type", "text"}, {"text", "no plugin loaded"}}})},
{"isError", true}
};
}
else if (toolName == "set_params") {
result = {
{"content", json::array({{{"type", "text"}, {"text", "no plugin loaded"}}})},
{"isError", true}
};
}
else if (toolName == "get_analysis") {
// Sprint 1: stub response
result = {
{"content", json::array({{{"type", "text"}, {"text", "silent"}}})},
{"isError", false}
};
}
else {
result = {
{"content", json::array({{{"type", "text"}, {"text", "unknown tool: " + toolName}}})},
{"isError", true}
};
}
json response = {
{"jsonrpc", "2.0"},
{"id", body.value("id", json(nullptr))},
{"result", result}
};
return response.dump();
}