@mthines/reaper-mcp 0.1.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.
Files changed (42) hide show
  1. package/README.md +281 -0
  2. package/claude-rules/architecture.md +39 -0
  3. package/claude-rules/development.md +54 -0
  4. package/claude-rules/lua-bridge.md +50 -0
  5. package/claude-rules/testing.md +42 -0
  6. package/claude-skills/learn-plugin.md +123 -0
  7. package/knowledge/genres/_template.md +109 -0
  8. package/knowledge/genres/electronic.md +112 -0
  9. package/knowledge/genres/hip-hop.md +111 -0
  10. package/knowledge/genres/metal.md +136 -0
  11. package/knowledge/genres/orchestral.md +132 -0
  12. package/knowledge/genres/pop.md +108 -0
  13. package/knowledge/genres/rock.md +117 -0
  14. package/knowledge/plugins/_template.md +82 -0
  15. package/knowledge/plugins/fabfilter/pro-c-2.md +117 -0
  16. package/knowledge/plugins/fabfilter/pro-l-2.md +95 -0
  17. package/knowledge/plugins/fabfilter/pro-q-3.md +112 -0
  18. package/knowledge/plugins/neural-dsp/helix-native.md +104 -0
  19. package/knowledge/plugins/stock-reaper/js-1175-compressor.md +94 -0
  20. package/knowledge/plugins/stock-reaper/rea-comp.md +100 -0
  21. package/knowledge/plugins/stock-reaper/rea-delay.md +95 -0
  22. package/knowledge/plugins/stock-reaper/rea-eq.md +103 -0
  23. package/knowledge/plugins/stock-reaper/rea-gate.md +99 -0
  24. package/knowledge/plugins/stock-reaper/rea-limit.md +75 -0
  25. package/knowledge/plugins/stock-reaper/rea-verb.md +76 -0
  26. package/knowledge/reference/common-mistakes.md +307 -0
  27. package/knowledge/reference/compression.md +176 -0
  28. package/knowledge/reference/frequencies.md +154 -0
  29. package/knowledge/reference/metering.md +166 -0
  30. package/knowledge/workflows/drum-bus.md +211 -0
  31. package/knowledge/workflows/gain-staging.md +165 -0
  32. package/knowledge/workflows/low-end.md +261 -0
  33. package/knowledge/workflows/master-bus.md +204 -0
  34. package/knowledge/workflows/vocal-chain.md +246 -0
  35. package/main.js +755 -0
  36. package/package.json +44 -0
  37. package/reaper/install.sh +50 -0
  38. package/reaper/mcp_analyzer.jsfx +167 -0
  39. package/reaper/mcp_bridge.lua +1105 -0
  40. package/reaper/mcp_correlation_meter.jsfx +148 -0
  41. package/reaper/mcp_crest_factor.jsfx +108 -0
  42. package/reaper/mcp_lufs_meter.jsfx +301 -0
