@mthines/reaper-mcp 0.14.1 → 0.14.2-beta.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/main.js +219 -33
  2. package/package.json +2 -1
  3. package/reaper/mcp_bridge.lua +114 -19
package/main.js CHANGED
@@ -2202,10 +2202,10 @@ function createServer() {
2202
2202
  }
2203
2203
 
2204
2204
  // apps/reaper-mcp-server/src/main.ts
2205
- import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
2206
- import { join as join4, dirname as dirname2 } from "node:path";
2205
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
2206
+ import { join as join5, dirname as dirname2 } from "node:path";
2207
2207
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2208
- import { homedir as homedir2 } from "node:os";
2208
+ import { homedir as homedir3 } from "node:os";
2209
2209
 
2210
2210
  // apps/reaper-mcp-server/src/cli.ts
2211
2211
  import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
@@ -2391,6 +2391,174 @@ function resolveAssetDirWithFallback(baseDir, buildName, sourceName) {
2391
2391
  return resolveAssetDir(baseDir, sourceName);
2392
2392
  }
2393
2393
 
2394
+ // apps/reaper-mcp-server/src/init.ts
2395
+ import { checkbox, select } from "@inquirer/prompts";
2396
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
2397
+ import { join as join4 } from "node:path";
2398
+ import { homedir as homedir2 } from "node:os";
2399
+ async function runInit(opts, dirResolver) {
2400
+ const isTTY = Boolean(process.stdin.isTTY);
2401
+ const headless = opts.yes || !isTTY;
2402
+ console.log("REAPER MCP \u2014 Interactive Setup\n");
2403
+ const bridgeDir = await ensureBridgeDir();
2404
+ console.log(`REAPER resource path: ${join4(bridgeDir, "..", "..")}
2405
+ `);
2406
+ let selections;
2407
+ if (headless) {
2408
+ selections = {
2409
+ bridge: true,
2410
+ skills: true,
2411
+ settings: true,
2412
+ projectConfig: opts.project,
2413
+ skillsScope: "global"
2414
+ };
2415
+ if (opts.yes) {
2416
+ console.log("Running in non-interactive mode (--yes flag).\n");
2417
+ } else {
2418
+ console.log("Non-interactive terminal detected. Running with defaults.\n");
2419
+ }
2420
+ } else {
2421
+ const components = await checkbox({
2422
+ message: "Which components would you like to install?",
2423
+ choices: [
2424
+ { name: "REAPER Bridge (Lua bridge + JSFX analyzers)", value: "bridge", checked: true },
2425
+ { name: `AI Skills & Agents (knowledge base, Claude agents, rules, skills)`, value: "skills", checked: true },
2426
+ { name: `Claude Code Settings (auto-allow ${MCP_TOOL_NAMES.length} REAPER tools)`, value: "settings", checked: true },
2427
+ { name: "Project Config (.mcp.json in current directory)", value: "projectConfig", checked: false }
2428
+ ]
2429
+ });
2430
+ let skillsScope = "global";
2431
+ if (components.includes("skills")) {
2432
+ const scopeAnswer = await select({
2433
+ message: "Install scope for AI Skills & Agents:",
2434
+ choices: [
2435
+ { name: "Global (~/.claude/) \u2014 available in all projects", value: "global" },
2436
+ { name: "Project (.claude/) \u2014 current directory only", value: "project" }
2437
+ ]
2438
+ });
2439
+ skillsScope = scopeAnswer;
2440
+ }
2441
+ selections = {
2442
+ bridge: components.includes("bridge"),
2443
+ skills: components.includes("skills"),
2444
+ settings: components.includes("settings"),
2445
+ projectConfig: components.includes("projectConfig"),
2446
+ skillsScope
2447
+ };
2448
+ }
2449
+ const __dirname2 = dirResolver();
2450
+ if (selections.bridge) {
2451
+ console.log("Installing REAPER Bridge...");
2452
+ const scriptsDir = getReaperScriptsPath();
2453
+ mkdirSync2(scriptsDir, { recursive: true });
2454
+ const reaperDir = resolveAssetDir(__dirname2, "reaper");
2455
+ const luaSrc = join4(reaperDir, "mcp_bridge.lua");
2456
+ const luaDest = join4(scriptsDir, "mcp_bridge.lua");
2457
+ if (installFile(luaSrc, luaDest)) {
2458
+ console.log(" Installed: mcp_bridge.lua");
2459
+ } else {
2460
+ console.log(` Not found: ${luaSrc}`);
2461
+ }
2462
+ const effectsDir = join4(getReaperEffectsPath(), "reaper-mcp");
2463
+ mkdirSync2(effectsDir, { recursive: true });
2464
+ for (const jsfx of REAPER_ASSETS) {
2465
+ if (jsfx === "mcp_bridge.lua") continue;
2466
+ const src = join4(reaperDir, jsfx);
2467
+ const dest = join4(effectsDir, jsfx);
2468
+ if (installFile(src, dest)) {
2469
+ console.log(` Installed: reaper-mcp/${jsfx}`);
2470
+ } else {
2471
+ console.log(` Not found: ${src}`);
2472
+ }
2473
+ }
2474
+ console.log("");
2475
+ }
2476
+ if (selections.skills) {
2477
+ const isGlobal = selections.skillsScope === "global";
2478
+ const baseDir = isGlobal ? join4(homedir2(), ".claude") : process.cwd();
2479
+ const claudeDir = isGlobal ? baseDir : join4(baseDir, ".claude");
2480
+ console.log(`Installing AI Skills & Agents (${selections.skillsScope})...`);
2481
+ const knowledgeSrc = resolveAssetDir(__dirname2, "knowledge");
2482
+ if (existsSync2(knowledgeSrc)) {
2483
+ const dest = join4(isGlobal ? join4(homedir2(), ".claude") : process.cwd(), "knowledge");
2484
+ const count = copyDirSync(knowledgeSrc, dest);
2485
+ console.log(` Installed knowledge base: ${count} files`);
2486
+ } else {
2487
+ console.log(" Knowledge base not found in package. Skipping.");
2488
+ }
2489
+ const rulesSrc = resolveAssetDirWithFallback(__dirname2, "claude-rules", join4(".claude", "rules"));
2490
+ if (existsSync2(rulesSrc)) {
2491
+ const dest = join4(claudeDir, "rules");
2492
+ const count = copyDirSync(rulesSrc, dest);
2493
+ console.log(` Installed Claude rules: ${count} files`);
2494
+ }
2495
+ const skillsSrc = resolveAssetDirWithFallback(__dirname2, "claude-skills", join4(".claude", "skills"));
2496
+ if (existsSync2(skillsSrc)) {
2497
+ const dest = join4(claudeDir, "skills");
2498
+ const count = copyDirSync(skillsSrc, dest);
2499
+ console.log(` Installed Claude skills: ${count} files`);
2500
+ }
2501
+ const agentsSrc = resolveAssetDirWithFallback(__dirname2, "claude-agents", join4(".claude", "agents"));
2502
+ if (existsSync2(agentsSrc)) {
2503
+ const dest = join4(claudeDir, "agents");
2504
+ const count = copyDirSync(agentsSrc, dest);
2505
+ console.log(` Installed Claude agents: ${count} files`);
2506
+ }
2507
+ console.log("");
2508
+ }
2509
+ if (selections.settings) {
2510
+ console.log("Configuring Claude Code Settings...");
2511
+ const settingsDir = join4(homedir2(), ".claude");
2512
+ const settingsPath = join4(settingsDir, "settings.json");
2513
+ const result = ensureClaudeSettings(settingsPath);
2514
+ if (result === "created") {
2515
+ console.log(` Created: ${settingsPath}`);
2516
+ } else if (result === "updated") {
2517
+ console.log(` Updated with new REAPER tools: ${settingsPath}`);
2518
+ } else {
2519
+ console.log(` Already configured: ${settingsPath}`);
2520
+ }
2521
+ console.log("");
2522
+ }
2523
+ if (selections.projectConfig) {
2524
+ console.log("Creating Project Config...");
2525
+ const mcpJsonPath = join4(process.cwd(), ".mcp.json");
2526
+ if (createMcpJson(mcpJsonPath)) {
2527
+ console.log(` Created: ${mcpJsonPath}`);
2528
+ } else {
2529
+ console.log(` Already exists: ${mcpJsonPath}`);
2530
+ }
2531
+ console.log("");
2532
+ }
2533
+ console.log("Running system check...");
2534
+ const bridgeRunning = await isBridgeRunning();
2535
+ console.log(` Lua bridge: ${bridgeRunning ? "Connected" : "Not detected (start after REAPER is open)"}`);
2536
+ const globalClaudeDir = join4(homedir2(), ".claude");
2537
+ const localAgents = existsSync2(join4(process.cwd(), ".claude", "agents"));
2538
+ const globalAgents = existsSync2(join4(globalClaudeDir, "agents"));
2539
+ const agentsExist = localAgents || globalAgents;
2540
+ console.log(` Mix agents: ${agentsExist ? "Installed" : "Not installed"}`);
2541
+ const mcpJsonExists = existsSync2(join4(process.cwd(), ".mcp.json"));
2542
+ console.log(` MCP config: ${mcpJsonExists ? ".mcp.json found" : ".mcp.json not present"}`);
2543
+ console.log("");
2544
+ console.log("Setup complete! Next steps:");
2545
+ if (selections.bridge) {
2546
+ const scriptsDir = getReaperScriptsPath();
2547
+ const luaDest = join4(scriptsDir, "mcp_bridge.lua");
2548
+ console.log(" 1. Open REAPER");
2549
+ console.log(" 2. Actions > Show action list > Load ReaScript");
2550
+ console.log(` 3. Select: ${luaDest}`);
2551
+ console.log(" 4. Run the script (it keeps running via defer loop)");
2552
+ console.log(" 5. Open Claude Code \u2014 REAPER tools + mix engineer agents are ready");
2553
+ } else {
2554
+ console.log(" 1. Open Claude Code \u2014 REAPER tools + mix engineer agents are ready");
2555
+ }
2556
+ if (agentsExist) {
2557
+ console.log('\nTry: @mix-engineer "Please gain stage my tracks"');
2558
+ console.log('Or: @mix-analyzer "Roast my mix"');
2559
+ }
2560
+ }
2561
+
2394
2562
  // apps/reaper-mcp-server/src/main.ts
