@mthines/reaper-mcp 0.6.0 → 0.8.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 +209 -46
- package/claude-agents/gain-stage.md +25 -8
- package/claude-agents/master.md +3 -0
- package/claude-agents/mix-analyzer.md +17 -8
- package/claude-agents/mix-engineer.md +56 -16
- package/knowledge/CLAUDE.md +71 -0
- package/knowledge/reference/common-mistakes.md +29 -1
- package/knowledge/reference/frequencies.md +15 -13
- package/knowledge/reference/metering.md +19 -0
- package/knowledge/reference/perceived-loudness.md +122 -0
- package/knowledge/workflows/gain-staging.md +20 -6
- package/knowledge/workflows/learn-plugin.md +143 -0
- package/main.js +877 -132
- package/package.json +6 -1
- package/reaper/CLAUDE.md +60 -0
- package/reaper/install.sh +8 -4
- package/reaper/mcp_bridge.lua +873 -48
package/main.js
CHANGED
|
@@ -1,16 +1,143 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// apps/reaper-mcp-server/src/main.ts
|
|
4
|
+
import { SpanKind as SpanKind3, SpanStatusCode as SpanStatusCode3 } from "@opentelemetry/api";
|
|
4
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
6
|
|
|
6
7
|
// apps/reaper-mcp-server/src/server.ts
|
|
7
8
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { SpanKind as SpanKind2, SpanStatusCode as SpanStatusCode2 } from "@opentelemetry/api";
|
|
10
|
+
|
|
11
|
+
// apps/reaper-mcp-server/src/telemetry.ts
|
|
12
|
+
import { trace, metrics } from "@opentelemetry/api";
|
|
13
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
14
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
15
|
+
import {
|
|
16
|
+
ATTR_SERVICE_NAME,
|
|
17
|
+
ATTR_SERVICE_NAMESPACE,
|
|
18
|
+
ATTR_SERVICE_VERSION
|
|
19
|
+
} from "@opentelemetry/semantic-conventions";
|
|
20
|
+
import { createRequire } from "node:module";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { dirname, join } from "node:path";
|
|
23
|
+
var SERVICE_NAME = process.env["OTEL_SERVICE_NAME"] ?? "reaper-mcp-server";
|
|
24
|
+
var SERVICE_NAMESPACE = "reaper-mcp";
|
|
25
|
+
var DEPLOYMENT_ENV = process.env["DEPLOYMENT_ENVIRONMENT"] ?? "development";
|
|
26
|
+
var SCOPE_NAME = "reaper-mcp-server";
|
|
27
|
+
function readServiceVersion() {
|
|
28
|
+
try {
|
|
29
|
+
const require2 = createRequire(import.meta.url);
|
|
30
|
+
const __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const pkgPaths = [
|
|
32
|
+
join(__dirname2, "..", "package.json"),
|
|
33
|
+
join(__dirname2, "package.json")
|
|
34
|
+
];
|
|
35
|
+
for (const p of pkgPaths) {
|
|
36
|
+
try {
|
|
37
|
+
const pkg = require2(p);
|
|
38
|
+
if (pkg.version) return pkg.version;
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
return "unknown";
|
|
45
|
+
}
|
|
46
|
+
var sdk = null;
|
|
47
|
+
async function initTelemetry() {
|
|
48
|
+
if (sdk) return;
|
|
49
|
+
const serviceVersion = readServiceVersion();
|
|
50
|
+
const resource = resourceFromAttributes({
|
|
51
|
+
[ATTR_SERVICE_NAME]: SERVICE_NAME,
|
|
52
|
+
[ATTR_SERVICE_NAMESPACE]: SERVICE_NAMESPACE,
|
|
53
|
+
[ATTR_SERVICE_VERSION]: serviceVersion,
|
|
54
|
+
"deployment.environment.name": DEPLOYMENT_ENV
|
|
55
|
+
});
|
|
56
|
+
sdk = new NodeSDK({ resource });
|
|
57
|
+
sdk.start();
|
|
58
|
+
const tracesExporter = process.env["OTEL_TRACES_EXPORTER"] ?? "none";
|
|
59
|
+
const metricsExporter = process.env["OTEL_METRICS_EXPORTER"] ?? "none";
|
|
60
|
+
const logsExporter = process.env["OTEL_LOGS_EXPORTER"] ?? "none";
|
|
61
|
+
const endpoint = process.env["OTEL_EXPORTER_OTLP_ENDPOINT"];
|
|
62
|
+
const hasExporter = [tracesExporter, metricsExporter, logsExporter].some(
|
|
63
|
+
(e) => e !== "none" && e !== ""
|
|
64
|
+
);
|
|
65
|
+
if (hasExporter) {
|
|
66
|
+
const signals = [
|
|
67
|
+
tracesExporter !== "none" && `traces=${tracesExporter}`,
|
|
68
|
+
metricsExporter !== "none" && `metrics=${metricsExporter}`,
|
|
69
|
+
logsExporter !== "none" && `logs=${logsExporter}`
|
|
70
|
+
].filter(Boolean).join(", ");
|
|
71
|
+
console.error(`[reaper-mcp] OpenTelemetry enabled (${signals})`);
|
|
72
|
+
if (endpoint) {
|
|
73
|
+
console.error(`[reaper-mcp] OTLP endpoint: ${endpoint}`);
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
console.error(
|
|
77
|
+
"[reaper-mcp] OpenTelemetry initialized (no exporters configured \u2014 telemetry stays local)"
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function shutdownTelemetry() {
|
|
82
|
+
if (!sdk) return;
|
|
83
|
+
try {
|
|
84
|
+
await sdk.shutdown();
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error("[reaper-mcp] OTel shutdown error:", err);
|
|
87
|
+
} finally {
|
|
88
|
+
sdk = null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function getTracer() {
|
|
92
|
+
return trace.getTracer(SCOPE_NAME);
|
|
93
|
+
}
|
|
94
|
+
function getMeter() {
|
|
95
|
+
return metrics.getMeter(SCOPE_NAME);
|
|
96
|
+
}
|
|
97
|
+
function getTraceContext() {
|
|
98
|
+
const span = trace.getActiveSpan();
|
|
99
|
+
if (!span) return { trace_id: "", span_id: "" };
|
|
100
|
+
const ctx = span.spanContext();
|
|
101
|
+
return { trace_id: ctx.traceId, span_id: ctx.spanId };
|
|
102
|
+
}
|
|
103
|
+
var _commandDurationHistogram = null;
|
|
104
|
+
var _commandCounter = null;
|
|
105
|
+
var _timeoutCounter = null;
|
|
106
|
+
function getCommandDurationHistogram() {
|
|
107
|
+
if (!_commandDurationHistogram) {
|
|
108
|
+
_commandDurationHistogram = getMeter().createHistogram(
|
|
109
|
+
"mcp.bridge.command.duration",
|
|
110
|
+
{
|
|
111
|
+
description: "Duration of MCP bridge commands (file IPC round-trip)",
|
|
112
|
+
unit: "ms"
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return _commandDurationHistogram;
|
|
117
|
+
}
|
|
118
|
+
function getCommandCounter() {
|
|
119
|
+
if (!_commandCounter) {
|
|
120
|
+
_commandCounter = getMeter().createCounter("mcp.bridge.command.count", {
|
|
121
|
+
description: "Number of MCP bridge commands sent, by type and success"
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return _commandCounter;
|
|
125
|
+
}
|
|
126
|
+
function getTimeoutCounter() {
|
|
127
|
+
if (!_timeoutCounter) {
|
|
128
|
+
_timeoutCounter = getMeter().createCounter("mcp.bridge.timeout.count", {
|
|
129
|
+
description: "Number of MCP bridge commands that timed out waiting for REAPER"
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return _timeoutCounter;
|
|
133
|
+
}
|
|
8
134
|
|
|
9
135
|
// apps/reaper-mcp-server/src/bridge.ts
|
|
10
136
|
import { randomUUID } from "node:crypto";
|
|
11
137
|
import { readFile, writeFile, readdir, unlink, mkdir, stat } from "node:fs/promises";
|
|
12
|
-
import { join } from "node:path";
|
|
138
|
+
import { join as join2 } from "node:path";
|
|
13
139
|
import { homedir, platform } from "node:os";
|
|
140
|
+
import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
|
|
14
141
|
var POLL_INTERVAL_MS = 50;
|
|
15
142
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
16
143
|
function getReaperResourcePath() {
|
|
@@ -19,15 +146,15 @@ function getReaperResourcePath() {
|
|
|
19
146
|
const home = homedir();
|
|
20
147
|
switch (platform()) {
|
|
21
148
|
case "darwin":
|
|
22
|
-
return
|
|
149
|
+
return join2(home, "Library", "Application Support", "REAPER");
|
|
23
150
|
case "win32":
|
|
24
|
-
return
|
|
151
|
+
return join2(process.env["APPDATA"] ?? join2(home, "AppData", "Roaming"), "REAPER");
|
|
25
152
|
default:
|
|
26
|
-
return
|
|
153
|
+
return join2(home, ".config", "REAPER");
|
|
27
154
|
}
|
|
28
155
|
}
|
|
29
156
|
function getBridgeDir() {
|
|
30
|
-
return
|
|
157
|
+
return join2(getReaperResourcePath(), "Scripts", "mcp_bridge_data");
|
|
31
158
|
}
|
|
32
159
|
async function ensureBridgeDir() {
|
|
33
160
|
const dir = getBridgeDir();
|
|
@@ -35,40 +162,102 @@ async function ensureBridgeDir() {
|
|
|
35
162
|
return dir;
|
|
36
163
|
}
|
|
37
164
|
async function sendCommand(type, params = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
165
|
+
const tracer = getTracer();
|
|
166
|
+
const startMs = Date.now();
|
|
167
|
+
return tracer.startActiveSpan(
|
|
168
|
+
`mcp.bridge ${type}`,
|
|
169
|
+
{
|
|
170
|
+
kind: SpanKind.CLIENT,
|
|
171
|
+
attributes: {
|
|
172
|
+
"mcp.command.type": type,
|
|
173
|
+
"mcp.bridge.timeout_ms": timeoutMs
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
async (span) => {
|
|
177
|
+
const dir = await ensureBridgeDir();
|
|
178
|
+
const id = randomUUID();
|
|
179
|
+
span.setAttribute("mcp.command.id", id);
|
|
180
|
+
const command2 = {
|
|
181
|
+
id,
|
|
182
|
+
type,
|
|
183
|
+
params,
|
|
184
|
+
timestamp: Date.now()
|
|
185
|
+
};
|
|
186
|
+
const commandPath = join2(dir, `command_${id}.json`);
|
|
187
|
+
await writeFile(commandPath, JSON.stringify(command2, null, 2), "utf-8");
|
|
188
|
+
const traceCtx = getTraceContext();
|
|
189
|
+
console.error(
|
|
190
|
+
JSON.stringify({
|
|
191
|
+
msg: "bridge_command_sent",
|
|
192
|
+
commandType: type,
|
|
193
|
+
commandId: id,
|
|
194
|
+
...traceCtx
|
|
195
|
+
})
|
|
196
|
+
);
|
|
197
|
+
const responsePath = join2(dir, `response_${id}.json`);
|
|
198
|
+
const deadline = Date.now() + timeoutMs;
|
|
199
|
+
while (Date.now() < deadline) {
|
|
200
|
+
try {
|
|
201
|
+
const data = await readFile(responsePath, "utf-8");
|
|
202
|
+
const response = JSON.parse(data);
|
|
203
|
+
await Promise.allSettled([unlink(commandPath), unlink(responsePath)]);
|
|
204
|
+
const durationMs2 = Date.now() - startMs;
|
|
205
|
+
const succeeded = response.success;
|
|
206
|
+
span.setAttribute("mcp.response.success", succeeded);
|
|
207
|
+
if (!succeeded) {
|
|
208
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: response.error ?? "Bridge error" });
|
|
209
|
+
span.setAttribute("mcp.response.error", response.error ?? "unknown");
|
|
210
|
+
console.error(
|
|
211
|
+
JSON.stringify({
|
|
212
|
+
msg: "bridge_command_error",
|
|
213
|
+
commandType: type,
|
|
214
|
+
commandId: id,
|
|
215
|
+
error: response.error,
|
|
216
|
+
durationMs: durationMs2,
|
|
217
|
+
...traceCtx
|
|
218
|
+
})
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
span.end();
|
|
222
|
+
getCommandDurationHistogram().record(durationMs2, { command_type: type });
|
|
223
|
+
getCommandCounter().add(1, { command_type: type, success: String(succeeded) });
|
|
224
|
+
return response;
|
|
225
|
+
} catch {
|
|
226
|
+
await sleep(POLL_INTERVAL_MS);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
await unlink(commandPath).catch(() => {
|
|
230
|
+
});
|
|
231
|
+
const durationMs = Date.now() - startMs;
|
|
232
|
+
const timeoutMsg = `Timeout: no response from REAPER Lua bridge after ${timeoutMs}ms. Is the bridge script running in REAPER?`;
|
|
233
|
+
span.setAttribute("mcp.response.success", false);
|
|
234
|
+
span.setAttribute("mcp.response.error", "timeout");
|
|
235
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: timeoutMsg });
|
|
236
|
+
span.end();
|
|
237
|
+
getCommandDurationHistogram().record(durationMs, { command_type: type });
|
|
238
|
+
getCommandCounter().add(1, { command_type: type, success: "false" });
|
|
239
|
+
getTimeoutCounter().add(1, { command_type: type });
|
|
240
|
+
console.error(
|
|
241
|
+
JSON.stringify({
|
|
242
|
+
msg: "bridge_command_timeout",
|
|
243
|
+
commandType: type,
|
|
244
|
+
commandId: id,
|
|
245
|
+
timeoutMs,
|
|
246
|
+
...traceCtx
|
|
247
|
+
})
|
|
248
|
+
);
|
|
249
|
+
return {
|
|
250
|
+
id,
|
|
251
|
+
success: false,
|
|
252
|
+
error: timeoutMsg,
|
|
253
|
+
timestamp: Date.now()
|
|
254
|
+
};
|
|
58
255
|
}
|
|
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
|
-
};
|
|
256
|
+
);
|
|
68
257
|
}
|
|
69
258
|
async function isBridgeRunning() {
|
|
70
259
|
const dir = getBridgeDir();
|
|
71
|
-
const heartbeatPath =
|
|
260
|
+
const heartbeatPath = join2(dir, "heartbeat.json");
|
|
72
261
|
try {
|
|
73
262
|
const info = await stat(heartbeatPath);
|
|
74
263
|
return Date.now() - info.mtimeMs < 5e3;
|
|
@@ -84,7 +273,7 @@ async function cleanupStaleFiles(maxAgeMs = 3e4) {
|
|
|
84
273
|
const now = Date.now();
|
|
85
274
|
for (const file of files) {
|
|
86
275
|
if (!file.startsWith("command_") && !file.startsWith("response_")) continue;
|
|
87
|
-
const filePath =
|
|
276
|
+
const filePath = join2(dir, file);
|
|
88
277
|
const info = await stat(filePath);
|
|
89
278
|
if (now - info.mtimeMs > maxAgeMs) {
|
|
90
279
|
await unlink(filePath).catch(() => {
|
|
@@ -97,10 +286,10 @@ async function cleanupStaleFiles(maxAgeMs = 3e4) {
|
|
|
97
286
|
return cleaned;
|
|
98
287
|
}
|
|
99
288
|
function getReaperScriptsPath() {
|
|
100
|
-
return
|
|
289
|
+
return join2(getReaperResourcePath(), "Scripts");
|
|
101
290
|
}
|
|
102
291
|
function getReaperEffectsPath() {
|
|
103
|
-
return
|
|
292
|
+
return join2(getReaperResourcePath(), "Effects");
|
|
104
293
|
}
|
|
105
294
|
function sleep(ms) {
|
|
106
295
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -151,11 +340,11 @@ function registerTrackTools(server) {
|
|
|
151
340
|
);
|
|
152
341
|
server.tool(
|
|
153
342
|
"set_track_property",
|
|
154
|
-
"Set a track property: volume (dB), pan (-1.0 to 1.0), mute (0/1),
|
|
343
|
+
"Set a track property: volume (dB), pan (-1.0 to 1.0), mute/solo/recordArm/phase (0/1), input (REAPER input index)",
|
|
155
344
|
{
|
|
156
345
|
trackIndex: z.coerce.number().int().min(0).describe("Zero-based track index"),
|
|
157
|
-
property: z.enum(["volume", "pan", "mute", "solo"]).describe("Property to set"),
|
|
158
|
-
value: z.coerce.number().describe("Value: volume in dB, pan -1.0\u20131.0, mute/solo 0 or 1")
|
|
346
|
+
property: z.enum(["volume", "pan", "mute", "solo", "recordArm", "phase", "input"]).describe("Property to set: volume (dB), pan (-1.0\u20131.0), mute/solo/recordArm/phase (0 or 1), input (REAPER input index, -1=no input)"),
|
|
347
|
+
value: z.coerce.number().describe("Value: volume in dB, pan -1.0\u20131.0, mute/solo/recordArm/phase 0 or 1, input = REAPER input index")
|
|
159
348
|
},
|
|
160
349
|
async ({ trackIndex, property, value }) => {
|
|
161
350
|
const res = await sendCommand("set_track_property", { trackIndex, property, value });
|
|
@@ -233,6 +422,38 @@ function registerFxTools(server) {
|
|
|
233
422
|
return { content: [{ type: "text", text: `Set FX ${fxIndex} param ${paramIndex} = ${value}` }] };
|
|
234
423
|
}
|
|
235
424
|
);
|
|
425
|
+
server.tool(
|
|
426
|
+
"set_fx_enabled",
|
|
427
|
+
"Enable or disable (bypass) an FX plugin on a track",
|
|
428
|
+
{
|
|
429
|
+
trackIndex: z2.coerce.number().int().min(0).describe("Zero-based track index"),
|
|
430
|
+
fxIndex: z2.coerce.number().int().min(0).describe("Zero-based FX index in the chain"),
|
|
431
|
+
enabled: z2.coerce.number().int().min(0).max(1).describe("1 = enabled, 0 = disabled (bypassed)")
|
|
432
|
+
},
|
|
433
|
+
async ({ trackIndex, fxIndex, enabled }) => {
|
|
434
|
+
const res = await sendCommand("set_fx_enabled", { trackIndex, fxIndex, enabled });
|
|
435
|
+
if (!res.success) {
|
|
436
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
437
|
+
}
|
|
438
|
+
return { content: [{ type: "text", text: `FX ${fxIndex} on track ${trackIndex} ${enabled ? "enabled" : "disabled"}` }] };
|
|
439
|
+
}
|
|
440
|
+
);
|
|
441
|
+
server.tool(
|
|
442
|
+
"set_fx_offline",
|
|
443
|
+
"Set an FX plugin online or offline. Offline FX uses no CPU but preserves settings.",
|
|
444
|
+
{
|
|
445
|
+
trackIndex: z2.coerce.number().int().min(0).describe("Zero-based track index"),
|
|
446
|
+
fxIndex: z2.coerce.number().int().min(0).describe("Zero-based FX index in the chain"),
|
|
447
|
+
offline: z2.coerce.number().int().min(0).max(1).describe("1 = offline, 0 = online")
|
|
448
|
+
},
|
|
449
|
+
async ({ trackIndex, fxIndex, offline }) => {
|
|
450
|
+
const res = await sendCommand("set_fx_offline", { trackIndex, fxIndex, offline });
|
|
451
|
+
if (!res.success) {
|
|
452
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
453
|
+
}
|
|
454
|
+
return { content: [{ type: "text", text: `FX ${fxIndex} on track ${trackIndex} set ${offline ? "offline" : "online"}` }] };
|
|
455
|
+
}
|
|
456
|
+
);
|
|
236
457
|
}
|
|
237
458
|
|
|
238
459
|
// apps/reaper-mcp-server/src/tools/meters.ts
|
|
@@ -552,13 +773,30 @@ function registerMidiTools(server) {
|
|
|
552
773
|
);
|
|
553
774
|
server.tool(
|
|
554
775
|
"get_midi_notes",
|
|
555
|
-
"Get
|
|
776
|
+
"Get MIDI notes in a MIDI item. Supports pagination with offset/limit for large items. For a high-level overview of a large MIDI item, use analyze_midi instead. Returns pitch (0-127, 60=C4), velocity (0-127), position and duration in beats from item start.",
|
|
777
|
+
{
|
|
778
|
+
trackIndex: z10.coerce.number().min(0).describe("0-based track index"),
|
|
779
|
+
itemIndex: z10.coerce.number().min(0).describe("0-based item index on the track"),
|
|
780
|
+
offset: z10.coerce.number().min(0).optional().describe("Skip first N notes (default 0)"),
|
|
781
|
+
limit: z10.coerce.number().min(1).optional().describe("Max notes to return (default all). Use with offset to paginate large items.")
|
|
782
|
+
},
|
|
783
|
+
async ({ trackIndex, itemIndex, offset, limit }) => {
|
|
784
|
+
const res = await sendCommand("get_midi_notes", { trackIndex, itemIndex, offset, limit });
|
|
785
|
+
if (!res.success) {
|
|
786
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
787
|
+
}
|
|
788
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
789
|
+
}
|
|
790
|
+
);
|
|
791
|
+
server.tool(
|
|
792
|
+
"analyze_midi",
|
|
793
|
+
"Analyze a MIDI item and return summary statistics computed in REAPER. Much more efficient than get_midi_notes for large items. Returns per-pitch stats (count, velocity min/max/avg/stddev), velocity histogram, and machine gun detection (consecutive identical velocities). Use this first to understand the MIDI data before reading individual notes.",
|
|
556
794
|
{
|
|
557
795
|
trackIndex: z10.coerce.number().min(0).describe("0-based track index"),
|
|
558
796
|
itemIndex: z10.coerce.number().min(0).describe("0-based item index on the track")
|
|
559
797
|
},
|
|
560
798
|
async ({ trackIndex, itemIndex }) => {
|
|
561
|
-
const res = await sendCommand("
|
|
799
|
+
const res = await sendCommand("analyze_midi", { trackIndex, itemIndex });
|
|
562
800
|
if (!res.success) {
|
|
563
801
|
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
564
802
|
}
|
|
@@ -595,11 +833,17 @@ function registerMidiTools(server) {
|
|
|
595
833
|
);
|
|
596
834
|
server.tool(
|
|
597
835
|
"insert_midi_notes",
|
|
598
|
-
|
|
836
|
+
"Batch insert multiple MIDI notes in a single call. Pass a native array of note objects. Each note: { pitch, velocity, startPosition, duration, channel? }. Positions/durations in beats from item start (1.0=quarter, 0.5=eighth).",
|
|
599
837
|
{
|
|
600
838
|
trackIndex: z10.coerce.number().min(0).describe("0-based track index"),
|
|
601
839
|
itemIndex: z10.coerce.number().min(0).describe("0-based item index on the track"),
|
|
602
|
-
notes: z10.
|
|
840
|
+
notes: z10.array(z10.object({
|
|
841
|
+
pitch: z10.coerce.number().min(0).max(127).describe("MIDI note number (60=C4/Middle C)"),
|
|
842
|
+
velocity: z10.coerce.number().min(1).max(127).describe("Note velocity (1-127)"),
|
|
843
|
+
startPosition: z10.coerce.number().min(0).describe("Start position in beats from item start"),
|
|
844
|
+
duration: z10.coerce.number().min(0).describe("Duration in beats (1.0=quarter note)"),
|
|
845
|
+
channel: z10.coerce.number().min(0).max(15).optional().describe("MIDI channel 0-15 (default 0)")
|
|
846
|
+
})).describe("Array of notes to insert")
|
|
603
847
|
},
|
|
604
848
|
async ({ trackIndex, itemIndex, notes }) => {
|
|
605
849
|
const res = await sendCommand("insert_midi_notes", { trackIndex, itemIndex, notes });
|
|
@@ -639,6 +883,29 @@ function registerMidiTools(server) {
|
|
|
639
883
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
640
884
|
}
|
|
641
885
|
);
|
|
886
|
+
server.tool(
|
|
887
|
+
"edit_midi_notes",
|
|
888
|
+
"Batch edit multiple MIDI notes in a single call. Much more efficient than calling edit_midi_note repeatedly. Pass a native array of edit objects, each with noteIndex and any fields to change (pitch, velocity, startPosition, duration, channel). Only provided fields are changed.",
|
|
889
|
+
{
|
|
890
|
+
trackIndex: z10.coerce.number().min(0).describe("0-based track index"),
|
|
891
|
+
itemIndex: z10.coerce.number().min(0).describe("0-based item index on the track"),
|
|
892
|
+
edits: z10.array(z10.object({
|
|
893
|
+
noteIndex: z10.coerce.number().min(0).describe("0-based note index to edit"),
|
|
894
|
+
pitch: z10.coerce.number().min(0).max(127).optional().describe("New pitch (0-127)"),
|
|
895
|
+
velocity: z10.coerce.number().min(1).max(127).optional().describe("New velocity (1-127)"),
|
|
896
|
+
startPosition: z10.coerce.number().min(0).optional().describe("New start position in beats from item start"),
|
|
897
|
+
duration: z10.coerce.number().min(0).optional().describe("New duration in beats"),
|
|
898
|
+
channel: z10.coerce.number().min(0).max(15).optional().describe("New MIDI channel (0-15)")
|
|
899
|
+
})).describe("Array of note edits to apply")
|
|
900
|
+
},
|
|
901
|
+
async ({ trackIndex, itemIndex, edits }) => {
|
|
902
|
+
const res = await sendCommand("edit_midi_notes", { trackIndex, itemIndex, edits });
|
|
903
|
+
if (!res.success) {
|
|
904
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
905
|
+
}
|
|
906
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
907
|
+
}
|
|
908
|
+
);
|
|
642
909
|
server.tool(
|
|
643
910
|
"delete_midi_note",
|
|
644
911
|
"Delete a MIDI note by index from a MIDI item",
|
|
@@ -820,6 +1087,30 @@ function registerMediaTools(server) {
|
|
|
820
1087
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
821
1088
|
}
|
|
822
1089
|
);
|
|
1090
|
+
server.tool(
|
|
1091
|
+
"set_media_items_properties",
|
|
1092
|
+
"Batch set properties on multiple media items in a single call. Much more efficient than calling set_media_item_properties repeatedly. Pass a native array of item objects, each with trackIndex, itemIndex, and any properties to change.",
|
|
1093
|
+
{
|
|
1094
|
+
items: z11.array(z11.object({
|
|
1095
|
+
trackIndex: z11.coerce.number().min(0).describe("0-based track index"),
|
|
1096
|
+
itemIndex: z11.coerce.number().min(0).describe("0-based item index on the track"),
|
|
1097
|
+
position: z11.coerce.number().min(0).optional().describe("New position in seconds"),
|
|
1098
|
+
length: z11.coerce.number().min(0).optional().describe("New length in seconds"),
|
|
1099
|
+
volume: z11.coerce.number().optional().describe("New volume in dB (0 = unity gain)"),
|
|
1100
|
+
mute: z11.coerce.number().min(0).max(1).optional().describe("Mute state (0=unmuted, 1=muted)"),
|
|
1101
|
+
fadeInLength: z11.coerce.number().min(0).optional().describe("Fade-in length in seconds"),
|
|
1102
|
+
fadeOutLength: z11.coerce.number().min(0).optional().describe("Fade-out length in seconds"),
|
|
1103
|
+
playRate: z11.coerce.number().min(0.1).max(10).optional().describe("Playback rate (1.0=normal)")
|
|
1104
|
+
})).describe("Array of item property updates to apply")
|
|
1105
|
+
},
|
|
1106
|
+
async ({ items }) => {
|
|
1107
|
+
const res = await sendCommand("set_media_items_properties", { items });
|
|
1108
|
+
if (!res.success) {
|
|
1109
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1110
|
+
}
|
|
1111
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1112
|
+
}
|
|
1113
|
+
);
|
|
823
1114
|
server.tool(
|
|
824
1115
|
"split_media_item",
|
|
825
1116
|
"Split a media item at a given position (absolute project time in seconds). Returns info about both resulting items.",
|
|
@@ -935,12 +1226,382 @@ function registerMediaTools(server) {
|
|
|
935
1226
|
);
|
|
936
1227
|
}
|
|
937
1228
|
|
|
1229
|
+
// apps/reaper-mcp-server/src/tools/selection.ts
|
|
1230
|
+
import { z as z12 } from "zod/v4";
|
|
1231
|
+
function registerSelectionTools(server) {
|
|
1232
|
+
server.tool(
|
|
1233
|
+
"get_selected_tracks",
|
|
1234
|
+
"Get all currently selected tracks in REAPER with their indices and names",
|
|
1235
|
+
{},
|
|
1236
|
+
async () => {
|
|
1237
|
+
const res = await sendCommand("get_selected_tracks");
|
|
1238
|
+
if (!res.success) {
|
|
1239
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1240
|
+
}
|
|
1241
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1242
|
+
}
|
|
1243
|
+
);
|
|
1244
|
+
server.tool(
|
|
1245
|
+
"get_time_selection",
|
|
1246
|
+
"Get the current time selection (loop selection) start and end in seconds",
|
|
1247
|
+
{},
|
|
1248
|
+
async () => {
|
|
1249
|
+
const res = await sendCommand("get_time_selection");
|
|
1250
|
+
if (!res.success) {
|
|
1251
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1252
|
+
}
|
|
1253
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1254
|
+
}
|
|
1255
|
+
);
|
|
1256
|
+
server.tool(
|
|
1257
|
+
"set_time_selection",
|
|
1258
|
+
"Set the time selection (loop selection) to a start and end position in seconds",
|
|
1259
|
+
{
|
|
1260
|
+
start: z12.coerce.number().min(0).describe("Start position in seconds"),
|
|
1261
|
+
end: z12.coerce.number().min(0).describe("End position in seconds")
|
|
1262
|
+
},
|
|
1263
|
+
async ({ start, end }) => {
|
|
1264
|
+
const res = await sendCommand("set_time_selection", { start, end });
|
|
1265
|
+
if (!res.success) {
|
|
1266
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1267
|
+
}
|
|
1268
|
+
return { content: [{ type: "text", text: `Time selection set: ${start}s - ${end}s` }] };
|
|
1269
|
+
}
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// apps/reaper-mcp-server/src/tools/markers.ts
|
|
1274
|
+
import { z as z13 } from "zod/v4";
|
|
1275
|
+
function registerMarkerTools(server) {
|
|
1276
|
+
server.tool(
|
|
1277
|
+
"list_markers",
|
|
1278
|
+
"List all project markers with index, name, position, and color",
|
|
1279
|
+
{},
|
|
1280
|
+
async () => {
|
|
1281
|
+
const res = await sendCommand("list_markers");
|
|
1282
|
+
if (!res.success) {
|
|
1283
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1284
|
+
}
|
|
1285
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1286
|
+
}
|
|
1287
|
+
);
|
|
1288
|
+
server.tool(
|
|
1289
|
+
"list_regions",
|
|
1290
|
+
"List all project regions with index, name, start/end positions, and color",
|
|
1291
|
+
{},
|
|
1292
|
+
async () => {
|
|
1293
|
+
const res = await sendCommand("list_regions");
|
|
1294
|
+
if (!res.success) {
|
|
1295
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1296
|
+
}
|
|
1297
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1298
|
+
}
|
|
1299
|
+
);
|
|
1300
|
+
server.tool(
|
|
1301
|
+
"add_marker",
|
|
1302
|
+
"Add a project marker at a position in seconds, with optional name and color",
|
|
1303
|
+
{
|
|
1304
|
+
position: z13.coerce.number().min(0).describe("Position in seconds"),
|
|
1305
|
+
name: z13.string().optional().describe("Marker name"),
|
|
1306
|
+
color: z13.coerce.number().optional().describe("REAPER native color int (0 = default)")
|
|
1307
|
+
},
|
|
1308
|
+
async ({ position, name, color }) => {
|
|
1309
|
+
const res = await sendCommand("add_marker", { position, name, color });
|
|
1310
|
+
if (!res.success) {
|
|
1311
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1312
|
+
}
|
|
1313
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1314
|
+
}
|
|
1315
|
+
);
|
|
1316
|
+
server.tool(
|
|
1317
|
+
"add_region",
|
|
1318
|
+
"Add a project region with start/end in seconds, optional name and color",
|
|
1319
|
+
{
|
|
1320
|
+
start: z13.coerce.number().min(0).describe("Region start in seconds"),
|
|
1321
|
+
end: z13.coerce.number().min(0).describe("Region end in seconds"),
|
|
1322
|
+
name: z13.string().optional().describe("Region name"),
|
|
1323
|
+
color: z13.coerce.number().optional().describe("REAPER native color int (0 = default)")
|
|
1324
|
+
},
|
|
1325
|
+
async ({ start, end, name, color }) => {
|
|
1326
|
+
const res = await sendCommand("add_region", { start, end, name, color });
|
|
1327
|
+
if (!res.success) {
|
|
1328
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1329
|
+
}
|
|
1330
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1331
|
+
}
|
|
1332
|
+
);
|
|
1333
|
+
server.tool(
|
|
1334
|
+
"delete_marker",
|
|
1335
|
+
"Delete a project marker by its marker index number",
|
|
1336
|
+
{
|
|
1337
|
+
markerIndex: z13.coerce.number().int().min(0).describe("Marker index number")
|
|
1338
|
+
},
|
|
1339
|
+
async ({ markerIndex }) => {
|
|
1340
|
+
const res = await sendCommand("delete_marker", { markerIndex });
|
|
1341
|
+
if (!res.success) {
|
|
1342
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1343
|
+
}
|
|
1344
|
+
return { content: [{ type: "text", text: `Deleted marker ${markerIndex}` }] };
|
|
1345
|
+
}
|
|
1346
|
+
);
|
|
1347
|
+
server.tool(
|
|
1348
|
+
"delete_region",
|
|
1349
|
+
"Delete a project region by its region index number",
|
|
1350
|
+
{
|
|
1351
|
+
regionIndex: z13.coerce.number().int().min(0).describe("Region index number")
|
|
1352
|
+
},
|
|
1353
|
+
async ({ regionIndex }) => {
|
|
1354
|
+
const res = await sendCommand("delete_region", { regionIndex });
|
|
1355
|
+
if (!res.success) {
|
|
1356
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1357
|
+
}
|
|
1358
|
+
return { content: [{ type: "text", text: `Deleted region ${regionIndex}` }] };
|
|
1359
|
+
}
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// apps/reaper-mcp-server/src/tools/tempo.ts
|
|
1364
|
+
function registerTempoTools(server) {
|
|
1365
|
+
server.tool(
|
|
1366
|
+
"get_tempo_map",
|
|
1367
|
+
"Get all tempo and time signature change points in the project (tempo map markers)",
|
|
1368
|
+
{},
|
|
1369
|
+
async () => {
|
|
1370
|
+
const res = await sendCommand("get_tempo_map");
|
|
1371
|
+
if (!res.success) {
|
|
1372
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1373
|
+
}
|
|
1374
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1375
|
+
}
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// apps/reaper-mcp-server/src/tools/envelopes.ts
|
|
1380
|
+
import { z as z14 } from "zod/v4";
|
|
1381
|
+
function registerEnvelopeTools(server) {
|
|
1382
|
+
server.tool(
|
|
1383
|
+
"get_track_envelopes",
|
|
1384
|
+
"List all automation envelopes on a track (volume, pan, mute, FX params, etc.) with point counts",
|
|
1385
|
+
{
|
|
1386
|
+
trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index")
|
|
1387
|
+
},
|
|
1388
|
+
async ({ trackIndex }) => {
|
|
1389
|
+
const res = await sendCommand("get_track_envelopes", { trackIndex });
|
|
1390
|
+
if (!res.success) {
|
|
1391
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1392
|
+
}
|
|
1393
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1394
|
+
}
|
|
1395
|
+
);
|
|
1396
|
+
server.tool(
|
|
1397
|
+
"get_envelope_points",
|
|
1398
|
+
"Get automation points for a track envelope with pagination. Returns time, value, shape, tension per point.",
|
|
1399
|
+
{
|
|
1400
|
+
trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
|
|
1401
|
+
envelopeIndex: z14.coerce.number().int().min(0).describe("Zero-based envelope index on the track"),
|
|
1402
|
+
offset: z14.coerce.number().int().min(0).optional().describe("Skip first N points (default 0)"),
|
|
1403
|
+
limit: z14.coerce.number().int().min(1).optional().describe("Max points to return (default all)")
|
|
1404
|
+
},
|
|
1405
|
+
async ({ trackIndex, envelopeIndex, offset, limit }) => {
|
|
1406
|
+
const res = await sendCommand("get_envelope_points", { trackIndex, envelopeIndex, offset, limit });
|
|
1407
|
+
if (!res.success) {
|
|
1408
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1409
|
+
}
|
|
1410
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1411
|
+
}
|
|
1412
|
+
);
|
|
1413
|
+
server.tool(
|
|
1414
|
+
"insert_envelope_point",
|
|
1415
|
+
"Insert an automation point on a track envelope at a given time with value and optional shape/tension",
|
|
1416
|
+
{
|
|
1417
|
+
trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
|
|
1418
|
+
envelopeIndex: z14.coerce.number().int().min(0).describe("Zero-based envelope index on the track"),
|
|
1419
|
+
time: z14.coerce.number().describe("Time position in seconds"),
|
|
1420
|
+
value: z14.coerce.number().describe("Envelope value (scale depends on envelope type)"),
|
|
1421
|
+
shape: z14.coerce.number().int().min(0).max(5).optional().describe("Shape: 0=linear, 1=square, 2=slow start/end, 3=fast start, 4=fast end, 5=bezier"),
|
|
1422
|
+
tension: z14.coerce.number().min(-1).max(1).optional().describe("Tension for bezier shape (-1.0 to 1.0)")
|
|
1423
|
+
},
|
|
1424
|
+
async ({ trackIndex, envelopeIndex, time, value, shape, tension }) => {
|
|
1425
|
+
const res = await sendCommand("insert_envelope_point", {
|
|
1426
|
+
trackIndex,
|
|
1427
|
+
envelopeIndex,
|
|
1428
|
+
time,
|
|
1429
|
+
value,
|
|
1430
|
+
shape,
|
|
1431
|
+
tension
|
|
1432
|
+
});
|
|
1433
|
+
if (!res.success) {
|
|
1434
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1435
|
+
}
|
|
1436
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1437
|
+
}
|
|
1438
|
+
);
|
|
1439
|
+
server.tool(
|
|
1440
|
+
"delete_envelope_point",
|
|
1441
|
+
"Delete an automation point from a track envelope by point index",
|
|
1442
|
+
{
|
|
1443
|
+
trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
|
|
1444
|
+
envelopeIndex: z14.coerce.number().int().min(0).describe("Zero-based envelope index on the track"),
|
|
1445
|
+
pointIndex: z14.coerce.number().int().min(0).describe("Zero-based point index")
|
|
1446
|
+
},
|
|
1447
|
+
async ({ trackIndex, envelopeIndex, pointIndex }) => {
|
|
1448
|
+
const res = await sendCommand("delete_envelope_point", {
|
|
1449
|
+
trackIndex,
|
|
1450
|
+
envelopeIndex,
|
|
1451
|
+
pointIndex
|
|
1452
|
+
});
|
|
1453
|
+
if (!res.success) {
|
|
1454
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1455
|
+
}
|
|
1456
|
+
return { content: [{ type: "text", text: `Deleted envelope point ${pointIndex}` }] };
|
|
1457
|
+
}
|
|
1458
|
+
);
|
|
1459
|
+
server.tool(
|
|
1460
|
+
"create_track_envelope",
|
|
1461
|
+
"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.",
|
|
1462
|
+
{
|
|
1463
|
+
trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
|
|
1464
|
+
envelopeName: z14.string().optional().describe('Built-in envelope name: "Volume", "Pan", "Mute", "Width", "Trim Volume"'),
|
|
1465
|
+
fxIndex: z14.coerce.number().int().min(0).optional().describe("FX chain index (for FX parameter envelopes)"),
|
|
1466
|
+
paramIndex: z14.coerce.number().int().min(0).optional().describe("FX parameter index (required if fxIndex provided)")
|
|
1467
|
+
},
|
|
1468
|
+
async ({ trackIndex, envelopeName, fxIndex, paramIndex }) => {
|
|
1469
|
+
const res = await sendCommand("create_track_envelope", {
|
|
1470
|
+
trackIndex,
|
|
1471
|
+
envelopeName,
|
|
1472
|
+
fxIndex,
|
|
1473
|
+
paramIndex
|
|
1474
|
+
});
|
|
1475
|
+
if (!res.success) {
|
|
1476
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1477
|
+
}
|
|
1478
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1479
|
+
}
|
|
1480
|
+
);
|
|
1481
|
+
server.tool(
|
|
1482
|
+
"set_envelope_properties",
|
|
1483
|
+
"Set properties (active, visible, armed) on a track envelope. Requires SWS extension for full support.",
|
|
1484
|
+
{
|
|
1485
|
+
trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
|
|
1486
|
+
envelopeIndex: z14.coerce.number().int().min(0).describe("Zero-based envelope index on the track"),
|
|
1487
|
+
active: z14.boolean().optional().describe("Set envelope active/inactive"),
|
|
1488
|
+
visible: z14.boolean().optional().describe("Set envelope visible/hidden in arrange view"),
|
|
1489
|
+
armed: z14.boolean().optional().describe("Set envelope armed for writing automation")
|
|
1490
|
+
},
|
|
1491
|
+
async ({ trackIndex, envelopeIndex, active, visible, armed }) => {
|
|
1492
|
+
const res = await sendCommand("set_envelope_properties", {
|
|
1493
|
+
trackIndex,
|
|
1494
|
+
envelopeIndex,
|
|
1495
|
+
active,
|
|
1496
|
+
visible,
|
|
1497
|
+
armed
|
|
1498
|
+
});
|
|
1499
|
+
if (!res.success) {
|
|
1500
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1501
|
+
}
|
|
1502
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1503
|
+
}
|
|
1504
|
+
);
|
|
1505
|
+
server.tool(
|
|
1506
|
+
"clear_envelope",
|
|
1507
|
+
"Delete ALL automation points from an envelope, resetting it to its default state",
|
|
1508
|
+
{
|
|
1509
|
+
trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
|
|
1510
|
+
envelopeIndex: z14.coerce.number().int().min(0).describe("Zero-based envelope index on the track")
|
|
1511
|
+
},
|
|
1512
|
+
async ({ trackIndex, envelopeIndex }) => {
|
|
1513
|
+
const res = await sendCommand("clear_envelope", { trackIndex, envelopeIndex });
|
|
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
|
+
"remove_envelope_points",
|
|
1522
|
+
"Delete automation points in a time range from a track envelope. Use to surgically remove a section of automation.",
|
|
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
|
+
timeStart: z14.coerce.number().describe("Start of time range in seconds (inclusive)"),
|
|
1527
|
+
timeEnd: z14.coerce.number().describe("End of time range in seconds (exclusive)")
|
|
1528
|
+
},
|
|
1529
|
+
async ({ trackIndex, envelopeIndex, timeStart, timeEnd }) => {
|
|
1530
|
+
const res = await sendCommand("remove_envelope_points", {
|
|
1531
|
+
trackIndex,
|
|
1532
|
+
envelopeIndex,
|
|
1533
|
+
timeStart,
|
|
1534
|
+
timeEnd
|
|
1535
|
+
});
|
|
1536
|
+
if (!res.success) {
|
|
1537
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1538
|
+
}
|
|
1539
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1540
|
+
}
|
|
1541
|
+
);
|
|
1542
|
+
server.tool(
|
|
1543
|
+
"insert_envelope_points",
|
|
1544
|
+
"Batch insert multiple automation points on a track envelope. Much faster than repeated insert_envelope_point calls.",
|
|
1545
|
+
{
|
|
1546
|
+
trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
|
|
1547
|
+
envelopeIndex: z14.coerce.number().int().min(0).describe("Zero-based envelope index on the track"),
|
|
1548
|
+
points: z14.string().describe("JSON array of point objects: [{time, value, shape?, tension?}, ...]")
|
|
1549
|
+
},
|
|
1550
|
+
async ({ trackIndex, envelopeIndex, points }) => {
|
|
1551
|
+
const res = await sendCommand("insert_envelope_points", {
|
|
1552
|
+
trackIndex,
|
|
1553
|
+
envelopeIndex,
|
|
1554
|
+
points
|
|
1555
|
+
});
|
|
1556
|
+
if (!res.success) {
|
|
1557
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1558
|
+
}
|
|
1559
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1560
|
+
}
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
|
|
938
1564
|
// apps/reaper-mcp-server/src/server.ts
|
|
1565
|
+
function instrumentToolHandlers(server) {
|
|
1566
|
+
const originalTool = server.tool.bind(server);
|
|
1567
|
+
server.tool = (...args) => {
|
|
1568
|
+
const toolName = args[0];
|
|
1569
|
+
const cbIndex = args.length - 1;
|
|
1570
|
+
const originalCb = args[cbIndex];
|
|
1571
|
+
args[cbIndex] = async (...cbArgs) => {
|
|
1572
|
+
const tracer = getTracer();
|
|
1573
|
+
return tracer.startActiveSpan(
|
|
1574
|
+
`mcp.tool ${toolName}`,
|
|
1575
|
+
{ kind: SpanKind2.SERVER, attributes: { "mcp.tool.name": toolName } },
|
|
1576
|
+
async (span) => {
|
|
1577
|
+
try {
|
|
1578
|
+
const result = await originalCb(...cbArgs);
|
|
1579
|
+
const res = result;
|
|
1580
|
+
if (res?.isError) {
|
|
1581
|
+
const errorText = res.content?.[0]?.text ?? "Tool error";
|
|
1582
|
+
span.setStatus({ code: SpanStatusCode2.ERROR, message: errorText });
|
|
1583
|
+
span.setAttribute("mcp.tool.error", errorText);
|
|
1584
|
+
}
|
|
1585
|
+
return result;
|
|
1586
|
+
} catch (err) {
|
|
1587
|
+
const message = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
1588
|
+
span.setStatus({ code: SpanStatusCode2.ERROR, message });
|
|
1589
|
+
throw err;
|
|
1590
|
+
} finally {
|
|
1591
|
+
span.end();
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
);
|
|
1595
|
+
};
|
|
1596
|
+
return originalTool(...args);
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
939
1599
|
function createServer() {
|
|
940
1600
|
const server = new McpServer({
|
|
941
1601
|
name: "reaper-mcp",
|
|
942
1602
|
version: "0.1.0"
|
|
943
1603
|
});
|
|
1604
|
+
instrumentToolHandlers(server);
|
|
944
1605
|
registerProjectTools(server);
|
|
945
1606
|
registerTrackTools(server);
|
|
946
1607
|
registerFxTools(server);
|
|
@@ -953,30 +1614,44 @@ function createServer() {
|
|
|
953
1614
|
registerAnalysisTools(server);
|
|
954
1615
|
registerMidiTools(server);
|
|
955
1616
|
registerMediaTools(server);
|
|
1617
|
+
registerSelectionTools(server);
|
|
1618
|
+
registerMarkerTools(server);
|
|
1619
|
+
registerTempoTools(server);
|
|
1620
|
+
registerEnvelopeTools(server);
|
|
956
1621
|
return server;
|
|
957
1622
|
}
|
|
958
1623
|
|
|
959
1624
|
// apps/reaper-mcp-server/src/main.ts
|
|
960
1625
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
961
|
-
import { join as
|
|
962
|
-
import { fileURLToPath } from "node:url";
|
|
1626
|
+
import { join as join4, dirname as dirname2 } from "node:path";
|
|
1627
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
963
1628
|
import { homedir as homedir2 } from "node:os";
|
|
964
1629
|
|
|
965
1630
|
// apps/reaper-mcp-server/src/cli.ts
|
|
966
1631
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
967
|
-
import { join as
|
|
1632
|
+
import { join as join3 } from "node:path";
|
|
968
1633
|
function resolveAssetDir(baseDir, name) {
|
|
969
|
-
const sibling =
|
|
1634
|
+
const sibling = join3(baseDir, name);
|
|
970
1635
|
if (existsSync(sibling)) return sibling;
|
|
971
|
-
|
|
1636
|
+
const parent = join3(baseDir, "..", name);
|
|
1637
|
+
if (existsSync(parent)) return parent;
|
|
1638
|
+
let dir = baseDir;
|
|
1639
|
+
for (let i = 0; i < 5; i++) {
|
|
1640
|
+
const candidate = join3(dir, name);
|
|
1641
|
+
if (existsSync(candidate)) return candidate;
|
|
1642
|
+
const up = join3(dir, "..");
|
|
1643
|
+
if (up === dir) break;
|
|
1644
|
+
dir = up;
|
|
1645
|
+
}
|
|
1646
|
+
return sibling;
|
|
972
1647
|
}
|
|
973
1648
|
function copyDirSync(src, dest) {
|
|
974
1649
|
if (!existsSync(src)) return 0;
|
|
975
1650
|
mkdirSync(dest, { recursive: true });
|
|
976
1651
|
let count = 0;
|
|
977
1652
|
for (const entry of readdirSync(src)) {
|
|
978
|
-
const srcPath =
|
|
979
|
-
const destPath =
|
|
1653
|
+
const srcPath = join3(src, entry);
|
|
1654
|
+
const destPath = join3(dest, entry);
|
|
980
1655
|
if (statSync(srcPath).isDirectory()) {
|
|
981
1656
|
count += copyDirSync(srcPath, destPath);
|
|
982
1657
|
} else {
|
|
@@ -1054,9 +1729,11 @@ var MCP_TOOL_NAMES = [
|
|
|
1054
1729
|
"create_midi_item",
|
|
1055
1730
|
"list_midi_items",
|
|
1056
1731
|
"get_midi_notes",
|
|
1732
|
+
"analyze_midi",
|
|
1057
1733
|
"insert_midi_note",
|
|
1058
1734
|
"insert_midi_notes",
|
|
1059
1735
|
"edit_midi_note",
|
|
1736
|
+
"edit_midi_notes",
|
|
1060
1737
|
"delete_midi_note",
|
|
1061
1738
|
"get_midi_cc",
|
|
1062
1739
|
"insert_midi_cc",
|
|
@@ -1067,18 +1744,45 @@ var MCP_TOOL_NAMES = [
|
|
|
1067
1744
|
"list_media_items",
|
|
1068
1745
|
"get_media_item_properties",
|
|
1069
1746
|
"set_media_item_properties",
|
|
1747
|
+
"set_media_items_properties",
|
|
1070
1748
|
"split_media_item",
|
|
1071
1749
|
"delete_media_item",
|
|
1072
1750
|
"move_media_item",
|
|
1073
1751
|
"trim_media_item",
|
|
1074
1752
|
"add_stretch_marker",
|
|
1075
1753
|
"get_stretch_markers",
|
|
1076
|
-
"delete_stretch_marker"
|
|
1754
|
+
"delete_stretch_marker",
|
|
1755
|
+
// selection & navigation
|
|
1756
|
+
"get_selected_tracks",
|
|
1757
|
+
"get_time_selection",
|
|
1758
|
+
"set_time_selection",
|
|
1759
|
+
// markers & regions
|
|
1760
|
+
"list_markers",
|
|
1761
|
+
"list_regions",
|
|
1762
|
+
"add_marker",
|
|
1763
|
+
"add_region",
|
|
1764
|
+
"delete_marker",
|
|
1765
|
+
"delete_region",
|
|
1766
|
+
// tempo map
|
|
1767
|
+
"get_tempo_map",
|
|
1768
|
+
// fx enable/offline
|
|
1769
|
+
"set_fx_enabled",
|
|
1770
|
+
"set_fx_offline",
|
|
1771
|
+
// envelopes
|
|
1772
|
+
"get_track_envelopes",
|
|
1773
|
+
"get_envelope_points",
|
|
1774
|
+
"insert_envelope_point",
|
|
1775
|
+
"insert_envelope_points",
|
|
1776
|
+
"delete_envelope_point",
|
|
1777
|
+
"create_track_envelope",
|
|
1778
|
+
"set_envelope_properties",
|
|
1779
|
+
"clear_envelope",
|
|
1780
|
+
"remove_envelope_points"
|
|
1077
1781
|
];
|
|
1078
1782
|
function ensureClaudeSettings(settingsPath) {
|
|
1079
1783
|
const allowList = MCP_TOOL_NAMES.map((t) => `mcp__reaper__${t}`);
|
|
1080
1784
|
if (!existsSync(settingsPath)) {
|
|
1081
|
-
mkdirSync(
|
|
1785
|
+
mkdirSync(join3(settingsPath, ".."), { recursive: true });
|
|
1082
1786
|
const config = { permissions: { allow: allowList } };
|
|
1083
1787
|
writeFileSync(settingsPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
1084
1788
|
return "created";
|
|
@@ -1095,7 +1799,7 @@ function ensureClaudeSettings(settingsPath) {
|
|
|
1095
1799
|
}
|
|
1096
1800
|
|
|
1097
1801
|
// apps/reaper-mcp-server/src/main.ts
|
|
1098
|
-
var __dirname =
|
|
1802
|
+
var __dirname = dirname2(fileURLToPath2(import.meta.url));
|
|
1099
1803
|
async function setup() {
|
|
1100
1804
|
console.log("REAPER MCP Server \u2014 Setup\n");
|
|
1101
1805
|
const bridgeDir = await ensureBridgeDir();
|
|
@@ -1104,23 +1808,23 @@ async function setup() {
|
|
|
1104
1808
|
const scriptsDir = getReaperScriptsPath();
|
|
1105
1809
|
mkdirSync2(scriptsDir, { recursive: true });
|
|
1106
1810
|
const reaperDir = resolveAssetDir(__dirname, "reaper");
|
|
1107
|
-
const luaSrc =
|
|
1108
|
-
const luaDest =
|
|
1811
|
+
const luaSrc = join4(reaperDir, "mcp_bridge.lua");
|
|
1812
|
+
const luaDest = join4(scriptsDir, "mcp_bridge.lua");
|
|
1109
1813
|
console.log("Installing Lua bridge...");
|
|
1110
1814
|
if (installFile(luaSrc, luaDest)) {
|
|
1111
1815
|
console.log(` Installed: mcp_bridge.lua`);
|
|
1112
1816
|
} else {
|
|
1113
1817
|
console.log(` Not found: ${luaSrc}`);
|
|
1114
1818
|
}
|
|
1115
|
-
const effectsDir = getReaperEffectsPath();
|
|
1819
|
+
const effectsDir = join4(getReaperEffectsPath(), "reaper-mcp");
|
|
1116
1820
|
mkdirSync2(effectsDir, { recursive: true });
|
|
1117
1821
|
console.log("\nInstalling JSFX analyzers...");
|
|
1118
1822
|
for (const jsfx of REAPER_ASSETS) {
|
|
1119
1823
|
if (jsfx === "mcp_bridge.lua") continue;
|
|
1120
|
-
const src =
|
|
1121
|
-
const dest =
|
|
1824
|
+
const src = join4(reaperDir, jsfx);
|
|
1825
|
+
const dest = join4(effectsDir, jsfx);
|
|
1122
1826
|
if (installFile(src, dest)) {
|
|
1123
|
-
console.log(` Installed:
|
|
1827
|
+
console.log(` Installed: reaper-mcp/${jsfx}`);
|
|
1124
1828
|
} else {
|
|
1125
1829
|
console.log(` Not found: ${src}`);
|
|
1126
1830
|
}
|
|
@@ -1133,70 +1837,66 @@ async function setup() {
|
|
|
1133
1837
|
console.log(" 4. Run the script (it will keep running in background via defer loop)");
|
|
1134
1838
|
console.log(" 5. Add reaper-mcp to your Claude Code config (see: npx @mthines/reaper-mcp doctor)");
|
|
1135
1839
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1840
|
+
function parseInstallScope(args) {
|
|
1841
|
+
if (args.includes("--project")) return "project";
|
|
1842
|
+
return "global";
|
|
1843
|
+
}
|
|
1844
|
+
async function installSkills(scope) {
|
|
1845
|
+
console.log(`REAPER MCP \u2014 Install AI Mix Engineer Skills (scope: ${scope})
|
|
1846
|
+
`);
|
|
1847
|
+
const isGlobal = scope === "global";
|
|
1848
|
+
const baseDir = isGlobal ? join4(homedir2(), ".claude") : process.cwd();
|
|
1849
|
+
const claudeDir = isGlobal ? baseDir : join4(baseDir, ".claude");
|
|
1140
1850
|
const knowledgeSrc = resolveAssetDir(__dirname, "knowledge");
|
|
1141
|
-
const knowledgeDest = join3(targetDir, "knowledge");
|
|
1142
1851
|
if (existsSync2(knowledgeSrc)) {
|
|
1143
|
-
const
|
|
1144
|
-
|
|
1852
|
+
const dest = join4(baseDir, "knowledge");
|
|
1853
|
+
const count = copyDirSync(knowledgeSrc, dest);
|
|
1854
|
+
console.log(`Installed knowledge base: ${count} files \u2192 ${dest}`);
|
|
1145
1855
|
} else {
|
|
1146
1856
|
console.log("Knowledge base not found in package. Skipping.");
|
|
1147
1857
|
}
|
|
1148
1858
|
const rulesSrc = resolveAssetDir(__dirname, "claude-rules");
|
|
1149
|
-
const rulesDir = join3(targetDir, ".claude", "rules");
|
|
1150
1859
|
if (existsSync2(rulesSrc)) {
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1860
|
+
const dest = join4(claudeDir, "rules");
|
|
1861
|
+
const count = copyDirSync(rulesSrc, dest);
|
|
1862
|
+
console.log(`Installed Claude rules: ${count} files \u2192 ${dest}`);
|
|
1153
1863
|
} else {
|
|
1154
1864
|
console.log("Claude rules not found in package. Skipping.");
|
|
1155
1865
|
}
|
|
1156
1866
|
const skillsSrc = resolveAssetDir(__dirname, "claude-skills");
|
|
1157
|
-
const skillsDir = join3(targetDir, ".claude", "skills");
|
|
1158
1867
|
if (existsSync2(skillsSrc)) {
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1868
|
+
const dest = join4(claudeDir, "skills");
|
|
1869
|
+
const count = copyDirSync(skillsSrc, dest);
|
|
1870
|
+
console.log(`Installed Claude skills: ${count} files \u2192 ${dest}`);
|
|
1161
1871
|
} else {
|
|
1162
1872
|
console.log("Claude skills not found in package. Skipping.");
|
|
1163
1873
|
}
|
|
1164
1874
|
const agentsSrc = resolveAssetDir(__dirname, "claude-agents");
|
|
1165
|
-
const agentsDir = join3(targetDir, ".claude", "agents");
|
|
1166
1875
|
if (existsSync2(agentsSrc)) {
|
|
1167
|
-
const
|
|
1168
|
-
|
|
1876
|
+
const dest = join4(claudeDir, "agents");
|
|
1877
|
+
const count = copyDirSync(agentsSrc, dest);
|
|
1878
|
+
console.log(`Installed Claude agents: ${count} files \u2192 ${dest}`);
|
|
1169
1879
|
} else {
|
|
1170
1880
|
console.log("Claude agents not found in package. Skipping.");
|
|
1171
1881
|
}
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
console.log(`
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
const localResult = ensureClaudeSettings(localSettingsPath);
|
|
1179
|
-
if (localResult === "created") {
|
|
1180
|
-
console.log(`Created Claude settings: ${localSettingsPath}`);
|
|
1181
|
-
} else if (localResult === "updated") {
|
|
1182
|
-
console.log(`Updated Claude settings with new REAPER tools: ${localSettingsPath}`);
|
|
1882
|
+
const settingsPath = join4(claudeDir, "settings.json");
|
|
1883
|
+
const result = ensureClaudeSettings(settingsPath);
|
|
1884
|
+
if (result === "created") {
|
|
1885
|
+
console.log(`Created Claude settings: ${settingsPath}`);
|
|
1886
|
+
} else if (result === "updated") {
|
|
1887
|
+
console.log(`Updated Claude settings with new REAPER tools: ${settingsPath}`);
|
|
1183
1888
|
} else {
|
|
1184
|
-
console.log(`Claude settings already has all REAPER tools: ${
|
|
1185
|
-
}
|
|
1186
|
-
const globalSettingsPath = join3(globalClaudeDir, "settings.json");
|
|
1187
|
-
const globalResult = ensureClaudeSettings(globalSettingsPath);
|
|
1188
|
-
if (globalResult === "created") {
|
|
1189
|
-
console.log(`Created Claude settings (global): ${globalSettingsPath}`);
|
|
1190
|
-
} else if (globalResult === "updated") {
|
|
1191
|
-
console.log(`Updated Claude settings (global) with new REAPER tools: ${globalSettingsPath}`);
|
|
1889
|
+
console.log(`Claude settings already has all REAPER tools: ${settingsPath}`);
|
|
1192
1890
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1891
|
+
if (!isGlobal) {
|
|
1892
|
+
const mcpJsonPath = join4(baseDir, ".mcp.json");
|
|
1893
|
+
if (createMcpJson(mcpJsonPath)) {
|
|
1894
|
+
console.log(`
|
|
1196
1895
|
Created: ${mcpJsonPath}`);
|
|
1197
|
-
|
|
1198
|
-
|
|
1896
|
+
} else {
|
|
1897
|
+
console.log(`
|
|
1199
1898
|
.mcp.json already exists \u2014 add the reaper server config manually if needed.`);
|
|
1899
|
+
}
|
|
1200
1900
|
}
|
|
1201
1901
|
console.log("\nDone! Claude Code now has mix engineer agents, knowledge, and REAPER MCP tools.");
|
|
1202
1902
|
console.log("All 48 REAPER tools are pre-approved \u2014 agents work autonomously.");
|
|
@@ -1210,20 +1910,27 @@ async function doctor() {
|
|
|
1210
1910
|
if (!bridgeRunning) {
|
|
1211
1911
|
console.log(' \u2192 Run "npx @mthines/reaper-mcp setup" then load mcp_bridge.lua in REAPER');
|
|
1212
1912
|
}
|
|
1213
|
-
const
|
|
1214
|
-
|
|
1913
|
+
const globalClaudeDir = join4(homedir2(), ".claude");
|
|
1914
|
+
const localAgents = existsSync2(join4(process.cwd(), ".claude", "agents"));
|
|
1915
|
+
const globalAgents = existsSync2(join4(globalClaudeDir, "agents"));
|
|
1916
|
+
const agentsExist = localAgents || globalAgents;
|
|
1917
|
+
const agentsLocation = localAgents ? ".claude/agents/" : globalAgents ? "~/.claude/agents/" : "";
|
|
1918
|
+
console.log(`Mix agents: ${agentsExist ? `\u2713 Found (${agentsLocation})` : "\u2717 Not installed"}`);
|
|
1215
1919
|
if (!agentsExist) {
|
|
1216
|
-
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"
|
|
1920
|
+
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"');
|
|
1217
1921
|
}
|
|
1218
|
-
const
|
|
1219
|
-
|
|
1922
|
+
const localKnowledge = existsSync2(join4(process.cwd(), "knowledge"));
|
|
1923
|
+
const globalKnowledge = existsSync2(join4(globalClaudeDir, "knowledge"));
|
|
1924
|
+
const knowledgeExists = localKnowledge || globalKnowledge;
|
|
1925
|
+
const knowledgeLocation = localKnowledge ? "project" : globalKnowledge ? "~/.claude/" : "";
|
|
1926
|
+
console.log(`Knowledge base: ${knowledgeExists ? `\u2713 Found (${knowledgeLocation})` : "\u2717 Not installed"}`);
|
|
1220
1927
|
if (!knowledgeExists) {
|
|
1221
|
-
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"
|
|
1928
|
+
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"');
|
|
1222
1929
|
}
|
|
1223
|
-
const mcpJsonExists = existsSync2(
|
|
1930
|
+
const mcpJsonExists = existsSync2(join4(process.cwd(), ".mcp.json"));
|
|
1224
1931
|
console.log(`MCP config: ${mcpJsonExists ? "\u2713 .mcp.json found" : "\u2717 .mcp.json missing"}`);
|
|
1225
1932
|
if (!mcpJsonExists) {
|
|
1226
|
-
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills" to create .mcp.json');
|
|
1933
|
+
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills --project" to create .mcp.json');
|
|
1227
1934
|
}
|
|
1228
1935
|
console.log('\nTo check SWS Extensions, start REAPER and use the "list_available_fx" tool.');
|
|
1229
1936
|
console.log("SWS provides enhanced plugin discovery and snapshot support.\n");
|
|
@@ -1231,25 +1938,53 @@ async function doctor() {
|
|
|
1231
1938
|
}
|
|
1232
1939
|
async function serve() {
|
|
1233
1940
|
const log = (...args) => console.error("[reaper-mcp]", ...args);
|
|
1941
|
+
await initTelemetry();
|
|
1234
1942
|
log("Starting REAPER MCP Server...");
|
|
1235
|
-
log(`Entry: ${
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1943
|
+
log(`Entry: ${fileURLToPath2(import.meta.url)}`);
|
|
1944
|
+
const tracer = getTracer();
|
|
1945
|
+
await tracer.startActiveSpan("mcp.server.startup", { kind: SpanKind3.INTERNAL }, async (startupSpan) => {
|
|
1946
|
+
try {
|
|
1947
|
+
await ensureBridgeDir();
|
|
1948
|
+
const cleaned = await cleanupStaleFiles();
|
|
1949
|
+
if (cleaned > 0) {
|
|
1950
|
+
startupSpan.setAttribute("mcp.bridge.stale_files_cleaned", cleaned);
|
|
1951
|
+
log(`Cleaned up ${cleaned} stale bridge files`);
|
|
1952
|
+
}
|
|
1953
|
+
const bridgeRunning = await tracer.startActiveSpan("mcp.bridge.connect", { kind: SpanKind3.INTERNAL }, async (bridgeSpan) => {
|
|
1954
|
+
const running = await isBridgeRunning();
|
|
1955
|
+
bridgeSpan.setAttribute("mcp.bridge.connected", running);
|
|
1956
|
+
if (!running) {
|
|
1957
|
+
bridgeSpan.setStatus({
|
|
1958
|
+
code: SpanStatusCode3.UNSET,
|
|
1959
|
+
message: "Lua bridge not detected \u2014 commands will timeout until started"
|
|
1960
|
+
});
|
|
1961
|
+
log("WARNING: Lua bridge does not appear to be running in REAPER.");
|
|
1962
|
+
log("Commands will timeout until the bridge script is started.");
|
|
1963
|
+
log('Run "npx @mthines/reaper-mcp setup" for installation instructions.');
|
|
1964
|
+
} else {
|
|
1965
|
+
bridgeSpan.setStatus({ code: SpanStatusCode3.OK });
|
|
1966
|
+
log("Lua bridge detected \u2014 connected to REAPER");
|
|
1967
|
+
}
|
|
1968
|
+
bridgeSpan.end();
|
|
1969
|
+
return running;
|
|
1970
|
+
});
|
|
1971
|
+
startupSpan.setAttribute("mcp.bridge.connected", bridgeRunning);
|
|
1972
|
+
const server = createServer();
|
|
1973
|
+
const transport = new StdioServerTransport();
|
|
1974
|
+
await server.connect(transport);
|
|
1975
|
+
startupSpan.setStatus({ code: SpanStatusCode3.OK });
|
|
1976
|
+
log("MCP server connected via stdio");
|
|
1977
|
+
} catch (error) {
|
|
1978
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1979
|
+
startupSpan.setStatus({
|
|
1980
|
+
code: SpanStatusCode3.ERROR,
|
|
1981
|
+
message: `${err.name}: ${err.message}`
|
|
1982
|
+
});
|
|
1983
|
+
throw error;
|
|
1984
|
+
} finally {
|
|
1985
|
+
startupSpan.end();
|
|
1986
|
+
}
|
|
1987
|
+
});
|
|
1253
1988
|
}
|
|
1254
1989
|
var command = process.argv[2];
|
|
1255
1990
|
switch (command) {
|
|
@@ -1260,7 +1995,7 @@ switch (command) {
|
|
|
1260
1995
|
});
|
|
1261
1996
|
break;
|
|
1262
1997
|
case "install-skills":
|
|
1263
|
-
installSkills().catch((err) => {
|
|
1998
|
+
installSkills(parseInstallScope(process.argv.slice(3))).catch((err) => {
|
|
1264
1999
|
console.error("Install failed:", err);
|
|
1265
2000
|
process.exit(1);
|
|
1266
2001
|
});
|
|
@@ -1293,14 +2028,16 @@ Usage:
|
|
|
1293
2028
|
npx @mthines/reaper-mcp Start MCP server (stdio mode)
|
|
1294
2029
|
npx @mthines/reaper-mcp serve Start MCP server (stdio mode)
|
|
1295
2030
|
npx @mthines/reaper-mcp setup Install Lua bridge + JSFX analyzers into REAPER
|
|
1296
|
-
npx @mthines/reaper-mcp install-skills Install AI
|
|
2031
|
+
npx @mthines/reaper-mcp install-skills Install AI knowledge + agents globally (default)
|
|
2032
|
+
npx @mthines/reaper-mcp install-skills --project Install into current project directory
|
|
2033
|
+
npx @mthines/reaper-mcp install-skills --global Install into ~/.claude/ (default)
|
|
1297
2034
|
npx @mthines/reaper-mcp doctor Check that everything is configured correctly
|
|
1298
2035
|
npx @mthines/reaper-mcp status Check if Lua bridge is running in REAPER
|
|
1299
2036
|
|
|
1300
2037
|
Quick Start:
|
|
1301
2038
|
1. npx @mthines/reaper-mcp setup # install REAPER components
|
|
1302
2039
|
2. Load mcp_bridge.lua in REAPER (Actions > Load ReaScript > Run)
|
|
1303
|
-
3. npx @mthines/reaper-mcp install-skills # install AI knowledge + agents
|
|
2040
|
+
3. npx @mthines/reaper-mcp install-skills # install AI knowledge + agents (globally)
|
|
1304
2041
|
4. Open Claude Code \u2014 REAPER tools + mix engineer agents are ready
|
|
1305
2042
|
|
|
1306
2043
|
Tip: install globally for shorter commands:
|
|
@@ -1311,10 +2048,18 @@ Tip: install globally for shorter commands:
|
|
|
1311
2048
|
}
|
|
1312
2049
|
process.on("SIGINT", () => {
|
|
1313
2050
|
console.error("[reaper-mcp] Interrupted");
|
|
1314
|
-
process.exit(0);
|
|
2051
|
+
shutdownTelemetry().finally(() => process.exit(0));
|
|
1315
2052
|
});
|
|
1316
2053
|
process.on("SIGTERM", () => {
|
|
1317
2054
|
console.error("[reaper-mcp] Terminated");
|
|
1318
|
-
process.exit(0);
|
|
2055
|
+
shutdownTelemetry().finally(() => process.exit(0));
|
|
2056
|
+
});
|
|
2057
|
+
process.on("uncaughtException", (err) => {
|
|
2058
|
+
console.error("[reaper-mcp] Uncaught exception:", err);
|
|
2059
|
+
shutdownTelemetry().finally(() => process.exit(1));
|
|
2060
|
+
});
|
|
2061
|
+
process.on("unhandledRejection", (reason) => {
|
|
2062
|
+
console.error("[reaper-mcp] Unhandled rejection:", reason);
|
|
2063
|
+
shutdownTelemetry().finally(() => process.exit(1));
|
|
1319
2064
|
});
|
|
1320
2065
|
//# sourceMappingURL=main.js.map
|