@mthines/reaper-mcp 0.10.0 → 0.11.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
@@ -131,6 +131,91 @@ function getTimeoutCounter() {
131
131
  }
132
132
  return _timeoutCounter;
133
133
  }
134
+ var _handlerDurationHistogram = null;
135
+ var _pickupDurationHistogram = null;
136
+ function getHandlerDurationHistogram() {
137
+ if (!_handlerDurationHistogram) {
138
+ _handlerDurationHistogram = getMeter().createHistogram(
139
+ "mcp.bridge.handler.duration",
140
+ {
141
+ description: "Time Lua spent executing the command handler inside REAPER",
142
+ unit: "ms",
143
+ advice: {
144
+ explicitBucketBoundaries: [
145
+ 0.1,
146
+ 0.25,
147
+ 0.5,
148
+ 1,
149
+ 2,
150
+ 5,
151
+ 10,
152
+ 25,
153
+ 50,
154
+ 100,
155
+ 250,
156
+ 500
157
+ ]
158
+ }
159
+ }
160
+ );
161
+ }
162
+ return _handlerDurationHistogram;
163
+ }
164
+ function getPickupDurationHistogram() {
165
+ if (!_pickupDurationHistogram) {
166
+ _pickupDurationHistogram = getMeter().createHistogram(
167
+ "mcp.bridge.pickup.duration",
168
+ {
169
+ description: "Time from Node writing the command file to Lua picking it up (defer cycle latency)",
170
+ unit: "ms",
171
+ advice: {
172
+ explicitBucketBoundaries: [
173
+ 1,
174
+ 2,
175
+ 5,
176
+ 10,
177
+ 16,
178
+ 33,
179
+ 50,
180
+ 100,
181
+ 200,
182
+ 500,
183
+ 1e3
184
+ ]
185
+ }
186
+ }
187
+ );
188
+ }
189
+ return _pickupDurationHistogram;
190
+ }
191
+ var _deferP50Ms = 0;
192
+ var _deferP95Ms = 0;
193
+ var _scanAvgMs = 0;
194
+ var _scanMaxMs = 0;
195
+ function updateDeferStats(p50, p95) {
196
+ _deferP50Ms = p50;
197
+ _deferP95Ms = p95;
198
+ }
199
+ function updateScanStats(avg, max) {
200
+ _scanAvgMs = avg;
201
+ _scanMaxMs = max;
202
+ }
203
+ function registerBridgeGauges() {
204
+ getMeter().createObservableGauge("mcp.bridge.defer.interval", {
205
+ description: "REAPER defer loop interval (p50/p95 over last 100 cycles)",
206
+ unit: "ms"
207
+ }).addCallback((obs) => {
208
+ obs.observe(_deferP50Ms, { percentile: "p50" });
209
+ obs.observe(_deferP95Ms, { percentile: "p95" });
210
+ });
211
+ getMeter().createObservableGauge("mcp.bridge.scan.duration", {
212
+ description: "Time Lua spends per main_loop scan cycle (avg/max over last 100 scans)",
213
+ unit: "ms"
214
+ }).addCallback((obs) => {
215
+ obs.observe(_scanAvgMs, { percentile: "avg" });
216
+ obs.observe(_scanMaxMs, { percentile: "max" });
217
+ });
218
+ }
134
219
 
135
220
  // apps/reaper-mcp-server/src/bridge.ts
136
221
  import { randomUUID } from "node:crypto";