2395
2563
  var __dirname = dirname2(fileURLToPath2(import.meta.url));
2396
2564
  async function setup() {
@@ -2399,23 +2567,23 @@ async function setup() {
2399
2567
  console.log(`Bridge directory: ${bridgeDir}
2400
2568
  `);
2401
2569
  const scriptsDir = getReaperScriptsPath();
2402
- mkdirSync2(scriptsDir, { recursive: true });
2570
+ mkdirSync3(scriptsDir, { recursive: true });
2403
2571
  const reaperDir = resolveAssetDir(__dirname, "reaper");
2404
- const luaSrc = join4(reaperDir, "mcp_bridge.lua");
2405
- const luaDest = join4(scriptsDir, "mcp_bridge.lua");
2572
+ const luaSrc = join5(reaperDir, "mcp_bridge.lua");
2573
+ const luaDest = join5(scriptsDir, "mcp_bridge.lua");
2406
2574
  console.log("Installing Lua bridge...");
2407
2575
  if (installFile(luaSrc, luaDest)) {
2408
2576
  console.log(` Installed: mcp_bridge.lua`);
2409
2577
  } else {
2410
2578
  console.log(` Not found: ${luaSrc}`);
2411
2579
  }
2412
- const effectsDir = join4(getReaperEffectsPath(), "reaper-mcp");
2413
- mkdirSync2(effectsDir, { recursive: true });
2580
+ const effectsDir = join5(getReaperEffectsPath(), "reaper-mcp");
2581
+ mkdirSync3(effectsDir, { recursive: true });
2414
2582
  console.log("\nInstalling JSFX analyzers...");
2415
2583
  for (const jsfx of REAPER_ASSETS) {
2416
2584
  if (jsfx === "mcp_bridge.lua") continue;
2417
- const src = join4(reaperDir, jsfx);
2418
- const dest = join4(effectsDir, jsfx);
2585
+ const src = join5(reaperDir, jsfx);
2586
+ const dest = join5(effectsDir, jsfx);
2419
2587
  if (installFile(src, dest)) {
2420
2588
  console.log(` Installed: reaper-mcp/${jsfx}`);
2421
2589
  } else {
@@ -2438,41 +2606,41 @@ async function installSkills(scope) {
2438
2606
  console.log(`REAPER MCP \u2014 Install AI Mix Engineer Skills (scope: ${scope})
2439
2607
  `);
2440
2608
  const isGlobal = scope === "global";
2441
- const baseDir = isGlobal ? join4(homedir2(), ".claude") : process.cwd();
2442
- const claudeDir = isGlobal ? baseDir : join4(baseDir, ".claude");
2609
+ const baseDir = isGlobal ? join5(homedir3(), ".claude") : process.cwd();
2610
+ const claudeDir = isGlobal ? baseDir : join5(baseDir, ".claude");
2443
2611
  const knowledgeSrc = resolveAssetDir(__dirname, "knowledge");
2444
- if (existsSync2(knowledgeSrc)) {
2445
- const dest = join4(baseDir, "knowledge");
2612
+ if (existsSync3(knowledgeSrc)) {
2613
+ const dest = join5(baseDir, "knowledge");
2446
2614
  const count = copyDirSync(knowledgeSrc, dest);
2447
2615
  console.log(`Installed knowledge base: ${count} files \u2192 ${dest}`);
2448
2616
  } else {
2449
2617
  console.log("Knowledge base not found in package. Skipping.");
2450
2618
  }
2451
- const rulesSrc = resolveAssetDirWithFallback(__dirname, "claude-rules", join4(".claude", "rules"));
2452
- if (existsSync2(rulesSrc)) {
2453
- const dest = join4(claudeDir, "rules");
2619
+ const rulesSrc = resolveAssetDirWithFallback(__dirname, "claude-rules", join5(".claude", "rules"));
2620
+ if (existsSync3(rulesSrc)) {
2621
+ const dest = join5(claudeDir, "rules");
2454
2622
  const count = copyDirSync(rulesSrc, dest);
2455
2623
  console.log(`Installed Claude rules: ${count} files \u2192 ${dest}`);
2456
2624
  } else {
2457
2625
  console.log("Claude rules not found in package. Skipping.");
2458
2626
  }
2459
- const skillsSrc = resolveAssetDirWithFallback(__dirname, "claude-skills", join4(".claude", "skills"));
2460
- if (existsSync2(skillsSrc)) {
2461
- const dest = join4(claudeDir, "skills");
2627
+ const skillsSrc = resolveAssetDirWithFallback(__dirname, "claude-skills", join5(".claude", "skills"));
2628
+ if (existsSync3(skillsSrc)) {
2629
+ const dest = join5(claudeDir, "skills");
2462
2630
  const count = copyDirSync(skillsSrc, dest);
2463
2631
  console.log(`Installed Claude skills: ${count} files \u2192 ${dest}`);
2464
2632
  } else {
2465
2633
  console.log("Claude skills not found in package. Skipping.");
2466
2634
  }
2467
- const agentsSrc = resolveAssetDirWithFallback(__dirname, "claude-agents", join4(".claude", "agents"));
2468
- if (existsSync2(agentsSrc)) {
2469
- const dest = join4(claudeDir, "agents");
2635
+ const agentsSrc = resolveAssetDirWithFallback(__dirname, "claude-agents", join5(".claude", "agents"));
2636
+ if (existsSync3(agentsSrc)) {
2637
+ const dest = join5(claudeDir, "agents");
2470
2638
  const count = copyDirSync(agentsSrc, dest);
2471
2639
  console.log(`Installed Claude agents: ${count} files \u2192 ${dest}`);
2472
2640
  } else {
2473
2641
  console.log("Claude agents not found in package. Skipping.");
2474
2642
  }
2475
- const settingsPath = join4(claudeDir, "settings.json");
2643
+ const settingsPath = join5(claudeDir, "settings.json");
2476
2644
  const result = ensureClaudeSettings(settingsPath);
2477
2645
  if (result === "created") {
2478
2646
  console.log(`Created Claude settings: ${settingsPath}`);
@@ -2482,7 +2650,7 @@ async function installSkills(scope) {
2482
2650
  console.log(`Claude settings already has all REAPER tools: ${settingsPath}`);
2483
2651
  }
2484
2652
  if (!isGlobal) {
2485
- const mcpJsonPath = join4(baseDir, ".mcp.json");
2653
+ const mcpJsonPath = join5(baseDir, ".mcp.json");
2486
2654
  if (createMcpJson(mcpJsonPath)) {
2487
2655
  console.log(`
2488
2656
  Created: ${mcpJsonPath}`);
@@ -2503,24 +2671,24 @@ async function doctor() {
2503
2671
  if (!bridgeRunning) {
2504
2672
  console.log(' \u2192 Run "npx @mthines/reaper-mcp setup" then load mcp_bridge.lua in REAPER');
2505
2673
  }
2506
- const globalClaudeDir = join4(homedir2(), ".claude");
2507
- const localAgents = existsSync2(join4(process.cwd(), ".claude", "agents"));
2508
- const globalAgents = existsSync2(join4(globalClaudeDir, "agents"));
2674
+ const globalClaudeDir = join5(homedir3(), ".claude");
2675
+ const localAgents = existsSync3(join5(process.cwd(), ".claude", "agents"));
2676
+ const globalAgents = existsSync3(join5(globalClaudeDir, "agents"));
2509
2677
  const agentsExist = localAgents || globalAgents;
2510
2678
  const agentsLocation = localAgents ? ".claude/agents/" : globalAgents ? "~/.claude/agents/" : "";
2511
2679
  console.log(`Mix agents: ${agentsExist ? `\u2713 Found (${agentsLocation})` : "\u2717 Not installed"}`);
2512
2680
  if (!agentsExist) {
2513
2681
  console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"');
2514
2682
  }
2515
- const localKnowledge = existsSync2(join4(process.cwd(), "knowledge"));
2516
- const globalKnowledge = existsSync2(join4(globalClaudeDir, "knowledge"));
2683
+ const localKnowledge = existsSync3(join5(process.cwd(), "knowledge"));
2684
+ const globalKnowledge = existsSync3(join5(globalClaudeDir, "knowledge"));
2517
2685
  const knowledgeExists = localKnowledge || globalKnowledge;
2518
2686
  const knowledgeLocation = localKnowledge ? "project" : globalKnowledge ? "~/.claude/" : "";
2519
2687
  console.log(`Knowledge base: ${knowledgeExists ? `\u2713 Found (${knowledgeLocation})` : "\u2717 Not installed"}`);
2520
2688
  if (!knowledgeExists) {
2521
2689
  console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"');
2522
2690
  }
2523
- const mcpJsonExists = existsSync2(join4(process.cwd(), ".mcp.json"));
2691
+ const mcpJsonExists = existsSync3(join5(process.cwd(), ".mcp.json"));
2524
2692
  console.log(`MCP config: ${mcpJsonExists ? "\u2713 .mcp.json found" : "\u2717 .mcp.json missing"}`);
2525
2693
  if (!mcpJsonExists) {
2526
2694
  console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills --project" to create .mcp.json');
@@ -2583,7 +2751,19 @@ async function serve() {
2583
2751
  });
2584
2752
  }
2585
2753
  var command = process.argv[2];
2754
+ var cliArgs = process.argv.slice(3);
2755
+ var hasYesFlag = cliArgs.includes("--yes") || cliArgs.includes("-y");
2756
+ var hasProjectFlag = cliArgs.includes("--project");
2586
2757
  switch (command) {
2758
+ case "init":
2759
+ runInit(
2760
+ { yes: hasYesFlag, project: hasProjectFlag },
2761
+ () => __dirname
2762
+ ).catch((err) => {
2763
+ console.error("Init failed:", err);
2764
+ process.exit(1);
2765
+ });
2766
+ break;
2587
2767
  case "setup":
2588
2768
  setup().catch((err) => {
2589
2769
  console.error("Setup failed:", err);
@@ -2623,6 +2803,9 @@ switch (command) {
2623
2803
  Usage:
2624
2804
  npx @mthines/reaper-mcp Start MCP server (stdio mode)
2625
2805
  npx @mthines/reaper-mcp serve Start MCP server (stdio mode)
2806
+ npx @mthines/reaper-mcp init Guided interactive setup (recommended for new users)
2807
+ npx @mthines/reaper-mcp init --yes Non-interactive setup (install everything with defaults)
2808
+ npx @mthines/reaper-mcp init --project Include .mcp.json in current directory
2626
2809
  npx @mthines/reaper-mcp setup Install Lua bridge + JSFX analyzers into REAPER
2627
2810
  npx @mthines/reaper-mcp install-skills Install AI knowledge + agents globally (default)
2628
2811
  npx @mthines/reaper-mcp install-skills --project Install into current project directory
@@ -2630,7 +2813,10 @@ Usage:
2630
2813
  npx @mthines/reaper-mcp doctor Check that everything is configured correctly
2631
2814
  npx @mthines/reaper-mcp status Check if Lua bridge is running in REAPER
2632
2815
 
2633
- Quick Start:
2816
+ Quick Start (interactive):
2817
+ npx @mthines/reaper-mcp init # guided setup \u2014 select components interactively
2818
+
2819
+ Quick Start (manual steps):
2634
2820
  1. npx @mthines/reaper-mcp setup # install REAPER components
2635
2821
  2. Load mcp_bridge.lua in REAPER (Actions > Load ReaScript > Run)
2636
2822
  3. npx @mthines/reaper-mcp install-skills # install AI knowledge + agents (globally)
@@ -2638,7 +2824,7 @@ Quick Start:
2638
2824
 
2639
2825
  Tip: install globally for shorter commands:
2640
2826
  npm install -g @mthines/reaper-mcp
2641
- reaper-mcp setup
2827
+ reaper-mcp init
2642
2828
  `);
2643
2829
  break;
2644
2830
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mthines/reaper-mcp",
3
- "version": "0.14.1",
3
+ "version": "0.14.2-beta.17.1",
4
4
  "type": "module",
5
5
  "description": "MCP server for controlling REAPER DAW — real-time mixing, FX control, and frequency analysis for AI agents",
6
6
  "license": "MIT",
@@ -27,6 +27,7 @@
27
27
  "reaper-mcp": "./main.js"
28
28
  },
29
29
  "dependencies": {
30
+ "@inquirer/prompts": "^8.3.2",
30
31
  "@modelcontextprotocol/sdk": "^1.12.0",
31
32
  "@opentelemetry/api": "^1.9.0",
32
33
  "@opentelemetry/auto-instrumentations-node": "^0.71.0",
@@ -584,23 +584,10 @@ function handlers.read_track_spectrum(params)
584
584
  local track = reaper.GetTrack(0, idx)
585
585
  if not track then return nil, "Track " .. idx .. " not found" end
586
586
 
587
- -- Check if MCP analyzer JSFX is already on the track
588
- local analyzer_idx = -1
589
- local fx_count = reaper.TrackFX_GetCount(track)
590
- for i = 0, fx_count - 1 do
591
- local _, name = reaper.TrackFX_GetFXName(track, i)
592
- if name and name:find(MCP_ANALYZER_FX_NAME) then
593
- analyzer_idx = i
594
- break
595
- end
596
- end
597
-
598
- -- Auto-insert if not present
599
- if analyzer_idx < 0 then
600
- analyzer_idx = reaper.TrackFX_AddByName(track, MCP_ANALYZER_FX_NAME, false, -1)
601
- if analyzer_idx < 0 then
602
- return nil, "MCP Spectrum Analyzer JSFX not found. Run 'reaper-mcp setup' to install it."
603
- end
587
+ -- Find or auto-insert analyzer (uses MCP Meters container if available)
588
+ local analyzer_idx, err = ensure_jsfx_on_track(track, MCP_ANALYZER_FX_NAME)
589
+ if not analyzer_idx then
590
+ return nil, err or "MCP Spectrum Analyzer JSFX not found. Run 'reaper-mcp setup' to install it."
604
591
  end
605
592
 
606
593
  -- Read spectrum data from gmem
@@ -1228,10 +1215,119 @@ end
1228
1215
  local MCP_LUFS_METER_FX_NAME = "reaper-mcp/mcp_lufs_meter"
1229
1216
  local MCP_CORRELATION_METER_FX_NAME = "reaper-mcp/mcp_correlation_meter"
1230
1217
  local MCP_CREST_FACTOR_FX_NAME = "reaper-mcp/mcp_crest_factor"
1218
+ local MCP_FX_PREFIX = "reaper%-mcp/" -- Lua pattern for matching MCP JSFX names
1219
+
1220
+ -- Per-track cache of MCP container FX index to avoid rescanning on every read.
1221
+ -- Keyed by track pointer (userdata), value is container FX index or false (no container).
1222
+ -- Invalidated when the FX chain changes (checked via TrackFX_GetCount).
1223
+ local container_cache = {}
1224
+
1225
+ -- Find or create the MCP Meters container on a track.
1226
+ -- Uses REAPER 7.06+ container_item.X API. Returns container FX index, or nil
1227
+ -- if containers are not supported (REAPER < 7.06).
1228
+ local function find_or_create_mcp_container(track)
1229
+ -- Check cache first
1230
+ local cache_entry = container_cache[tostring(track)]
1231
+ if cache_entry then
1232
+ local cached_idx, cached_fx_count = cache_entry[1], cache_entry[2]
1233
+ local current_fx_count = reaper.TrackFX_GetCount(track)
1234
+ if current_fx_count == cached_fx_count then
1235
+ if cached_idx == false then return nil end -- cached negative result
1236
+ return cached_idx
1237
+ end
1238
+ -- FX count changed — invalidate cache
1239
+ container_cache[tostring(track)] = nil
1240
+ end
1241
+
1242
+ local fx_count = reaper.TrackFX_GetCount(track)
1243
+
1244
+ -- Search for existing container that holds MCP JSFX
1245
+ for i = 0, fx_count - 1 do
1246
+ local _, name = reaper.TrackFX_GetFXName(track, i)
1247
+ if name and (name:find("Container") or name:find("MCP Meters")) then
1248
+ local ok, count_str = reaper.TrackFX_GetNamedConfigParm(track, i, "container_count")
1249
+ if ok then
1250
+ local count = tonumber(count_str) or 0
1251
+ for j = 0, count - 1 do
1252
+ local _, addr_str = reaper.TrackFX_GetNamedConfigParm(track, i, "container_item." .. j)
1253
+ local addr = tonumber(addr_str)
1254
+ if addr then
1255
+ local _, fx_name = reaper.TrackFX_GetFXName(track, addr)
1256
+ if fx_name and fx_name:find(MCP_FX_PREFIX) then
1257
+ -- Found our container — cache and return
1258
+ container_cache[tostring(track)] = { i, fx_count }
1259
+ return i
1260
+ end
1261
+ end
1262
+ end
1263
+ end
1264
+ end
1265
+ end
1266
+
1267
+ -- No existing MCP container — create one at end of chain
1268
+ local idx = reaper.TrackFX_AddByName(track, "Container", false, -1)
1269
+ if idx < 0 then
1270
+ -- Container FX not available (REAPER < 7)
1271
+ container_cache[tostring(track)] = { false, fx_count }
1272
+ return nil
1273
+ end
1274
+
1275
+ -- Name the container "MCP Meters" for easy identification
1276
+ reaper.TrackFX_SetNamedConfigParm(track, idx, "renamed_name", "MCP Meters")
1277
+
1278
+ -- Verify container API works (confirms REAPER 7.06+)
1279
+ local ok = reaper.TrackFX_GetNamedConfigParm(track, idx, "container_count")
1280
+ if not ok then
1281
+ -- API not available — remove the container and fall back
1282
+ reaper.TrackFX_Delete(track, idx)
1283
+ container_cache[tostring(track)] = { false, fx_count }
1284
+ return nil
1285
+ end
1286
+
1287
+ -- Cache the new container (fx_count + 1 because we just added the container)
1288
+ container_cache[tostring(track)] = { idx, reaper.TrackFX_GetCount(track) }
1289
+ return idx
1290
+ end
1231
1291
 
1232
1292
  -- Helper: find or auto-insert a named JSFX on a track.
1233
- -- Returns the FX index (0-based) on success, or nil + error message on failure.
1293
+ -- Tries to place inside an "MCP Meters" FX Container (REAPER 7.06+).
1294
+ -- Falls back to direct insertion on older REAPER versions.
1295
+ -- Returns the FX index (possibly container-addressed) on success, or nil + error.
1234
1296
  local function ensure_jsfx_on_track(track, fx_name)
1297
+ -- Try container approach first (REAPER 7.06+)
1298
+ local container_idx = find_or_create_mcp_container(track)
1299
+ if container_idx then
1300
+ local ok, count_str = reaper.TrackFX_GetNamedConfigParm(track, container_idx, "container_count")
1301
+ local count = tonumber(count_str) or 0
1302
+
1303
+ -- Search inside container for existing JSFX
1304
+ for i = 0, count - 1 do
1305
+ local _, addr_str = reaper.TrackFX_GetNamedConfigParm(track, container_idx, "container_item." .. i)
1306
+ local addr = tonumber(addr_str)
1307
+ if addr then
1308
+ local _, name = reaper.TrackFX_GetFXName(track, addr)
1309
+ if name and name:find(fx_name, 1, true) then
1310
+ return addr -- Found inside container
1311
+ end
1312
+ end
1313
+ end
1314
+
1315
+ -- Not found — insert inside container
1316
+ -- Use legacy addressing: 0x2000000 + (1-based slot) * (top_chain_count + 1) + (1-based container pos)
1317
+ local top_count = reaper.TrackFX_GetCount(track)
1318
+ local slot = count + 1 -- 1-based, next empty slot
1319
+ local container_pos = container_idx + 1 -- 1-based
1320
+ local insert_addr = 0x2000000 + slot * (top_count + 1) + container_pos
1321
+ local new_idx = reaper.TrackFX_AddByName(track, fx_name, false, insert_addr)
1322
+ if new_idx >= 0 then
1323
+ -- Invalidate cache since FX count changed
1324
+ container_cache[tostring(track)] = nil
1325
+ return new_idx
1326
+ end
1327
+ -- Container insertion failed — fall through to direct
1328
+ end
1329
+
1330
+ -- Fallback: direct insertion (REAPER < 7.06 or container failed)
1235
1331
  local fx_count = reaper.TrackFX_GetCount(track)
1236
1332
  for i = 0, fx_count - 1 do
1237
1333
  local _, name = reaper.TrackFX_GetFXName(track, i)
@@ -1239,7 +1335,6 @@ local function ensure_jsfx_on_track(track, fx_name)
1239
1335
  return i
1240
1336
  end
1241
1337
  end
1242
- -- Auto-insert
1243
1338
  local idx = reaper.TrackFX_AddByName(track, fx_name, false, -1)
1244
1339
  if idx < 0 then
1245
1340
  return nil, fx_name .. " JSFX not found. Run 'reaper-mcp setup' to install it."