package/main.js ADDED
@@ -0,0 +1,755 @@
1
+ #!/usr/bin/env node
2
+
3
+ // apps/reaper-mcp-server/src/main.ts
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // apps/reaper-mcp-server/src/server.ts
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+
9
+ // apps/reaper-mcp-server/src/bridge.ts
10
+ import { randomUUID } from "node:crypto";
11
+ import { readFile, writeFile, readdir, unlink, mkdir, stat } from "node:fs/promises";
12
+ import { join } from "node:path";
13
+ import { homedir, platform } from "node:os";
14
+ var POLL_INTERVAL_MS = 50;
15
+ var DEFAULT_TIMEOUT_MS = 1e4;
16
+ function getReaperResourcePath() {
17
+ const env = process.env["REAPER_RESOURCE_PATH"];
18
+ if (env) return env;
19
+ const home = homedir();
20
+ switch (platform()) {
21
+ case "darwin":
22
+ return join(home, "Library", "Application Support", "REAPER");
23
+ case "win32":
24
+ return join(process.env["APPDATA"] ?? join(home, "AppData", "Roaming"), "REAPER");
25
+ default:
26
+ return join(home, ".config", "REAPER");
27
+ }
28
+ }
29
+ function getBridgeDir() {
30
+ return join(getReaperResourcePath(), "Scripts", "mcp_bridge_data");
31
+ }
32
+ async function ensureBridgeDir() {
33
+ const dir = getBridgeDir();
34
+ await mkdir(dir, { recursive: true });
35
+ return dir;
36
+ }
37
+ async function sendCommand(type, params = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
38
+ const dir = await ensureBridgeDir();
39
+ const id = randomUUID();
40
+ const command2 = {
41
+ id,
42
+ type,
43
+ params,
44
+ timestamp: Date.now()
45
+ };
46
+ const commandPath = join(dir, `command_${id}.json`);
47
+ await writeFile(commandPath, JSON.stringify(command2, null, 2), "utf-8");
48
+ const responsePath = join(dir, `response_${id}.json`);
49
+ const deadline = Date.now() + timeoutMs;
50
+ while (Date.now() < deadline) {
51
+ try {
52
+ const data = await readFile(responsePath, "utf-8");
53
+ const response = JSON.parse(data);
54
+ await Promise.allSettled([unlink(commandPath), unlink(responsePath)]);
55
+ return response;
56
+ } catch {
57
+ await sleep(POLL_INTERVAL_MS);
58
+ }
59
+ }
60
+ await unlink(commandPath).catch(() => {
61
+ });
62
+ return {
63
+ id,
64
+ success: false,
65
+ error: `Timeout: no response from REAPER Lua bridge after ${timeoutMs}ms. Is the bridge script running in REAPER?`,
66
+ timestamp: Date.now()
67
+ };
68
+ }
69
+ async function isBridgeRunning() {
70
+ const dir = getBridgeDir();
71
+ const heartbeatPath = join(dir, "heartbeat.json");
72
+ try {
73
+ const info = await stat(heartbeatPath);
74
+ return Date.now() - info.mtimeMs < 5e3;
75
+ } catch {
76
+ return false;
77
+ }
78
+ }
79
+ async function cleanupStaleFiles(maxAgeMs = 3e4) {
80
+ const dir = getBridgeDir();
81
+ let cleaned = 0;
82
+ try {
83
+ const files = await readdir(dir);
84
+ const now = Date.now();
85
+ for (const file of files) {
86
+ if (!file.startsWith("command_") && !file.startsWith("response_")) continue;
87
+ const filePath = join(dir, file);
88
+ const info = await stat(filePath);
89
+ if (now - info.mtimeMs > maxAgeMs) {
90
+ await unlink(filePath).catch(() => {
91
+ });
92
+ cleaned++;
93
+ }
94
+ }
95
+ } catch {
96
+ }
97
+ return cleaned;
98
+ }
99
+ function getReaperScriptsPath() {
100
+ return join(getReaperResourcePath(), "Scripts");
101
+ }
102
+ function getReaperEffectsPath() {
103
+ return join(getReaperResourcePath(), "Effects");
104
+ }
105
+ function sleep(ms) {
106
+ return new Promise((resolve) => setTimeout(resolve, ms));
107
+ }
108
+
109
+ // apps/reaper-mcp-server/src/tools/project.ts
110
+ function registerProjectTools(server) {
111
+ server.tool(
112
+ "get_project_info",
113
+ "Get current REAPER project info: name, track count, tempo, time signature, sample rate, transport state",
114
+ {},
115
+ async () => {
116
+ const res = await sendCommand("get_project_info");
117
+ if (!res.success) {
118
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
119
+ }
120
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
121
+ }
122
+ );
123
+ }
124
+
125
+ // apps/reaper-mcp-server/src/tools/tracks.ts
126
+ import { z } from "zod";
127
+ function registerTrackTools(server) {
128
+ server.tool(
129
+ "list_tracks",
130
+ "List all tracks in the current REAPER project with name, index, volume, mute/solo state, and folder structure",
131
+ {},
132
+ async () => {
133
+ const res = await sendCommand("list_tracks");
134
+ if (!res.success) {
135
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
136
+ }
137
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
138
+ }
139
+ );
140
+ server.tool(
141
+ "get_track_properties",
142
+ "Get detailed properties of a specific track including volume, pan, mute, solo, and FX chain",
143
+ { trackIndex: z.number().int().min(0).describe("Zero-based track index") },
144
+ async ({ trackIndex }) => {
145
+ const res = await sendCommand("get_track_properties", { trackIndex });
146
+ if (!res.success) {
147
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
148
+ }
149
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
150
+ }
151
+ );
152
+ server.tool(
153
+ "set_track_property",
154
+ "Set a track property: volume (dB), pan (-1.0 to 1.0), mute (0/1), or solo (0/1)",
155
+ {
156
+ trackIndex: z.number().int().min(0).describe("Zero-based track index"),
157
+ property: z.enum(["volume", "pan", "mute", "solo"]).describe("Property to set"),
158
+ value: z.number().describe("Value: volume in dB, pan -1.0\u20131.0, mute/solo 0 or 1")
159
+ },
160
+ async ({ trackIndex, property, value }) => {
161
+ const res = await sendCommand("set_track_property", { trackIndex, property, value });
162
+ if (!res.success) {
163
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
164
+ }
165
+ return { content: [{ type: "text", text: `Set track ${trackIndex} ${property} = ${value}` }] };
166
+ }
167
+ );
168
+ }
169
+
170
+ // apps/reaper-mcp-server/src/tools/fx.ts
171
+ import { z as z2 } from "zod";
172
+ function registerFxTools(server) {
173
+ server.tool(
174
+ "add_fx",
175
+ `Add an FX plugin to a track by name (e.g. "ReaEQ", "JS: Schwa's Spectral Analyzer", "VST: Pro-Q 3")`,
176
+ {
177
+ trackIndex: z2.number().int().min(0).describe("Zero-based track index"),
178
+ fxName: z2.string().describe("FX plugin name (partial match supported)"),
179
+ position: z2.number().int().optional().describe("Position in FX chain (-1 or omit for end)")
180
+ },
181
+ async ({ trackIndex, fxName, position }) => {
182
+ const res = await sendCommand("add_fx", { trackIndex, fxName, position: position ?? -1 });
183
+ if (!res.success) {
184
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
185
+ }
186
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
187
+ }
188
+ );
189
+ server.tool(
190
+ "remove_fx",
191
+ "Remove an FX plugin from a track by its index in the FX chain",
192
+ {
193
+ trackIndex: z2.number().int().min(0).describe("Zero-based track index"),
194
+ fxIndex: z2.number().int().min(0).describe("Zero-based FX index in the chain")
195
+ },
196
+ async ({ trackIndex, fxIndex }) => {
197
+ const res = await sendCommand("remove_fx", { trackIndex, fxIndex });
198
+ if (!res.success) {
199
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
200
+ }
201
+ return { content: [{ type: "text", text: `Removed FX ${fxIndex} from track ${trackIndex}` }] };
202
+ }
203
+ );
204
+ server.tool(
205
+ "get_fx_parameters",
206
+ "List all parameters of an FX plugin with current values and ranges",
207
+ {
208
+ trackIndex: z2.number().int().min(0).describe("Zero-based track index"),
209
+ fxIndex: z2.number().int().min(0).describe("Zero-based FX index in the chain")
210
+ },
211
+ async ({ trackIndex, fxIndex }) => {
212
+ const res = await sendCommand("get_fx_parameters", { trackIndex, fxIndex });
213
+ if (!res.success) {
214
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
215
+ }
216
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
217
+ }
218
+ );
219
+ server.tool(
220
+ "set_fx_parameter",
221
+ "Set a specific FX parameter value (normalized 0.0\u20131.0)",
222
+ {
223
+ trackIndex: z2.number().int().min(0).describe("Zero-based track index"),
224
+ fxIndex: z2.number().int().min(0).describe("Zero-based FX index in the chain"),
225
+ paramIndex: z2.number().int().min(0).describe("Zero-based parameter index"),
226
+ value: z2.number().min(0).max(1).describe("Normalized parameter value 0.0\u20131.0")
227
+ },
228
+ async ({ trackIndex, fxIndex, paramIndex, value }) => {
229
+ const res = await sendCommand("set_fx_parameter", { trackIndex, fxIndex, paramIndex, value });
230
+ if (!res.success) {
231
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
232
+ }
233
+ return { content: [{ type: "text", text: `Set FX ${fxIndex} param ${paramIndex} = ${value}` }] };
234
+ }
235
+ );
236
+ }
237
+
238
+ // apps/reaper-mcp-server/src/tools/meters.ts
239
+ import { z as z3 } from "zod";
240
+ function registerMeterTools(server) {
241
+ server.tool(
242
+ "read_track_meters",
243
+ "Read real-time peak and RMS levels (in dB) for a track. Returns L/R peak and RMS values.",
244
+ {
245
+ trackIndex: z3.number().int().min(0).describe("Zero-based track index")
246
+ },
247
+ async ({ trackIndex }) => {
248
+ const res = await sendCommand("read_track_meters", { trackIndex });
249
+ if (!res.success) {
250
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
251
+ }
252
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
253
+ }
254
+ );
255
+ server.tool(
256
+ "read_track_spectrum",
257
+ "Read real-time FFT frequency spectrum data for a track. Auto-inserts the MCP Spectrum Analyzer JSFX if not present. Returns frequency bins in dB from 0 Hz to Nyquist.",
258
+ {
259
+ trackIndex: z3.number().int().min(0).describe("Zero-based track index"),
260
+ fftSize: z3.number().int().optional().describe("FFT size (default 4096). Options: 512, 1024, 2048, 4096, 8192")
261
+ },
262
+ async ({ trackIndex, fftSize }) => {
263
+ const res = await sendCommand("read_track_spectrum", { trackIndex, fftSize: fftSize ?? 4096 });
264
+ if (!res.success) {
265
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
266
+ }
267
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
268
+ }
269
+ );
270
+ }
271
+
272
+ // apps/reaper-mcp-server/src/tools/transport.ts
273
+ import { z as z4 } from "zod";
274
+ function registerTransportTools(server) {
275
+ server.tool(
276
+ "play",
277
+ "Start playback in REAPER",
278
+ {},
279
+ async () => {
280
+ const res = await sendCommand("play");
281
+ if (!res.success) {
282
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
283
+ }
284
+ return { content: [{ type: "text", text: "Playback started" }] };
285
+ }
286
+ );
287
+ server.tool(
288
+ "stop",
289
+ "Stop playback/recording in REAPER",
290
+ {},
291
+ async () => {
292
+ const res = await sendCommand("stop");
293
+ if (!res.success) {
294
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
295
+ }
296
+ return { content: [{ type: "text", text: "Playback stopped" }] };
297
+ }
298
+ );
299
+ server.tool(
300
+ "record",
301
+ "Start recording in REAPER (arms must be set on target tracks)",
302
+ {},
303
+ async () => {
304
+ const res = await sendCommand("record");
305
+ if (!res.success) {
306
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
307
+ }
308
+ return { content: [{ type: "text", text: "Recording started" }] };
309
+ }
310
+ );
311
+ server.tool(
312
+ "get_transport_state",
313
+ "Get current transport state: play/record/pause status, cursor positions, tempo, time signature",
314
+ {},
315
+ async () => {
316
+ const res = await sendCommand("get_transport_state");
317
+ if (!res.success) {
318
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
319
+ }
320
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
321
+ }
322
+ );
323
+ server.tool(
324
+ "set_cursor_position",
325
+ "Move the edit cursor to a specific position in seconds from project start",
326
+ {
327
+ position: z4.number().min(0).describe("Position in seconds from project start")
328
+ },
329
+ async ({ position }) => {
330
+ const res = await sendCommand("set_cursor_position", { position });
331
+ if (!res.success) {
332
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
333
+ }
334
+ return { content: [{ type: "text", text: `Cursor moved to ${position}s` }] };
335
+ }
336
+ );
337
+ }
338
+
339
+ // apps/reaper-mcp-server/src/tools/discovery.ts
340
+ import { z as z5 } from "zod";
341
+ function registerDiscoveryTools(server) {
342
+ server.tool(
343
+ "list_available_fx",
344
+ "Enumerate all installed FX plugins in REAPER (VST, VST3, JS, CLAP, AU) with an optional category filter",
345
+ {
346
+ category: z5.string().optional().describe('Optional filter: e.g. "VST", "VST3", "JS", "AU", "CLAP"')
347
+ },
348
+ async ({ category }) => {
349
+ const res = await sendCommand("list_available_fx", { category });
350
+ if (!res.success) {
351
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
352
+ }
353
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
354
+ }
355
+ );
356
+ server.tool(
357
+ "search_fx",
358
+ "Search installed FX plugins by name (case-insensitive substring match)",
359
+ {
360
+ query: z5.string().min(1).describe("Search term to match against FX plugin names")
361
+ },
362
+ async ({ query }) => {
363
+ const res = await sendCommand("search_fx", { query });
364
+ if (!res.success) {
365
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
366
+ }
367
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
368
+ }
369
+ );
370
+ }
371
+
372
+ // apps/reaper-mcp-server/src/tools/presets.ts
373
+ import { z as z6 } from "zod";
374
+ function registerPresetTools(server) {
375
+ server.tool(
376
+ "get_fx_preset_list",
377
+ "List all available presets for a specific FX plugin on a track",
378
+ {
379
+ trackIndex: z6.number().int().min(0).describe("Zero-based track index"),
380
+ fxIndex: z6.number().int().min(0).describe("Zero-based FX index in the chain")
381
+ },
382
+ async ({ trackIndex, fxIndex }) => {
383
+ const res = await sendCommand("get_fx_preset_list", { trackIndex, fxIndex });
384
+ if (!res.success) {
385
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
386
+ }
387
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
388
+ }
389
+ );
390
+ server.tool(
391
+ "set_fx_preset",
392
+ "Apply a named preset to an FX plugin on a track",
393
+ {
394
+ trackIndex: z6.number().int().min(0).describe("Zero-based track index"),
395
+ fxIndex: z6.number().int().min(0).describe("Zero-based FX index in the chain"),
396
+ presetName: z6.string().min(1).describe("Exact preset name to apply")
397
+ },
398
+ async ({ trackIndex, fxIndex, presetName }) => {
399
+ const res = await sendCommand("set_fx_preset", { trackIndex, fxIndex, presetName });
400
+ if (!res.success) {
401
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
402
+ }
403
+ return { content: [{ type: "text", text: `Applied preset "${presetName}" to FX ${fxIndex} on track ${trackIndex}` }] };
404
+ }
405
+ );
406
+ }
407
+
408
+ // apps/reaper-mcp-server/src/tools/snapshots.ts
409
+ import { z as z7 } from "zod";
410
+ function registerSnapshotTools(server) {
411
+ server.tool(
412
+ "snapshot_save",
413
+ "Save the current mixer state as a named snapshot. Uses SWS Snapshots if available, otherwise captures track volumes, pans, mutes, solos, and FX bypass states manually.",
414
+ {
415
+ name: z7.string().min(1).describe('Unique snapshot name (e.g. "before-compression", "v1-mix")'),
416
+ description: z7.string().optional().describe("Optional human-readable description")
417
+ },
418
+ async ({ name, description }) => {
419
+ const res = await sendCommand("snapshot_save", { name, description });
420
+ if (!res.success) {
421
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
422
+ }
423
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
424
+ }
425
+ );
426
+ server.tool(
427
+ "snapshot_restore",
428
+ "Restore a previously saved mixer snapshot by name",
429
+ {
430
+ name: z7.string().min(1).describe("Name of the snapshot to restore")
431
+ },
432
+ async ({ name }) => {
433
+ const res = await sendCommand("snapshot_restore", { name });
434
+ if (!res.success) {
435
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
436
+ }
437
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
438
+ }
439
+ );
440
+ server.tool(
441
+ "snapshot_list",
442
+ "List all saved mixer snapshots with names, descriptions, and timestamps",
443
+ {},
444
+ async () => {
445
+ const res = await sendCommand("snapshot_list", {});
446
+ if (!res.success) {
447
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
448
+ }
449
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
450
+ }
451
+ );
452
+ }
453
+
454
+ // apps/reaper-mcp-server/src/tools/routing.ts
455
+ import { z as z8 } from "zod";
456
+ function registerRoutingTools(server) {
457
+ server.tool(
458
+ "get_track_routing",
459
+ "Get sends, receives, and parent/folder information for a track \u2014 useful for understanding bus structure",
460
+ {
461
+ trackIndex: z8.number().int().min(0).describe("Zero-based track index")
462
+ },
463
+ async ({ trackIndex }) => {
464
+ const res = await sendCommand("get_track_routing", { trackIndex });
465
+ if (!res.success) {
466
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
467
+ }
468
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
469
+ }
470
+ );
471
+ }
472
+
473
+ // apps/reaper-mcp-server/src/tools/analysis.ts
474
+ import { z as z9 } from "zod";
475
+ function registerAnalysisTools(server) {
476
+ server.tool(
477
+ "read_track_lufs",
478
+ "Read ITU-R BS.1770 loudness data for a track. Auto-inserts the MCP LUFS Meter JSFX if not present. Returns integrated, short-term (3s), and momentary (400ms) LUFS plus true inter-sample peak levels. Audio must be playing to accumulate data.",
479
+ {
480
+ trackIndex: z9.number().int().min(0).describe("Zero-based track index")
481
+ },
482
+ async ({ trackIndex }) => {
483
+ const res = await sendCommand("read_track_lufs", { trackIndex });
484
+ if (!res.success) {
485
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
486
+ }
487
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
488
+ }
489
+ );
490
+ server.tool(
491
+ "read_track_correlation",
492
+ "Read stereo field correlation and M/S analysis for a track. Auto-inserts the MCP Correlation Meter JSFX if not present. Returns correlation coefficient (-1 to +1), stereo width, and mid/side levels. Audio must be playing to accumulate data.",
493
+ {
494
+ trackIndex: z9.number().int().min(0).describe("Zero-based track index")
495
+ },
496
+ async ({ trackIndex }) => {
497
+ const res = await sendCommand("read_track_correlation", { trackIndex });
498
+ if (!res.success) {
499
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
500
+ }
501
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
502
+ }
503
+ );
504
+ server.tool(
505
+ "read_track_crest",
506
+ "Read crest factor (peak-to-RMS ratio) for a track. Auto-inserts the MCP Crest Factor Meter JSFX if not present. Returns crest factor in dB (higher = more dynamic, lower = over-compressed), peak hold level, and RMS level. Audio must be playing to accumulate data.",
507
+ {
508
+ trackIndex: z9.number().int().min(0).describe("Zero-based track index")
509
+ },
510
+ async ({ trackIndex }) => {
511
+ const res = await sendCommand("read_track_crest", { trackIndex });
512
+ if (!res.success) {
513
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
514
+ }
515
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
516
+ }
517
+ );
518
+ }
519
+
520
+ // apps/reaper-mcp-server/src/server.ts
521
+ function createServer() {
522
+ const server = new McpServer({
523
+ name: "reaper-mcp",
524
+ version: "0.1.0"
525
+ });
526
+ registerProjectTools(server);
527
+ registerTrackTools(server);
528
+ registerFxTools(server);
529
+ registerMeterTools(server);
530
+ registerTransportTools(server);
531
+ registerDiscoveryTools(server);
532
+ registerPresetTools(server);
533
+ registerSnapshotTools(server);
534
+ registerRoutingTools(server);
535
+ registerAnalysisTools(server);
536
+ return server;
537
+ }
538
+
539
+ // apps/reaper-mcp-server/src/main.ts
540
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
541
+ import { join as join2, dirname } from "node:path";
542
+ import { fileURLToPath } from "node:url";
543
+ var __dirname = dirname(fileURLToPath(import.meta.url));
544
+ function copyDirSync(src, dest) {
545
+ if (!existsSync(src)) return 0;
546
+ mkdirSync(dest, { recursive: true });
547
+ let count = 0;
548
+ for (const entry of readdirSync(src)) {
549
+ const srcPath = join2(src, entry);
550
+ const destPath = join2(dest, entry);
551
+ if (statSync(srcPath).isDirectory()) {
552
+ count += copyDirSync(srcPath, destPath);
553
+ } else {
554
+ copyFileSync(srcPath, destPath);
555
+ count++;
556
+ }
557
+ }
558
+ return count;
559
+ }
560
+ function installFile(src, dest, label) {
561
+ if (existsSync(src)) {
562
+ copyFileSync(src, dest);
563
+ console.log(` Installed: ${label}`);
564
+ return true;
565
+ }
566
+ console.log(` Not found: ${src}`);
567
+ return false;
568
+ }
569
+ async function setup() {
570
+ console.log("REAPER MCP Server \u2014 Setup\n");
571
+ const bridgeDir = await ensureBridgeDir();
572
+ console.log(`Bridge directory: ${bridgeDir}
573
+ `);
574
+ const scriptsDir = getReaperScriptsPath();
575
+ mkdirSync(scriptsDir, { recursive: true });
576
+ const luaSrc = join2(__dirname, "..", "reaper", "mcp_bridge.lua");
577
+ const luaDest = join2(scriptsDir, "mcp_bridge.lua");
578
+ console.log("Installing Lua bridge...");
579
+ installFile(luaSrc, luaDest, "mcp_bridge.lua");
580
+ const effectsDir = getReaperEffectsPath();
581
+ mkdirSync(effectsDir, { recursive: true });
582
+ const jsfxFiles = [
583
+ "mcp_analyzer.jsfx",
584
+ "mcp_lufs_meter.jsfx",
585
+ "mcp_correlation_meter.jsfx",
586
+ "mcp_crest_factor.jsfx"
587
+ ];
588
+ console.log("\nInstalling JSFX analyzers...");
589
+ for (const jsfx of jsfxFiles) {
590
+ const src = join2(__dirname, "..", "reaper", jsfx);
591
+ const dest = join2(effectsDir, jsfx);
592
+ installFile(src, dest, jsfx);
593
+ }
594
+ console.log("\nSetup complete!\n");
595
+ console.log("Next steps:");
596
+ console.log(" 1. Open REAPER");
597
+ console.log(" 2. Actions > Show action list > Load ReaScript");
598
+ console.log(` 3. Select: ${luaDest}`);
599
+ console.log(" 4. Run the script (it will keep running in background via defer loop)");
600
+ console.log(" 5. Add reaper-mcp to your Claude Code config (see: reaper-mcp doctor)");
601
+ }
602
+ async function installSkills() {
603
+ console.log("REAPER MCP \u2014 Install AI Mix Engineer Skills\n");
604
+ const targetDir = process.cwd();
605
+ const knowledgeSrc = join2(__dirname, "..", "knowledge");
606
+ const knowledgeDest = join2(targetDir, "knowledge");
607
+ if (existsSync(knowledgeSrc)) {
608
+ const count = copyDirSync(knowledgeSrc, knowledgeDest);
609
+ console.log(`Installed knowledge base: ${count} files \u2192 ${knowledgeDest}`);
610
+ } else {
611
+ console.log("Knowledge base not found in package. Skipping.");
612
+ }
613
+ const rulesSrc = join2(__dirname, "..", "claude-rules");
614
+ const rulesDir = join2(targetDir, ".claude", "rules");
615
+ if (existsSync(rulesSrc)) {
616
+ const count = copyDirSync(rulesSrc, rulesDir);
617
+ console.log(`Installed Claude rules: ${count} files \u2192 ${rulesDir}`);
618
+ } else {
619
+ console.log("Claude rules not found in package. Skipping.");
620
+ }
621
+ const skillsSrc = join2(__dirname, "..", "claude-skills");
622
+ const skillsDir = join2(targetDir, ".claude", "skills");
623
+ if (existsSync(skillsSrc)) {
624
+ const count = copyDirSync(skillsSrc, skillsDir);
625
+ console.log(`Installed Claude skills: ${count} files \u2192 ${skillsDir}`);
626
+ } else {
627
+ console.log("Claude skills not found in package. Skipping.");
628
+ }
629
+ const mcpJsonPath = join2(targetDir, ".mcp.json");
630
+ if (!existsSync(mcpJsonPath)) {
631
+ const mcpConfig = JSON.stringify({
632
+ mcpServers: {
633
+ reaper: {
634
+ command: "npx",
635
+ args: ["@mthines/reaper-mcp", "serve"]
636
+ }
637
+ }
638
+ }, null, 2);
639
+ copyFileSync("/dev/null", mcpJsonPath);
640
+ const { writeFileSync } = await import("node:fs");
641
+ writeFileSync(mcpJsonPath, mcpConfig + "\n", "utf-8");
642
+ console.log(`
643
+ Created: ${mcpJsonPath}`);
644
+ } else {
645
+ console.log(`
646
+ .mcp.json already exists \u2014 add the reaper server config manually if needed.`);
647
+ }
648
+ console.log("\nDone! Claude Code now has mix engineer knowledge and REAPER MCP tools.");
649
+ console.log('Try asking: "Please gain stage my tracks" or "Roast my mix"');
650
+ }
651
+ async function doctor() {
652
+ console.log("REAPER MCP \u2014 System Check\n");
653
+ const bridgeRunning = await isBridgeRunning();
654
+ console.log(`Lua bridge: ${bridgeRunning ? "\u2713 Connected" : "\u2717 Not detected"}`);
655
+ if (!bridgeRunning) {
656
+ console.log(' \u2192 Run "reaper-mcp setup" then load mcp_bridge.lua in REAPER');
657
+ }
658
+ const knowledgeExists = existsSync(join2(process.cwd(), "knowledge"));
659
+ console.log(`Knowledge base: ${knowledgeExists ? "\u2713 Found in project" : "\u2717 Not installed"}`);
660
+ if (!knowledgeExists) {
661
+ console.log(' \u2192 Run "reaper-mcp install-skills" in your project directory');
662
+ }
663
+ const mcpJsonExists = existsSync(join2(process.cwd(), ".mcp.json"));
664
+ console.log(`MCP config: ${mcpJsonExists ? "\u2713 .mcp.json found" : "\u2717 .mcp.json missing"}`);
665
+ if (!mcpJsonExists) {
666
+ console.log(' \u2192 Run "reaper-mcp install-skills" to create .mcp.json');
667
+ }
668
+ console.log('\nTo check SWS Extensions, start REAPER and use the "list_available_fx" tool.');
669
+ console.log("SWS provides enhanced plugin discovery and snapshot support.\n");
670
+ process.exit(bridgeRunning && knowledgeExists && mcpJsonExists ? 0 : 1);
671
+ }
672
+ async function serve() {
673
+ const log = (...args) => console.error("[reaper-mcp]", ...args);
674
+ log("Starting REAPER MCP Server...");
675
+ await ensureBridgeDir();
676
+ const cleaned = await cleanupStaleFiles();
677
+ if (cleaned > 0) {
678
+ log(`Cleaned up ${cleaned} stale bridge files`);
679
+ }
680
+ const bridgeRunning = await isBridgeRunning();
681
+ if (!bridgeRunning) {
682
+ log("WARNING: Lua bridge does not appear to be running in REAPER.");
683
+ log("Commands will timeout until the bridge script is started.");
684
+ log('Run "reaper-mcp setup" for installation instructions.');
685
+ } else {
686
+ log("Lua bridge detected \u2014 connected to REAPER");
687
+ }
688
+ const server = createServer();
689
+ const transport = new StdioServerTransport();
690
+ await server.connect(transport);
691
+ log("MCP server connected via stdio");
692
+ }
693
+ var command = process.argv[2];
694
+ switch (command) {
695
+ case "setup":
696
+ setup().catch((err) => {
697
+ console.error("Setup failed:", err);
698
+ process.exit(1);
699
+ });
700
+ break;
701
+ case "install-skills":
702
+ installSkills().catch((err) => {
703
+ console.error("Install failed:", err);
704
+ process.exit(1);
705
+ });
706
+ break;
707
+ case "doctor":
708
+ doctor().catch((err) => {
709
+ console.error("Doctor failed:", err);
710
+ process.exit(1);
711
+ });
712
+ break;
713
+ case "status": {
714
+ (async () => {
715
+ const running = await isBridgeRunning();
716
+ console.log(`Bridge status: ${running ? "CONNECTED" : "NOT DETECTED"}`);
717
+ process.exit(running ? 0 : 1);
718
+ })();
719
+ break;
720
+ }
721
+ case "serve":
722
+ case void 0:
723
+ serve().catch((err) => {
724
+ console.error("[reaper-mcp] Fatal error:", err);
725
+ process.exit(1);
726
+ });
727
+ break;
728
+ default:
729
+ console.log(`reaper-mcp \u2014 AI-powered mixing for REAPER DAW
730
+
731
+ Usage:
732
+ reaper-mcp Start MCP server (stdio mode)
733
+ reaper-mcp serve Start MCP server (stdio mode)
734
+ reaper-mcp setup Install Lua bridge + JSFX analyzers into REAPER
735
+ reaper-mcp install-skills Install AI mix engineer knowledge into your project
736
+ reaper-mcp doctor Check that everything is configured correctly
737
+ reaper-mcp status Check if Lua bridge is running in REAPER
738
+
739
+ Quick Start:
740
+ 1. reaper-mcp setup # install REAPER components
741
+ 2. Load mcp_bridge.lua in REAPER (Actions > Load ReaScript > Run)
742
+ 3. reaper-mcp install-skills # install AI knowledge in your project
743
+ 4. Open Claude Code \u2014 REAPER tools + mix engineer brain are ready
744
+ `);
745
+ break;
746
+ }
747
+ process.on("SIGINT", () => {
748
+ console.error("[reaper-mcp] Interrupted");
749
+ process.exit(0);
750
+ });
751
+ process.on("SIGTERM", () => {
752
+ console.error("[reaper-mcp] Terminated");
753
+ process.exit(0);
754
+ });
755
+ //# sourceMappingURL=main.js.map