@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 +175 -0
- package/package.json +1 -1
- package/reaper/mcp_bridge.lua +33 -1
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
package/reaper/mcp_bridge.lua
CHANGED
|
@@ -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
|
-
|
|
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()
|