@mthines/reaper-mcp 0.7.0 → 0.9.0

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.
@@ -0,0 +1,175 @@
1
+ ---
2
+ name: Stem Preparation
3
+ id: stem-prep
4
+ description: Verify bus structure and routing for stem export readiness
5
+ ---
6
+
7
+ # Stem Preparation
8
+
9
+ ## When to Use
10
+
11
+ After mastering is complete and before final delivery. Stem preparation ensures the session's bus structure, routing, and naming conventions are correct so that exported stems will sum to match the full mix exactly.
12
+
13
+ Use this workflow when:
14
+ - The mix is complete and mastered, ready for stem export
15
+ - Stems are needed for sync licensing, remix, or immersive audio
16
+ - A mastering engineer has requested stems instead of a stereo mix
17
+ - The session needs routing verification before export
18
+
19
+ ## Prerequisites
20
+
21
+ - Mix is complete (all mixing and mastering decisions are finalized)
22
+ - Bus structure exists (drum bus, vocal bus, instrument bus, etc.)
23
+ - Session is playing back correctly (no muted tracks that should be active)
24
+ - Genre and delivery specs are known
25
+
26
+ ## Step-by-Step
27
+
28
+ ### Step 1: Save a safety snapshot
29
+
30
+ ```
31
+ tool: snapshot_save
32
+ params:
33
+ name: "pre-stem-prep"
34
+ description: "State before stem preparation"
35
+ ```
36
+
37
+ ### Step 2: Document the current bus structure
38
+
39
+ ```
40
+ tool: list_tracks
41
+ tool: get_project_info
42
+ ```
43
+
44
+ Map out the full routing hierarchy. For each track:
45
+
46
+ ```
47
+ tool: get_track_routing
48
+ params:
49
+ trackIndex: [n]
50
+ ```
51
+
52
+ Identify:
53
+ - Which tracks route to which buses
54
+ - Which buses route to the master
55
+ - Any orphan tracks routing directly to master (should go through a bus)
56
+ - Any tracks with sends to multiple destinations
57
+
58
+ ### Step 3: Verify standard stem groups
59
+
60
+ Typical stem groups for export:
61
+
62
+ | Stem | Contains | Bus |
63
+ |------|----------|-----|
64
+ | Drums | Kick, Snare, Toms, OH, Room | Drum Bus |
65
+ | Bass | Bass DI, Bass Amp | Bass Bus |
66
+ | Guitars | All guitar tracks | Guitar Bus |
67
+ | Keys/Synths | Piano, Organ, Pads, Leads | Keys Bus |
68
+ | Vocals | Lead, BVs, Harmonies | Vocal Bus |
69
+ | Effects | Reverbs, Delays (printed) | Effects Bus |
70
+
71
+ Verify each group routes correctly:
72
+
73
+ ```
74
+ tool: get_track_properties
75
+ params:
76
+ trackIndex: [bus track index]
77
+ ```
78
+
79
+ ### Step 4: Check for routing problems
80
+
81
+ Common issues to flag:
82
+
83
+ 1. **Orphan tracks**: Source tracks routing directly to master instead of through a bus
84
+ 2. **Double-routing**: A track routing to both a bus AND the master (will be in the stem AND the full mix twice)
85
+ 3. **Missing tracks**: Tracks that are muted or have no output
86
+ 4. **Send-only tracks**: Reverb returns that should be captured in a stem but are going to master instead of a bus
87
+ 5. **Master bus processing**: Note which FX are on the master bus — stems are typically exported pre-master-bus-processing
88
+
89
+ For each bus, check that all expected source tracks are present:
90
+
91
+ ```
92
+ tool: get_track_routing
93
+ params:
94
+ trackIndex: [bus index]
95
+ ```
96
+
97
+ ### Step 5: Verify naming conventions
98
+
99
+ Stems need clear, consistent names for delivery. Verify bus names follow a convention:
100
+
101
+ ```
102
+ [Song Title]_[Stem Name]_[Bit Depth]-[Sample Rate]
103
+ ```
104
+
105
+ Examples:
106
+ - `MySong_Drums_24-48.wav`
107
+ - `MySong_Vocals_24-48.wav`
108
+ - `MySong_Bass_24-48.wav`
109
+
110
+ Check that bus track names are clean and descriptive:
111
+
112
+ ```
113
+ tool: get_track_properties
114
+ params:
115
+ trackIndex: [bus index]
116
+ ```
117
+
118
+ Rename if needed:
119
+ ```
120
+ tool: set_track_property
121
+ params:
122
+ trackIndex: [bus index]
123
+ property: "name"
124
+ value: "Drum Bus"
125
+ ```
126
+
127
+ ### Step 6: Verify technical specs
128
+
129
+ ```
130
+ tool: get_project_info
131
+ ```
132
+
133
+ Confirm:
134
+ - **Sample rate**: Matches delivery requirements (typically 44.1 kHz or 48 kHz)
135
+ - **Bit depth**: 24-bit or 32-bit float for stems
136
+ - **All stems have same duration**: Check that all bus tracks span the full song (from before first note to after last decay)
137
+
138
+ ### Step 7: Document the stem map
139
+
140
+ Create a clear report of:
141
+ - Each stem name and what tracks it contains
142
+ - Routing chain for each stem
143
+ - Any processing on stem buses (baked into the stem)
144
+ - Master bus processing that will NOT be in individual stems
145
+ - Total stem count
146
+ - Technical specs (sample rate, bit depth)
147
+
148
+ ### Step 8: Save post-prep snapshot
149
+
150
+ ```
151
+ tool: snapshot_save
152
+ params:
153
+ name: "post-stem-prep"
154
+ description: "Stems verified — routing correct, naming consistent, ready for export"
155
+ ```
156
+
157
+ ## Verification
158
+
159
+ After completing stem preparation:
160
+
161
+ 1. Every source track routes to exactly one bus (no orphans, no double-routing)
162
+ 2. All buses route to the master
163
+ 3. Bus names are clear and follow a consistent convention
164
+ 4. No tracks are accidentally muted or disabled
165
+ 5. Stem groups make musical sense (all drums together, all vocals together, etc.)
166
+ 6. Technical specs are documented (sample rate, bit depth, duration)
167
+ 7. The user knows which master bus processing will NOT be included in individual stems
168
+
169
+ ## Common Pitfalls
170
+
171
+ - **Forgetting reverb/delay returns**: Shared effects (reverb bus, delay bus) need to be assigned to a stem or exported as their own stem. If they route to master only, they'll be in the full mix but not in any stem.
172
+ - **Double-counting sends**: If a track sends to both a bus and a reverb return, the dry signal is in one stem and the wet signal might be in another. This is usually correct, but document it.
173
+ - **Master bus processing**: If the master bus has EQ/compression/limiting, individual stems will sound different than the full mix. This is normal — stems are typically pre-master-bus.
174
+ - **Mismatched levels**: All stems should sum to equal the full mix at the master bus. If they don't, there's a routing or level issue.
175
+ - **Not checking mute states**: A muted track won't export. Verify all intended tracks are unmuted and active.
package/main.js CHANGED
@@ -134,12 +134,14 @@ function getTimeoutCounter() {
134
134
 
135
135
  // apps/reaper-mcp-server/src/bridge.ts
136
136
  import { randomUUID } from "node:crypto";
137
- import { readFile, writeFile, readdir, unlink, mkdir, stat } from "node:fs/promises";
137
+ import { appendFile, readFile, writeFile, readdir, unlink, mkdir, stat } from "node:fs/promises";
138
138
  import { join as join2 } from "node:path";
139
139
  import { homedir, platform } from "node:os";
140
140
  import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
141
- var POLL_INTERVAL_MS = 50;
141
+ var POLL_INTERVAL_MS = 10;
142
142
  var DEFAULT_TIMEOUT_MS = 1e4;
143
+ var PROFILE_BRIDGE = process.env["BRIDGE_PROFILE"] === "1";
144
+ var lastTimings = null;
143
145
  function getReaperResourcePath() {
144
146
  const env = process.env["REAPER_RESOURCE_PATH"];
145
147
  if (env) return env;
@@ -164,6 +166,10 @@ async function ensureBridgeDir() {
164
166
  async function sendCommand(type, params = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
165
167
  const tracer = getTracer();
166
168
  const startMs = Date.now();
169
+ const t = {};
170
+ const profiling = PROFILE_BRIDGE;
171
+ const now = profiling ? () => performance.now() : () => 0;
172
+ const t0 = now();
167
173
  return tracer.startActiveSpan(
168
174
  `mcp.bridge ${type}`,
169
175
  {
@@ -174,7 +180,10 @@ async function sendCommand(type, params = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
174
180
  }
175
181
  },
176
182
  async (span) => {
183
+ if (profiling) t["spanSetup"] = now() - t0;
184
+ const tDir = now();
177
185
  const dir = await ensureBridgeDir();
186
+ if (profiling) t["ensureDir"] = now() - tDir;
178
187
  const id = randomUUID();
179
188
  span.setAttribute("mcp.command.id", id);
180
189
  const command2 = {
@@ -183,8 +192,13 @@ async function sendCommand(type, params = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
183
192
  params,
184
193
  timestamp: Date.now()
185
194
  };
195
+ const tWrite = now();
186
196
  const commandPath = join2(dir, `command_${id}.json`);
187
197
  await writeFile(commandPath, JSON.stringify(command2, null, 2), "utf-8");
198
+ const notifyPath = join2(dir, "_notify");
199
+ await appendFile(notifyPath, id + "\n");
200
+ if (profiling) t["write"] = now() - tWrite;
201
+ const tLog = now();
188
202
  const traceCtx = getTraceContext();
189
203
  console.error(
190
204
  JSON.stringify({
@@ -194,13 +208,22 @@ async function sendCommand(type, params = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
194
208
  ...traceCtx
195
209
  })
196
210
  );
211
+ if (profiling) t["log"] = now() - tLog;
197
212
  const responsePath = join2(dir, `response_${id}.json`);
198
213
  const deadline = Date.now() + timeoutMs;
214
+ const tPoll = now();
215
+ let pollAttempts = 0;
199
216
  while (Date.now() < deadline) {
200
217
  try {
218
+ pollAttempts++;
219
+ const tRead = now();
201
220
  const data = await readFile(responsePath, "utf-8");
202
221
  const response = JSON.parse(data);
222
+ if (profiling) t["readParse"] = now() - tRead;
223
+ if (profiling) t["pollWait"] = now() - tPoll - (t["readParse"] ?? 0);
224
+ const tCleanup = now();
203
225
  await Promise.allSettled([unlink(commandPath), unlink(responsePath)]);
226
+ if (profiling) t["cleanup"] = now() - tCleanup;
204
227
  const durationMs2 = Date.now() - startMs;
205
228
  const succeeded = response.success;
206
229
  span.setAttribute("mcp.response.success", succeeded);
@@ -219,8 +242,24 @@ async function sendCommand(type, params = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
219
242
  );
220
243
  }
221
244
  span.end();
245
+ const tMetrics = now();
222
246
  getCommandDurationHistogram().record(durationMs2, { command_type: type });
223
247
  getCommandCounter().add(1, { command_type: type, success: String(succeeded) });
248
+ if (profiling) t["metrics"] = now() - tMetrics;
249
+ if (profiling) {
250
+ lastTimings = {
251
+ spanSetupMs: Math.round(t["spanSetup"] ?? 0),
252
+ ensureDirMs: Math.round(t["ensureDir"] ?? 0),
253
+ writeMs: Math.round(t["write"] ?? 0),
254
+ logMs: Math.round(t["log"] ?? 0),
255
+ pollWaitMs: Math.round(t["pollWait"] ?? 0),
256
+ pollAttempts,
257
+ readParseMs: Math.round(t["readParse"] ?? 0),
258
+ cleanupMs: Math.round(t["cleanup"] ?? 0),
259
+ metricsMs: Math.round(t["metrics"] ?? 0),
260
+ totalMs: Math.round(now() - t0)
261
+ };
262
+ }
224
263
  return response;
225
264
  } catch {
226
265
  await sleep(POLL_INTERVAL_MS);
@@ -1456,6 +1495,109 @@ function registerEnvelopeTools(server) {
1456
1495
  return { content: [{ type: "text", text: `Deleted envelope point ${pointIndex}` }] };
1457
1496
  }
1458
1497
  );
1498
+ server.tool(
1499
+ "create_track_envelope",
1500
+ "Create/show an automation envelope on a track. Use envelopeName for built-in envelopes (Volume, Pan, Mute, Width, Trim Volume) or fxIndex+paramIndex for FX parameter envelopes. The envelope is made visible and active.",
1501
+ {
1502
+ trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
1503
+ envelopeName: z14.string().optional().describe('Built-in envelope name: "Volume", "Pan", "Mute", "Width", "Trim Volume"'),
1504
+ fxIndex: z14.coerce.number().int().min(0).optional().describe("FX chain index (for FX parameter envelopes)"),
1505
+ paramIndex: z14.coerce.number().int().min(0).optional().describe("FX parameter index (required if fxIndex provided)")
1506
+ },
1507
+ async ({ trackIndex, envelopeName, fxIndex, paramIndex }) => {
1508
+ const res = await sendCommand("create_track_envelope", {
1509
+ trackIndex,
1510
+ envelopeName,
1511
+ fxIndex,
1512
+ paramIndex
1513
+ });
1514
+ if (!res.success) {
1515
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
1516
+ }
1517
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
1518
+ }
1519
+ );
1520
+ server.tool(
1521
+ "set_envelope_properties",
1522
+ "Set properties (active, visible, armed) on a track envelope. Requires SWS extension for full support.",
1523
+ {
1524
+ trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
1525
+ envelopeIndex: z14.coerce.number().int().min(0).describe("Zero-based envelope index on the track"),
1526
+ active: z14.boolean().optional().describe("Set envelope active/inactive"),
1527
+ visible: z14.boolean().optional().describe("Set envelope visible/hidden in arrange view"),
1528
+ armed: z14.boolean().optional().describe("Set envelope armed for writing automation")
1529
+ },
1530
+ async ({ trackIndex, envelopeIndex, active, visible, armed }) => {
1531
+ const res = await sendCommand("set_envelope_properties", {
1532
+ trackIndex,
1533
+ envelopeIndex,
1534
+ active,
1535
+ visible,
1536
+ armed
1537
+ });
1538
+ if (!res.success) {
1539
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
1540
+ }
1541
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
1542
+ }
1543
+ );
1544
+ server.tool(
1545
+ "clear_envelope",
1546
+ "Delete ALL automation points from an envelope, resetting it to its default state",
1547
+ {
1548
+ trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
1549
+ envelopeIndex: z14.coerce.number().int().min(0).describe("Zero-based envelope index on the track")
1550
+ },
1551
+ async ({ trackIndex, envelopeIndex }) => {
1552
+ const res = await sendCommand("clear_envelope", { trackIndex, envelopeIndex });
1553
+ if (!res.success) {
1554
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
1555
+ }
1556
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
1557
+ }
1558
+ );
1559
+ server.tool(
1560
+ "remove_envelope_points",
1561
+ "Delete automation points in a time range from a track envelope. Use to surgically remove a section of automation.",
1562
+ {
1563
+ trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
1564
+ envelopeIndex: z14.coerce.number().int().min(0).describe("Zero-based envelope index on the track"),
1565
+ timeStart: z14.coerce.number().describe("Start of time range in seconds (inclusive)"),
1566
+ timeEnd: z14.coerce.number().describe("End of time range in seconds (exclusive)")
1567
+ },
1568
+ async ({ trackIndex, envelopeIndex, timeStart, timeEnd }) => {
1569
+ const res = await sendCommand("remove_envelope_points", {
1570
+ trackIndex,
1571
+ envelopeIndex,
1572
+ timeStart,
1573
+ timeEnd
1574
+ });
1575
+ if (!res.success) {
1576
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
1577
+ }
1578
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
1579
+ }
1580
+ );
1581
+ server.tool(
1582
+ "insert_envelope_points",
1583
+ "Batch insert multiple automation points on a track envelope. Much faster than repeated insert_envelope_point calls.",
1584
+ {
1585
+ trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
1586
+ envelopeIndex: z14.coerce.number().int().min(0).describe("Zero-based envelope index on the track"),
1587
+ points: z14.string().describe("JSON array of point objects: [{time, value, shape?, tension?}, ...]")
1588
+ },
1589
+ async ({ trackIndex, envelopeIndex, points }) => {
1590
+ const res = await sendCommand("insert_envelope_points", {
1591
+ trackIndex,
1592
+ envelopeIndex,
1593
+ points
1594
+ });
1595
+ if (!res.success) {
1596
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
1597
+ }
1598
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
1599
+ }
1600
+ );
1459
1601
  }
1460
1602
 
1461
1603
  // apps/reaper-mcp-server/src/server.ts
@@ -1669,7 +1811,12 @@ var MCP_TOOL_NAMES = [
1669
1811
  "get_track_envelopes",
1670
1812
  "get_envelope_points",
1671
1813
  "insert_envelope_point",
1672
- "delete_envelope_point"
1814
+ "insert_envelope_points",
1815
+ "delete_envelope_point",
1816
+ "create_track_envelope",
1817
+ "set_envelope_properties",
1818
+ "clear_envelope",
1819
+ "remove_envelope_points"
1673
1820
  ];
1674
1821
  function ensureClaudeSettings(settingsPath) {
1675
1822
  const allowList = MCP_TOOL_NAMES.map((t) => `mcp__reaper__${t}`);
@@ -1689,6 +1836,11 @@ function ensureClaudeSettings(settingsPath) {
1689
1836
  writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
1690
1837
  return "updated";
1691
1838
  }
1839
+ function resolveAssetDirWithFallback(baseDir, buildName, sourceName) {
1840
+ const resolved = resolveAssetDir(baseDir, buildName);
1841
+ if (existsSync(resolved)) return resolved;
1842
+ return resolveAssetDir(baseDir, sourceName);
1843
+ }
1692
1844
 
1693
1845
  // apps/reaper-mcp-server/src/main.ts
1694
1846
  var __dirname = dirname2(fileURLToPath2(import.meta.url));
@@ -1708,7 +1860,7 @@ async function setup() {
1708
1860
  } else {
1709
1861
  console.log(` Not found: ${luaSrc}`);
1710
1862
  }
1711
- const effectsDir = getReaperEffectsPath();
1863
+ const effectsDir = join4(getReaperEffectsPath(), "reaper-mcp");
1712
1864
  mkdirSync2(effectsDir, { recursive: true });
1713
1865
  console.log("\nInstalling JSFX analyzers...");
1714
1866
  for (const jsfx of REAPER_ASSETS) {
@@ -1716,7 +1868,7 @@ async function setup() {
1716
1868
  const src = join4(reaperDir, jsfx);
1717
1869
  const dest = join4(effectsDir, jsfx);
1718
1870
  if (installFile(src, dest)) {
1719
- console.log(` Installed: ${jsfx}`);
1871
+ console.log(` Installed: reaper-mcp/${jsfx}`);
1720
1872
  } else {
1721
1873
  console.log(` Not found: ${src}`);
1722
1874
  }
@@ -1729,73 +1881,69 @@ async function setup() {
1729
1881
  console.log(" 4. Run the script (it will keep running in background via defer loop)");
1730
1882
  console.log(" 5. Add reaper-mcp to your Claude Code config (see: npx @mthines/reaper-mcp doctor)");
1731
1883
  }
1732
- async function installSkills() {
1733
- console.log("REAPER MCP \u2014 Install AI Mix Engineer Skills\n");
1734
- const targetDir = process.cwd();
1735
- const globalClaudeDir = join4(homedir2(), ".claude");
1884
+ function parseInstallScope(args) {
1885
+ if (args.includes("--project")) return "project";
1886
+ return "global";
1887
+ }
1888
+ async function installSkills(scope) {
1889
+ console.log(`REAPER MCP \u2014 Install AI Mix Engineer Skills (scope: ${scope})
1890
+ `);
1891
+ const isGlobal = scope === "global";
1892
+ const baseDir = isGlobal ? join4(homedir2(), ".claude") : process.cwd();
1893
+ const claudeDir = isGlobal ? baseDir : join4(baseDir, ".claude");
1736
1894
  const knowledgeSrc = resolveAssetDir(__dirname, "knowledge");
1737
- const knowledgeDest = join4(targetDir, "knowledge");
1738
1895
  if (existsSync2(knowledgeSrc)) {
1739
- const count = copyDirSync(knowledgeSrc, knowledgeDest);
1740
- console.log(`Installed knowledge base: ${count} files \u2192 ${knowledgeDest}`);
1896
+ const dest = join4(baseDir, "knowledge");
1897
+ const count = copyDirSync(knowledgeSrc, dest);
1898
+ console.log(`Installed knowledge base: ${count} files \u2192 ${dest}`);
1741
1899
  } else {
1742
1900
  console.log("Knowledge base not found in package. Skipping.");
1743
1901
  }
1744
- const rulesSrc = resolveAssetDir(__dirname, "claude-rules");
1745
- const rulesDir = join4(targetDir, ".claude", "rules");
1902
+ const rulesSrc = resolveAssetDirWithFallback(__dirname, "claude-rules", join4(".claude", "rules"));
1746
1903
  if (existsSync2(rulesSrc)) {
1747
- const count = copyDirSync(rulesSrc, rulesDir);
1748
- console.log(`Installed Claude rules: ${count} files \u2192 ${rulesDir}`);
1904
+ const dest = join4(claudeDir, "rules");
1905
+ const count = copyDirSync(rulesSrc, dest);
1906
+ console.log(`Installed Claude rules: ${count} files \u2192 ${dest}`);
1749
1907
  } else {
1750
1908
  console.log("Claude rules not found in package. Skipping.");
1751
1909
  }
1752
- const skillsSrc = resolveAssetDir(__dirname, "claude-skills");
1753
- const skillsDir = join4(targetDir, ".claude", "skills");
1910
+ const skillsSrc = resolveAssetDirWithFallback(__dirname, "claude-skills", join4(".claude", "skills"));
1754
1911
  if (existsSync2(skillsSrc)) {
1755
- const count = copyDirSync(skillsSrc, skillsDir);
1756
- console.log(`Installed Claude skills: ${count} files \u2192 ${skillsDir}`);
1912
+ const dest = join4(claudeDir, "skills");
1913
+ const count = copyDirSync(skillsSrc, dest);
1914
+ console.log(`Installed Claude skills: ${count} files \u2192 ${dest}`);
1757
1915
  } else {
1758
1916
  console.log("Claude skills not found in package. Skipping.");
1759
1917
  }
1760
- const agentsSrc = resolveAssetDir(__dirname, "claude-agents");
1761
- const agentsDir = join4(targetDir, ".claude", "agents");
1918
+ const agentsSrc = resolveAssetDirWithFallback(__dirname, "claude-agents", join4(".claude", "agents"));
1762
1919
  if (existsSync2(agentsSrc)) {
1763
- const count = copyDirSync(agentsSrc, agentsDir);
1764
- console.log(`Installed Claude agents: ${count} files \u2192 ${agentsDir}`);
1920
+ const dest = join4(claudeDir, "agents");
1921
+ const count = copyDirSync(agentsSrc, dest);
1922
+ console.log(`Installed Claude agents: ${count} files \u2192 ${dest}`);
1765
1923
  } else {
1766
1924
  console.log("Claude agents not found in package. Skipping.");
1767
1925
  }
1768
- const globalAgentsDir = join4(globalClaudeDir, "agents");
1769
- if (existsSync2(agentsSrc)) {
1770
- const count = copyDirSync(agentsSrc, globalAgentsDir);
1771
- console.log(`Installed Claude agents (global): ${count} files \u2192 ${globalAgentsDir}`);
1772
- }
1773
- const localSettingsPath = join4(targetDir, ".claude", "settings.json");
1774
- const localResult = ensureClaudeSettings(localSettingsPath);
1775
- if (localResult === "created") {
1776
- console.log(`Created Claude settings: ${localSettingsPath}`);
1777
- } else if (localResult === "updated") {
1778
- console.log(`Updated Claude settings with new REAPER tools: ${localSettingsPath}`);
1926
+ const settingsPath = join4(claudeDir, "settings.json");
1927
+ const result = ensureClaudeSettings(settingsPath);
1928
+ if (result === "created") {
1929
+ console.log(`Created Claude settings: ${settingsPath}`);
1930
+ } else if (result === "updated") {
1931
+ console.log(`Updated Claude settings with new REAPER tools: ${settingsPath}`);
1779
1932
  } else {
1780
- console.log(`Claude settings already has all REAPER tools: ${localSettingsPath}`);
1781
- }
1782
- const globalSettingsPath = join4(globalClaudeDir, "settings.json");
1783
- const globalResult = ensureClaudeSettings(globalSettingsPath);
1784
- if (globalResult === "created") {
1785
- console.log(`Created Claude settings (global): ${globalSettingsPath}`);
1786
- } else if (globalResult === "updated") {
1787
- console.log(`Updated Claude settings (global) with new REAPER tools: ${globalSettingsPath}`);
1933
+ console.log(`Claude settings already has all REAPER tools: ${settingsPath}`);
1788
1934
  }
1789
- const mcpJsonPath = join4(targetDir, ".mcp.json");
1790
- if (createMcpJson(mcpJsonPath)) {
1791
- console.log(`
1935
+ if (!isGlobal) {
1936
+ const mcpJsonPath = join4(baseDir, ".mcp.json");
1937
+ if (createMcpJson(mcpJsonPath)) {
1938
+ console.log(`
1792
1939
  Created: ${mcpJsonPath}`);
1793
- } else {
1794
- console.log(`
1940
+ } else {
1941
+ console.log(`
1795
1942
  .mcp.json already exists \u2014 add the reaper server config manually if needed.`);
1943
+ }
1796
1944
  }
1797
1945
  console.log("\nDone! Claude Code now has mix engineer agents, knowledge, and REAPER MCP tools.");
1798
- console.log("All 48 REAPER tools are pre-approved \u2014 agents work autonomously.");
1946
+ console.log(`All ${MCP_TOOL_NAMES.length} REAPER tools are pre-approved \u2014 agents work autonomously.`);
1799
1947
  console.log('\nTry: @mix-engineer "Please gain stage my tracks"');
1800
1948
  console.log('Or: @mix-analyzer "Roast my mix"');
1801
1949
  }
@@ -1806,20 +1954,27 @@ async function doctor() {
1806
1954
  if (!bridgeRunning) {
1807
1955
  console.log(' \u2192 Run "npx @mthines/reaper-mcp setup" then load mcp_bridge.lua in REAPER');
1808
1956
  }
1809
- const agentsExist = existsSync2(join4(process.cwd(), ".claude", "agents"));
1810
- console.log(`Mix agents: ${agentsExist ? "\u2713 Found (.claude/agents/)" : "\u2717 Not installed"}`);
1957
+ const globalClaudeDir = join4(homedir2(), ".claude");
1958
+ const localAgents = existsSync2(join4(process.cwd(), ".claude", "agents"));
1959
+ const globalAgents = existsSync2(join4(globalClaudeDir, "agents"));
1960
+ const agentsExist = localAgents || globalAgents;
1961
+ const agentsLocation = localAgents ? ".claude/agents/" : globalAgents ? "~/.claude/agents/" : "";
1962
+ console.log(`Mix agents: ${agentsExist ? `\u2713 Found (${agentsLocation})` : "\u2717 Not installed"}`);
1811
1963
  if (!agentsExist) {
1812
- console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills" in your project directory');
1964
+ console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"');
1813
1965
  }
1814
- const knowledgeExists = existsSync2(join4(process.cwd(), "knowledge"));
1815
- console.log(`Knowledge base: ${knowledgeExists ? "\u2713 Found in project" : "\u2717 Not installed"}`);
1966
+ const localKnowledge = existsSync2(join4(process.cwd(), "knowledge"));
1967
+ const globalKnowledge = existsSync2(join4(globalClaudeDir, "knowledge"));
1968
+ const knowledgeExists = localKnowledge || globalKnowledge;
1969
+ const knowledgeLocation = localKnowledge ? "project" : globalKnowledge ? "~/.claude/" : "";
1970
+ console.log(`Knowledge base: ${knowledgeExists ? `\u2713 Found (${knowledgeLocation})` : "\u2717 Not installed"}`);
1816
1971
  if (!knowledgeExists) {
1817
- console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills" in your project directory');
1972
+ console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"');
1818
1973
  }
1819
1974
  const mcpJsonExists = existsSync2(join4(process.cwd(), ".mcp.json"));
1820
1975
  console.log(`MCP config: ${mcpJsonExists ? "\u2713 .mcp.json found" : "\u2717 .mcp.json missing"}`);
1821
1976
  if (!mcpJsonExists) {
1822
- console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills" to create .mcp.json');
1977
+ console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills --project" to create .mcp.json');
1823
1978
  }
1824
1979
  console.log('\nTo check SWS Extensions, start REAPER and use the "list_available_fx" tool.');
1825
1980
  console.log("SWS provides enhanced plugin discovery and snapshot support.\n");
@@ -1884,7 +2039,7 @@ switch (command) {
1884
2039
  });
1885
2040
  break;
1886
2041
  case "install-skills":
1887
- installSkills().catch((err) => {
2042
+ installSkills(parseInstallScope(process.argv.slice(3))).catch((err) => {
1888
2043
  console.error("Install failed:", err);
1889
2044
  process.exit(1);
1890
2045
  });
@@ -1917,14 +2072,16 @@ Usage:
1917
2072
  npx @mthines/reaper-mcp Start MCP server (stdio mode)
1918
2073
  npx @mthines/reaper-mcp serve Start MCP server (stdio mode)
1919
2074
  npx @mthines/reaper-mcp setup Install Lua bridge + JSFX analyzers into REAPER
1920
- npx @mthines/reaper-mcp install-skills Install AI mix engineer knowledge + agents into your project
2075
+ npx @mthines/reaper-mcp install-skills Install AI knowledge + agents globally (default)
2076
+ npx @mthines/reaper-mcp install-skills --project Install into current project directory
2077
+ npx @mthines/reaper-mcp install-skills --global Install into ~/.claude/ (default)
1921
2078
  npx @mthines/reaper-mcp doctor Check that everything is configured correctly
1922
2079
  npx @mthines/reaper-mcp status Check if Lua bridge is running in REAPER
1923
2080
 
1924
2081
  Quick Start:
1925
2082
  1. npx @mthines/reaper-mcp setup # install REAPER components
1926
2083
  2. Load mcp_bridge.lua in REAPER (Actions > Load ReaScript > Run)
1927
- 3. npx @mthines/reaper-mcp install-skills # install AI knowledge + agents
2084
+ 3. npx @mthines/reaper-mcp install-skills # install AI knowledge + agents (globally)
1928
2085
  4. Open Claude Code \u2014 REAPER tools + mix engineer agents are ready
1929
2086
 
1930
2087
  Tip: install globally for shorter commands:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mthines/reaper-mcp",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
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",
@@ -0,0 +1,60 @@
1
+ # REAPER Scripts
2
+
3
+ Files installed INTO the REAPER DAW by the `setup` command. These run inside REAPER's scripting environment.
4
+
5
+ ## Files
6
+
7
+ | File | Language | Purpose |
8
+ |------|----------|---------|
9
+ | `mcp_bridge.lua` | Lua | Persistent bridge: polls for JSON commands, executes ReaScript API, writes responses |
10
+ | `mcp_analyzer.jsfx` | JSFX/EEL2 | Real-time FFT spectrum analyzer, writes to gmem[] |
11
+ | `mcp_lufs_meter.jsfx` | JSFX/EEL2 | LUFS loudness metering |
12
+ | `mcp_correlation_meter.jsfx` | JSFX/EEL2 | Stereo correlation and width analysis |
13
+ | `mcp_crest_factor.jsfx` | JSFX/EEL2 | Crest factor (peak-to-RMS) measurement |
14
+ | `install.sh` | Shell | Manual install helper |
15
+
16
+ ## Lua Bridge (`mcp_bridge.lua`)
17
+
18
+ ### How It Works
19
+ 1. Runs as a persistent `reaper.defer()` loop (polls every ~30ms)
20
+ 2. Reads `command_{uuid}.json` from bridge directory
21
+ 3. Dispatches to handler function in the `handlers` table
22
+ 4. Writes `response_{uuid}.json` with results
23
+ 5. Writes `heartbeat.json` every 1s for liveness detection
24
+
25
+ ### Adding a Handler
26
+ ```lua
27
+ handlers["command_type"] = function(params)
28
+ local result = reaper.SomeApiCall(params.paramName)
29
+ return { field = result }
30
+ end
31
+ ```
32
+
33
+ The command type string must exactly match `CommandType` in `libs/protocol/src/commands.ts`.
34
+
35
+ ### Key Constraints
36
+ - REAPER Lua is sandboxed: **no sockets, no HTTP, no stdin/stdout** — file-based IPC only
37
+ - JSON parsing: uses `CF_Json_Parse` if available (REAPER 7+), falls back to custom Lua parser
38
+ - Track indices are 0-based (same as ReaScript)
39
+ - Volume values: bridge converts between dB (MCP protocol) and linear (ReaScript internally)
40
+ - Always wrap file reads in `pcall` for resilience
41
+
42
+ ## JSFX Meters
43
+
44
+ - Run in REAPER's **audio thread** (not scripting thread)
45
+ - Communicate with Lua via `gmem[]` shared memory
46
+ - Each JSFX uses a unique gmem namespace (e.g., `MCPAnalyzer`, `MCPLufsMeter`)
47
+ - Must pass audio through unmodified (transparent inserts)
48
+ - Auto-inserted by corresponding MCP tools (`read_track_spectrum`, `read_track_lufs`, etc.)
49
+
50
+ ## Testing
51
+
52
+ - **No automated tests possible** — REAPER's Lua/JSFX environment cannot be unit tested outside REAPER
53
+ - Test manually: install bridge, run MCP Inspector, exercise commands
54
+ - Server-side tests mock `sendCommand()` in `bridge.ts`
55
+
56
+ ## Installation
57
+
58
+ Files are copied to `{REAPER_RESOURCE_PATH}/Scripts/` by:
59
+ - `node dist/apps/reaper-mcp-server/main.js setup` (programmatic)
60
+ - `install.sh` (manual)