# 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 searchParamCallback; std::function& ids)> getParamsCallback; std::function& 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 ids; for (auto& v : idsJson) ids.push_back(v.get()); 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 values; for (auto& [k,v] : valuesJson.items()) values[std::stoi(k)] = v.get(); 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& 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& 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 ✅ 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 ```