PluginBridge β Live Sync (keep this SHORT β history goes in SPRINT-LOG.md)
CURRENT STATE
Version: v0.8.0
Build: β
Clean (2026-05-17)
GUI: β
TreeView picker + in-process createEditor() + live spectrum + channel dropdown
MCP: β
6 tools β list_plugins, list_instances, search_param, get_params, set_params, get_analysis
AudioAnalyser: β
LUFS / FFT bands / stereo width / centroid / true peak
Param routing: β
Sprint 7 β search/get/set_params go through inProcessPlugin for SAFE plugins β EQ changes visible in Pro-Q 4 GUI
Port: β
Sprint 8 β first instance gets port 16620, additional instances get auto-port
ML INTERN β CLAUDE CODE
Post questions here before writing any Mac-specific code. Claude Code will test on Mac and answer before you write anything.
(no pending questions)
CLAUDE CODE β ML INTERN
Read this at the start of every coding session.
STATUS: TASK READY β Sprint 7
TASK: Route MCP param ops through in-process plugin for SAFE plugins
(fix: EQ changes via MCP must appear in Pro-Q 4 / plugin GUI)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ROOT CAUSE:
After Sprint 5, audio routes through inProcessPlugin.
But search_param / get_params / set_params still use
HelperConnection IPC β Helper process β Helper's plugin instance.
That instance has no GUI β Pro-Q 4 GUI shows inProcessPlugin's state.
Result: EQ changes are applied silently to a hidden instance nobody sees.
FIX ARCHITECTURE:
Add three param callbacks to McpServer (same pattern as
getAnalysisCallback and channelNameCallback already in McpServer.h).
Processor wires them to inProcessPlugin when SAFE plugin is loaded.
McpServer uses callbacks when set; falls back to Helper IPC when null.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ββ PART 1: McpServer.h ββββββββββββββββββββββββββββββββββββββ
ADD three new callbacks (next to existing getAnalysisCallback):
// Param callbacks β wired to inProcessPlugin when SAFE, null when UNSAFE
std::function<std::string(const std::string& keyword)> searchParamCallback;
std::function<std::string(const std::vector<int>& ids)> getParamsCallback;
std::function<bool(const std::map<int,float>& values)> setParamsCallback;
ββ PART 2: McpServer.cpp β handleToolsCall() ββββββββββββββββ
CHANGE search_param handler (in-process first, Helper fallback):
else if (toolName == "search_param")
{
if (searchParamCallback)
{
resultText = searchParamCallback(args.value("keyword",""));
}
else
{
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; }
}
}
CHANGE get_params handler similarly:
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
{
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; }
}
}
CHANGE set_params handler similarly:
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
{
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; }
}
}
ALSO update search_param tool description in handleToolsList():
// Before: "Search params by keyword"
// After: "Search params by keyword. Returns index 'i' and current value. Use 'i' as key for set_params/get_params."
ββ PART 3: PluginBridgeProcessor.cpp β wire callbacks ββββββ
Call wireInProcessParamCallbacks() from loadInProcessPlugin() AFTER
inProcessPlugin is ready. Call clearInProcessParamCallbacks() from
unloadInProcessPlugin() BEFORE inProcessPlugin is cleared.
Wire searchParamCallback:
mcpServer->searchParamCallback = [this](const std::string& keyword) -> std::string {
auto& params = inProcessPlugin->getParameters();
json results = json::array();
std::string kw = keyword;
std::transform(kw.begin(), kw.end(), kw.begin(), ::tolower);
for (int i = 0; i < params.size(); ++i)
{
std::string name = params[i]->getName(128).toStdString();
std::string nameLow = name;
std::transform(nameLow.begin(), nameLow.end(), nameLow.begin(), ::tolower);
if (nameLow.find(kw) != std::string::npos)
results.push_back({{"i", i}, {"name", name}, {"value", params[i]->getValue()}});
}
return results.dump();
};
// NOTE: "i" = array index. Use this as key for set_params/get_params.
// This also fixes the id-vs-index bug from Sprint 6.
Wire getParamsCallback:
mcpServer->getParamsCallback = [this](const std::vector<int>& ids) -> std::string {
auto& params = inProcessPlugin->getParameters();
json values = json::object();
for (int idx : ids)
if (idx >= 0 && idx < params.size())
values[std::to_string(idx)] = params[idx]->getValue();
return values.dump();
};
Wire setParamsCallback:
mcpServer->setParamsCallback = [this](const std::map<int,float>& values) -> bool {
juce::MessageManager::callAsync([this, values]() {
auto& p = inProcessPlugin->getParameters();
for (auto& [idx, val] : values)
if (idx >= 0 && idx < p.size())
p[idx]->setValueNotifyingHost(val);
});
return true;
};
// setValueNotifyingHost β notifies plugin GUI, changes appear in Pro-Q 4 instantly.
Clear in clearInProcessParamCallbacks():
mcpServer->searchParamCallback = nullptr;
mcpServer->getParamsCallback = nullptr;
mcpServer->setParamsCallback = nullptr;
ββ SCOPE ββββββββββββββββββββββββββββββββββββββββββββββββββββ
TOUCH:
Source/Plugin/McpServer.h β add 3 callbacks
Source/Plugin/McpServer.cpp β use callbacks in handleToolsCall(), update description
Source/Plugin/PluginBridgeProcessor.cpp β wire/clear callbacks
DO NOT TOUCH:
PluginBridgeEditor.mm β LOCKED
PluginPickerComponent.mm β LOCKED
HelperPluginHost.mm β LOCKED
HelperConnection.cpp β LOCKED
CMakeLists.txt β no new files
ββ VERIFIED APIs (Claude Code β 2026-05-17) βββββββββββββββββ
inProcessPlugin->getParameters() β juce::Array<AudioProcessorParameter*> β
params[i]->getName(128).toStdString() β parameter name string β
params[i]->getValue() β float 0.0β1.0 β
params[i]->setValueNotifyingHost(float) β sets value + notifies GUI β
juce::MessageManager::callAsync(lambda) β dispatch to message thread β
getInProcessPlugin() on Processor β already public (PluginBridgeProcessor.h line 64) β
ββ EXPECTED RESULT ββββββββββββββββββββββββββββββββββββββββββ
search_param "freq" β returns in-process Pro-Q 4 params with index "i"
set_params {"26": 0.748} β Band 2 moves to ~4kHz in Pro-Q 4 GUI immediately
get_params [26] β returns current value from in-process instance
UNSAFE plugins: callbacks null β falls back to Helper IPC (unchanged)
NEXT PUSH β ML Intern fills before every push
STATUS: (empty β awaiting Sprint 8 task from Claude Code)
LAST BUILD RESULT β Claude Code fills after pull+build
Date: 2026-05-17 (Sprint 8 complete)
Commits pulled: e07a4f2 (McpServer.cpp port fix)
Build: β
Clean β [100%] Built target PluginBridge_VST3
Tests:
search_param "freq" β
returns in-process params with index "i" + display value
set_params {24:1, 26:0.748, 27:0.75} β
Band 2 appeared at ~4kHz in Pro-Q 4 GUI instantly
get_params [26] β
reads back 0.748 from in-process instance
port 16620 first-inst β
restored (uses kDefaultMcpPort constant)
NOTE: must set "Band X Used"=1.0 to make a band visible β it's a separate param from "Enabled"
Issues: none