@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.
- package/README.md +20 -12
- package/claude-agents/{mix-analyzer.md → critique.md} +3 -3
- package/claude-agents/editor.md +184 -0
- package/claude-agents/{gain-stage.md → levels.md} +1 -1
- package/claude-agents/{master.md → mastering.md} +1 -1
- package/claude-agents/{mix-engineer.md → mixer.md} +1 -1
- package/claude-agents/preflight.md +149 -0
- package/claude-agents/producer.md +159 -0
- package/claude-agents/setup.md +150 -0
- package/claude-agents/stems.md +135 -0
- package/knowledge/CLAUDE.md +71 -0
- package/knowledge/workflows/delivery.md +202 -0
- package/knowledge/workflows/editing.md +251 -0
- package/knowledge/workflows/learn-plugin.md +143 -0
- package/knowledge/workflows/session-prep.md +205 -0
- package/knowledge/workflows/stem-prep.md +175 -0
- package/main.js +216 -59
- package/package.json +1 -1
- package/reaper/CLAUDE.md +60 -0
- package/reaper/install.sh +8 -4
- package/reaper/mcp_bridge.lua +331 -7
|
@@ -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 =
|
|
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
|
-
"
|
|
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:
|
|
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
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
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
|
|
1740
|
-
|
|
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 =
|
|
1745
|
-
const rulesDir = join4(targetDir, ".claude", "rules");
|
|
1902
|
+
const rulesSrc = resolveAssetDirWithFallback(__dirname, "claude-rules", join4(".claude", "rules"));
|
|
1746
1903
|
if (existsSync2(rulesSrc)) {
|
|
1747
|
-
const
|
|
1748
|
-
|
|
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 =
|
|
1753
|
-
const skillsDir = join4(targetDir, ".claude", "skills");
|
|
1910
|
+
const skillsSrc = resolveAssetDirWithFallback(__dirname, "claude-skills", join4(".claude", "skills"));
|
|
1754
1911
|
if (existsSync2(skillsSrc)) {
|
|
1755
|
-
const
|
|
1756
|
-
|
|
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 =
|
|
1761
|
-
const agentsDir = join4(targetDir, ".claude", "agents");
|
|
1918
|
+
const agentsSrc = resolveAssetDirWithFallback(__dirname, "claude-agents", join4(".claude", "agents"));
|
|
1762
1919
|
if (existsSync2(agentsSrc)) {
|
|
1763
|
-
const
|
|
1764
|
-
|
|
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
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
console.log(`
|
|
1772
|
-
}
|
|
1773
|
-
|
|
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: ${
|
|
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
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1935
|
+
if (!isGlobal) {
|
|
1936
|
+
const mcpJsonPath = join4(baseDir, ".mcp.json");
|
|
1937
|
+
if (createMcpJson(mcpJsonPath)) {
|
|
1938
|
+
console.log(`
|
|
1792
1939
|
Created: ${mcpJsonPath}`);
|
|
1793
|
-
|
|
1794
|
-
|
|
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(
|
|
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
|
|
1810
|
-
|
|
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"
|
|
1964
|
+
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"');
|
|
1813
1965
|
}
|
|
1814
|
-
const
|
|
1815
|
-
|
|
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"
|
|
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
|
|
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
package/reaper/CLAUDE.md
ADDED
|
@@ -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)
|