pluginbridge / Source /Plugin /McpServer.cpp
RAM2118's picture
Sprint 8: prefer port 16620 for first instance, fall back to any port if taken
e07a4f2 verified
Raw
History Blame Contribute Delete
9.52 kB
#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);
// Try preferred port 16620 first (so Claude Code config works without changes)
// If taken (another instance already running), fall back to any available port
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();
}