| #include "McpServer.h" |
| #include "HelperConnection.h" |
| #include "InstanceRegistry.h" |
| #include "IPCProtocol.h" |
| #include "Constants.h" |
|
|
| #define CPPHTTPLIB_THREAD_POOL_COUNT 2 |
| #include "httplib.h" |
| #include "json.hpp" |
|
|
| using json = nlohmann::json; |
|
|
| McpServer::McpServer(HelperConnection& helper) |
| : server(std::make_unique<httplib::Server>()), 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) { |
| std::string channel = channelNameCallback ? channelNameCallback() : "Unknown"; |
| json health = {{"status","ok"},{"port",port},{"channel",channel}, |
| {"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"); |
| res.status = 204; |
| }); |
| } |
|
|
| void McpServer::start() |
| { |
| if (running.load()) return; |
| serverThread = std::thread([this]() { |
| running.store(true); |
|
|
| |
| |
| if (server->bind_to_port("127.0.0.1", PluginBridgeConstants::kDefaultMcpPort)) |
| { |
| port = PluginBridgeConstants::kDefaultMcpPort; |
| } |
| else |
| { |
| port = server->bind_to_any_port("127.0.0.1"); |
| } |
|
|
| server->listen_after_bind(); |
| 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",PluginBridgeConstants::kVersion}}}}}}; |
| 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","list_instances"},{"description","List all PluginBridge instances in DAW (channel names + ports)"},{"inputSchema",{{"type","object"},{"properties",json::object()}}}}, |
| {{"name","search_param"},{"description","Search params by keyword. Returns index 'i' and current value. Use 'i' as key for set_params/get_params."},{"inputSchema",{{"type","object"},{"properties",{{"keyword",{{"type","string"}}}}},{"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). Keys are param index from search_param."},{"inputSchema",{{"type","object"},{"properties",{{"values",{{"type","object"}}}}},{"required",json::array({"values"})}}}}, |
| {{"name","get_analysis"},{"description","Real-time audio analysis: LUFS, true peak, 7-band balance, stereo width, brightness. Prefixed with channel name."},{"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 (toolName == "list_instances") |
| { |
| auto registry = InstanceRegistry::readAll(); |
| json instances = json::array(); |
| for (auto& [name, p] : registry) |
| instances.push_back({{"channel", name}, {"port", p}}); |
| resultText = instances.dump(); |
| } |
| else if (toolName == "list_plugins") |
| { |
| std::string channel = channelNameCallback ? channelNameCallback() : "Unknown"; |
| json info = {{"channel", channel}, {"port", port}, |
| {"helper", helper.isHelperRunning() ? "connected" : "disconnected"}}; |
| resultText = info.dump(); |
| } |
| else if (toolName == "search_param") |
| { |
| if (searchParamCallback) |
| { |
| resultText = searchParamCallback(args.value("keyword", "")); |
| } |
| else if (helper.isHelperRunning()) |
| { |
| json cmd = {{"cmd","search_param"},{"keyword",args.value("keyword","")}}; |
| auto resp = helper.sendCommand(cmd.dump()); |
| try { auto j = json::parse(resp); resultText = j.value("ok",false) ? j["results"].dump() : "search failed"; } |
| catch (...) { resultText = "parse error"; isError = true; } |
| } |
| else { resultText = "no plugin loaded"; isError = true; } |
| } |
| else if (toolName == "get_params") |
| { |
| if (getParamsCallback) |
| { |
| auto idsJson = args.value("ids", json::array()); |
| std::vector<int> ids; |
| for (auto& v : idsJson) ids.push_back(v.get<int>()); |
| resultText = getParamsCallback(ids); |
| } |
| else if (helper.isHelperRunning()) |
| { |
| json cmd = {{"cmd","get_params"},{"ids",args.value("ids",json::array())}}; |
| auto resp = helper.sendCommand(cmd.dump()); |
| try { auto j = json::parse(resp); resultText = j.value("ok",false) ? j["values"].dump() : "get failed"; } |
| catch (...) { resultText = "parse error"; isError = true; } |
| } |
| else { resultText = "no plugin loaded"; isError = true; } |
| } |
| else if (toolName == "set_params") |
| { |
| if (setParamsCallback) |
| { |
| auto valuesJson = args.value("values", json::object()); |
| std::map<int,float> values; |
| for (auto& [k, v] : valuesJson.items()) |
| values[std::stoi(k)] = v.get<float>(); |
| bool ok = setParamsCallback(values); |
| resultText = ok ? "ok" : "set failed"; |
| if (!ok) isError = true; |
| } |
| else if (helper.isHelperRunning()) |
| { |
| 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 { resultText = "no plugin loaded"; isError = true; } |
| } |
| else if (toolName == "get_analysis") |
| { |
| std::string channel = channelNameCallback ? channelNameCallback() : "Unknown"; |
| if (getAnalysisCallback) |
| resultText = "[" + channel + "] " + getAnalysisCallback(); |
| else |
| resultText = "[" + channel + "] analyser not ready"; |
| } |
| 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(); |
| } |
|
|