#include "McpServer.h" #include "PluginBridgeProcessor.h" #define CPPHTTPLIB_THREAD_POOL_COUNT 2 #include "httplib.h" #include "json.hpp" using json = nlohmann::json; McpServer::McpServer(int port, PluginBridgeProcessor& processor) : server(std::make_unique()), port(port), processor(processor) { 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}, {"plugin", processor.isPluginLoaded() ? processor.getLoadedPluginName().toStdString() : "none"}, {"params", processor.getLoadedPluginParamCount()} }; 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(); } 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.3.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","Show loaded plugin info"}, {"inputSchema",{{"type","object"},{"properties",json::object()}}}}, {{"name","search_param"},{"description","Search params by keyword"}, {"inputSchema",{{"type","object"},{"properties",{ {"keyword",{{"type","string"},{"description","e.g. 'gain', 'band 1', 'cutoff'"}}} }},{"required",json::array({"keyword"})}}}}, {{"name","get_params"},{"description","Get param values by index"}, {"inputSchema",{{"type","object"},{"properties",{ {"ids",{{"type","array"},{"items",{{"type","integer"}}}}} }},{"required",json::array({"ids"})}}}}, {{"name","set_params"},{"description","Set params (batch, 0.0-1.0)"}, {"inputSchema",{{"type","object"},{"properties",{ {"values",{{"type","object"},{"description","index→value map"}}} }},{"required",json::array({"values"})}}}}, {{"name","get_analysis"},{"description","Real-time audio analysis (future)"}, {"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 (!processor.isPluginLoaded()) { resultText = "no plugin loaded"; isError = true; } else if (toolName == "list_plugins") { json info = { {"name", processor.getLoadedPluginName().toStdString()}, {"params", processor.getLoadedPluginParamCount()}, {"path", processor.getLastPluginPath().toStdString()} }; resultText = info.dump(); } else if (toolName == "search_param") { auto keyword = args.value("keyword", ""); auto results = processor.searchParams(juce::String(keyword)); json arr = json::array(); for (auto& p : results) { arr.push_back({ {"i", p.index}, {"id", p.id.toStdString()}, {"n", p.name.toStdString()}, {"v", std::round(p.value * 1000.0f) / 1000.0f}, {"t", p.displayText.toStdString()} }); } resultText = arr.dump(); } else if (toolName == "get_params") { std::vector ids; for (auto& id : args["ids"]) ids.push_back(id.get()); auto results = processor.getParams(ids); json values = json::object(); for (auto& [idx, val] : results) values[std::to_string(idx)] = std::round(val * 1000.0f) / 1000.0f; resultText = values.dump(); } else if (toolName == "set_params") { std::map values; for (auto& [key, val] : args["values"].items()) values[std::stoi(key)] = val.get(); bool ok = processor.setParams(values); resultText = ok ? "ok" : "set failed"; } else if (toolName == "get_analysis") { resultText = "silent"; // Sprint 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(); }