@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/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 join(home, "Library", "Application Support", "REAPER");
149
+ return join2(home, "Library", "Application Support", "REAPER");
23
150
  case "win32":
24
- return join(process.env["APPDATA"] ?? join(home, "AppData", "Roaming"), "REAPER");
151
+ return join2(process.env["APPDATA"] ?? join2(home, "AppData", "Roaming"), "REAPER");
25
152
  default:
26
- return join(home, ".config", "REAPER");
153
+ return join2(home, ".config", "REAPER");
27
154
  }
28
155
  }
29
156
  function getBridgeDir() {
30
- return join(getReaperResourcePath(), "Scripts", "mcp_bridge_data");
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 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);
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 = join(dir, "heartbeat.json");
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 = join(dir, file);
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 join(getReaperResourcePath(), "Scripts");
289
+ return join2(getReaperResourcePath(), "Scripts");
101
290
  }
102
291
  function getReaperEffectsPath() {
103
- return join(getReaperResourcePath(), "Effects");
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), or solo (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 all MIDI notes in a MIDI item. Returns pitch (0-127, 60=C4), velocity (0-127), position and duration in beats from item start.",
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("get_midi_notes", { trackIndex, itemIndex });
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
- 'Batch insert multiple MIDI notes. Pass a JSON array of notes as a string. Each note: { "pitch": 60, "velocity": 100, "startPosition": 0.0, "duration": 1.0, "channel": 0 }. Positions/durations in beats from item start.',
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.string().describe('JSON array string of notes: [{"pitch":60,"velocity":100,"startPosition":0,"duration":1,"channel":0}, ...]')
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 join3, dirname } from "node:path";
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 join2 } from "node:path";
1632
+ import { join as join3 } from "node:path";
968
1633
  function resolveAssetDir(baseDir, name) {
969
- const sibling = join2(baseDir, name);
1634
+ const sibling = join3(baseDir, name);
970
1635
  if (existsSync(sibling)) return sibling;
971
- return join2(baseDir, "..", name);
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 = join2(src, entry);
979
- const destPath = join2(dest, entry);
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(join2(settingsPath, ".."), { recursive: true });
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 = dirname(fileURLToPath(import.meta.url));
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 = join3(reaperDir, "mcp_bridge.lua");
1108
- const luaDest = join3(scriptsDir, "mcp_bridge.lua");
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 = join3(reaperDir, jsfx);
1121
- const dest = join3(effectsDir, jsfx);
1824
+ const src = join4(reaperDir, jsfx);
1825
+ const dest = join4(effectsDir, jsfx);
1122
1826
  if (installFile(src, dest)) {
1123
- console.log(` Installed: ${jsfx}`);
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
- async function installSkills() {
1137
- console.log("REAPER MCP \u2014 Install AI Mix Engineer Skills\n");
1138
- const targetDir = process.cwd();
1139
- const globalClaudeDir = join3(homedir2(), ".claude");
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 count = copyDirSync(knowledgeSrc, knowledgeDest);
1144
- console.log(`Installed knowledge base: ${count} files \u2192 ${knowledgeDest}`);
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 count = copyDirSync(rulesSrc, rulesDir);
1152
- console.log(`Installed Claude rules: ${count} files \u2192 ${rulesDir}`);
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 count = copyDirSync(skillsSrc, skillsDir);
1160
- console.log(`Installed Claude skills: ${count} files \u2192 ${skillsDir}`);
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 count = copyDirSync(agentsSrc, agentsDir);
1168
- console.log(`Installed Claude agents: ${count} files \u2192 ${agentsDir}`);
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 globalAgentsDir = join3(globalClaudeDir, "agents");
1173
- if (existsSync2(agentsSrc)) {
1174
- const count = copyDirSync(agentsSrc, globalAgentsDir);
1175
- console.log(`Installed Claude agents (global): ${count} files \u2192 ${globalAgentsDir}`);
1176
- }
1177
- const localSettingsPath = join3(targetDir, ".claude", "settings.json");
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: ${localSettingsPath}`);
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
- const mcpJsonPath = join3(targetDir, ".mcp.json");
1194
- if (createMcpJson(mcpJsonPath)) {
1195
- console.log(`
1891
+ if (!isGlobal) {
1892
+ const mcpJsonPath = join4(baseDir, ".mcp.json");
1893
+ if (createMcpJson(mcpJsonPath)) {
1894
+ console.log(`
1196
1895
  Created: ${mcpJsonPath}`);
1197
- } else {
1198
- console.log(`
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 agentsExist = existsSync2(join3(process.cwd(), ".claude", "agents"));
1214
- console.log(`Mix agents: ${agentsExist ? "\u2713 Found (.claude/agents/)" : "\u2717 Not installed"}`);
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" in your project directory');
1920
+ console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"');
1217
1921
  }
1218
- const knowledgeExists = existsSync2(join3(process.cwd(), "knowledge"));
1219
- console.log(`Knowledge base: ${knowledgeExists ? "\u2713 Found in project" : "\u2717 Not installed"}`);
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" in your project directory');
1928
+ console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"');
1222
1929
  }
1223
- const mcpJsonExists = existsSync2(join3(process.cwd(), ".mcp.json"));
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: ${fileURLToPath(import.meta.url)}`);
1236
- await ensureBridgeDir();
1237
- const cleaned = await cleanupStaleFiles();
1238
- if (cleaned > 0) {
1239
- log(`Cleaned up ${cleaned} stale bridge files`);
1240
- }
1241
- const bridgeRunning = await isBridgeRunning();
1242
- if (!bridgeRunning) {
1243
- log("WARNING: Lua bridge does not appear to be running in REAPER.");
1244
- log("Commands will timeout until the bridge script is started.");
1245
- log('Run "npx @mthines/reaper-mcp setup" for installation instructions.');
1246
- } else {
1247
- log("Lua bridge detected \u2014 connected to REAPER");
1248
- }
1249
- const server = createServer();
1250
- const transport = new StdioServerTransport();
1251
- await server.connect(transport);
1252
- log("MCP server connected via stdio");
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 mix engineer knowledge + agents into your project
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