@@ -224,9 +309,29 @@ async function sendCommand(type, params = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
224
309
  const tCleanup = now();
225
310
  await Promise.allSettled([unlink(commandPath), unlink(responsePath)]);
226
311
  if (profiling) t["cleanup"] = now() - tCleanup;
312
+ const rawResponse = response;
313
+ const handlerMs = rawResponse["_handlerMs"];
314
+ const pickupMs = rawResponse["_pickupMs"];
315
+ const deferCycle = rawResponse["_deferCycle"];
227
316
  const durationMs2 = Date.now() - startMs;
228
317
  const succeeded = response.success;
229
318
  span.setAttribute("mcp.response.success", succeeded);
319
+ if (handlerMs != null) {
320
+ span.setAttribute("mcp.bridge.handler_ms", handlerMs);
321
+ getHandlerDurationHistogram().record(handlerMs, {
322
+ command_type: type,
323
+ success: String(succeeded)
324
+ });
325
+ }
326
+ if (pickupMs != null) {
327
+ span.setAttribute("mcp.bridge.pickup_ms", pickupMs);
328
+ getPickupDurationHistogram().record(pickupMs, {
329
+ command_type: type
330
+ });
331
+ }
332
+ if (deferCycle != null) {
333
+ span.setAttribute("mcp.bridge.defer_cycle", deferCycle);
334
+ }
230
335
  if (!succeeded) {
231
336
  span.setStatus({ code: SpanStatusCode.ERROR, message: response.error ?? "Bridge error" });
232
337
  span.setAttribute("mcp.response.error", response.error ?? "unknown");
@@ -333,6 +438,69 @@ function getReaperEffectsPath() {
333
438
  function sleep(ms) {
334
439
  return new Promise((resolve) => setTimeout(resolve, ms));
335
440
  }
441
+ async function readBridgeEvents() {
442
+ const eventsPath = join2(getBridgeDir(), "bridge_events.jsonl");
443
+ let content;
444
+ try {
445
+ content = await readFile(eventsPath, "utf-8");
446
+ } catch {
447
+ return;
448
+ }
449
+ await writeFile(eventsPath, "", "utf-8").catch(() => {
450
+ });
451
+ const lines = content.split("\n").filter((l) => l.trim() !== "");
452
+ for (const line of lines) {
453
+ try {
454
+ const evt = JSON.parse(line);
455
+ console.error(JSON.stringify({ msg: "bridge_event", ...evt }));
456
+ } catch {
457
+ }
458
+ }
459
+ }
460
+ var _diagnosticsTimer = null;
461
+ var _eventsTimer = null;
462
+ function startDiagnosticsPoller(intervalMs = 6e4) {
463
+ if (_diagnosticsTimer) return;
464
+ _diagnosticsTimer = setInterval(async () => {
465
+ try {
466
+ const res = await sendCommand(
467
+ "_bridge_diagnostics",
468
+ {},
469
+ 5e3
470
+ );
471
+ if (res.success && res.data) {
472
+ const d = res.data;
473
+ if (d.deferIntervals) {
474
+ updateDeferStats(d.deferIntervals.p50Ms, d.deferIntervals.p95Ms);
475
+ }
476
+ if (d.scanDurations) {
477
+ updateScanStats(d.scanDurations.avgMs, d.scanDurations.maxMs);
478
+ }
479
+ }
480
+ } catch {
481
+ }
482
+ }, intervalMs);
483
+ }
484
+ function startEventsPoller(intervalMs = 3e4) {
485
+ if (_eventsTimer) return;
486
+ readBridgeEvents().catch(() => {
487
+ });
488
+ _eventsTimer = setInterval(
489
+ () => readBridgeEvents().catch(() => {
490
+ }),
491
+ intervalMs
492
+ );
493
+ }
494
+ function stopPollers() {
495
+ if (_diagnosticsTimer) {
496
+ clearInterval(_diagnosticsTimer);
497
+ _diagnosticsTimer = null;
498
+ }
499
+ if (_eventsTimer) {
500
+ clearInterval(_eventsTimer);
501
+ _eventsTimer = null;
502
+ }
503
+ }
336
504
 
337
505
  // apps/reaper-mcp-server/src/tools/project.ts
338
506
  function registerProjectTools(server) {
@@ -2086,6 +2254,9 @@ async function doctor() {
2086
2254
  async function serve() {
2087
2255
  const log = (...args) => console.error("[reaper-mcp]", ...args);
2088
2256
  await initTelemetry();
2257
+ registerBridgeGauges();
2258
+ startDiagnosticsPoller();
2259
+ startEventsPoller();
2089
2260
  log("Starting REAPER MCP Server...");
2090
2261
  log(`Entry: ${fileURLToPath2(import.meta.url)}`);
2091
2262
  const tracer = getTracer();
@@ -2195,18 +2366,22 @@ Tip: install globally for shorter commands:
2195
2366
  }
2196
2367
  process.on("SIGINT", () => {
2197
2368
  console.error("[reaper-mcp] Interrupted");
2369
+ stopPollers();
2198
2370
  shutdownTelemetry().finally(() => process.exit(0));
2199
2371
  });
2200
2372
  process.on("SIGTERM", () => {
2201
2373
  console.error("[reaper-mcp] Terminated");
2374
+ stopPollers();
2202
2375
  shutdownTelemetry().finally(() => process.exit(0));
2203
2376
  });
2204
2377
  process.on("uncaughtException", (err) => {
2205
2378
  console.error("[reaper-mcp] Uncaught exception:", err);
2379
+ stopPollers();
2206
2380
  shutdownTelemetry().finally(() => process.exit(1));
2207
2381
  });
2208
2382
  process.on("unhandledRejection", (reason) => {
2209
2383
  console.error("[reaper-mcp] Unhandled rejection:", reason);
2384
+ stopPollers();
2210
2385
  shutdownTelemetry().finally(() => process.exit(1));
2211
2386
  });
2212
2387
  //# sourceMappingURL=main.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mthines/reaper-mcp",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "description": "MCP server for controlling REAPER DAW — real-time mixing, FX control, and frequency analysis for AI agents",
6
6
  "license": "MIT",
@@ -231,6 +231,22 @@ local function write_file(path, content)
231
231
  return true
232
232
  end
233
233
 
234
+ -- =============================================================================
235
+ -- Telemetry event logging
236
+ -- =============================================================================
237
+
238
+ local EVENTS_LOG = bridge_dir .. "bridge_events.jsonl"
239
+
240
+ local function log_event(event_type, payload)
241
+ local f = io.open(EVENTS_LOG, "a")
242
+ if not f then return end
243
+ payload = payload or {}
244
+ payload.ts = os.time() * 1000
245
+ payload.type = event_type
246
+ f:write(json_encode(payload) .. "\n")
247
+ f:close()
248
+ end
249
+
234
250
  local function file_exists(path)
235
251
  local f = io.open(path, "r")
236
252
  if f then f:close() return true end
@@ -2788,12 +2804,18 @@ local function process_command(filename)
2788
2804
  -- Dispatch to handler
2789
2805
  local handler = handlers[cmd.type]
2790
2806
  local response = {}
2807
+ local handler_ms = 0
2791
2808
 
2792
2809
  if handler then
2810
+ local handler_start = reaper.time_precise()
2793
2811
  local ok, data_or_err, err_msg = pcall(handler, cmd.params or {})
2812
+ handler_ms = (reaper.time_precise() - handler_start) * 1000
2813
+
2794
2814
  if not ok then
2795
2815
  -- pcall caught a Lua runtime error — handler crashed
2796
- response = { id = cmd.id, success = false, error = "Handler error: " .. tostring(data_or_err), timestamp = os.time() * 1000 }
2816
+ local err_str = tostring(data_or_err)
2817
+ log_event("handler_error", { commandType = cmd.type, commandId = cmd.id, error = err_str })
2818
+ response = { id = cmd.id, success = false, error = "Handler error: " .. err_str, timestamp = os.time() * 1000 }
2797
2819
  elseif err_msg then
2798
2820
  -- Handler returned (nil, errorString)
2799
2821
  response = { id = cmd.id, success = false, error = err_msg, timestamp = os.time() * 1000 }
@@ -2802,6 +2824,7 @@ local function process_command(filename)
2802
2824
  response = { id = cmd.id, success = true, data = data_or_err, timestamp = os.time() * 1000 }
2803
2825
  end
2804
2826
  else
2827
+ log_event("handler_unknown", { commandType = cmd.type, commandId = cmd.id })
2805
2828
  response = {
2806
2829
  id = cmd.id,
2807
2830
  success = false,
@@ -2810,6 +2833,9 @@ local function process_command(filename)
2810
2833
  }
2811
2834
  end
2812
2835
 
2836
+ -- Add handler timing to response (rounded to microsecond precision)
2837
+ response._handlerMs = math.floor(handler_ms * 1000 + 0.5) / 1000
2838
+
2813
2839
  -- Add pickup timing to response for profiling
2814
2840
  if cmd.timestamp then
2815
2841
  response._pickupMs = math.floor((pickup_time - (cmd.timestamp / 1000)) * 1000 + 0.5)
@@ -2887,5 +2913,11 @@ reaper.ShowConsoleMsg("MCP Bridge: Polling every " .. (POLL_INTERVAL * 1000) ..
2887
2913
  -- Write initial heartbeat
2888
2914
  write_heartbeat()
2889
2915
 
2916
+ -- Log bridge start event for OTel consumption
2917
+ log_event("bridge_start", { reaperVersion = reaper.GetAppVersion(), bridgeDir = bridge_dir })
2918
+
2919
+ -- Register shutdown handler
2920
+ reaper.atexit(function() log_event("bridge_stop", {}) end)
2921
+
2890
2922
  -- Start the loop
2891
2923
  main_loop()