@mthines/reaper-mcp 0.9.1-beta.10.1 → 0.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mthines/reaper-mcp",
3
- "version": "0.9.1-beta.10.1",
3
+ "version": "0.10.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",
@@ -413,6 +413,8 @@ function handlers.set_track_property(params)
413
413
  local track = reaper.GetTrack(0, idx)
414
414
  if not track then return nil, "Track " .. idx .. " not found" end
415
415
 
416
+ reaper.Undo_BeginBlock()
417
+
416
418
  if prop == "volume" then
417
419
  reaper.SetMediaTrackInfo_Value(track, "D_VOL", from_db(value))
418
420
  elseif prop == "pan" then
@@ -422,9 +424,11 @@ function handlers.set_track_property(params)
422
424
  elseif prop == "solo" then
423
425
  reaper.SetMediaTrackInfo_Value(track, "I_SOLO", value)
424
426
  else
427
+ reaper.Undo_EndBlock("MCP: Set track property", -1)
425
428
  return nil, "Unknown property: " .. tostring(prop)
426
429
  end
427
430
 
431
+ reaper.Undo_EndBlock("MCP: Set track " .. prop, -1)
428
432
  return { success = true, trackIndex = idx, property = prop, value = value }
429
433
  end
430
434
 
@@ -439,12 +443,16 @@ function handlers.add_fx(params)
439
443
  if not track then return nil, "Track " .. idx .. " not found" end
440
444
 
441
445
  local position = params.position or -1
446
+
447
+ reaper.Undo_BeginBlock()
442
448
  local fx_idx = reaper.TrackFX_AddByName(track, fx_name, false, position)
443
449
  if fx_idx < 0 then
450
+ reaper.Undo_EndBlock("MCP: Add FX (failed)", -1)
444
451
  return nil, "FX not found: " .. fx_name
445
452
  end
446
453
 
447
454
  local _, actual_name = reaper.TrackFX_GetFXName(track, fx_idx)
455
+ reaper.Undo_EndBlock("MCP: Add FX '" .. (actual_name or fx_name) .. "' to track " .. idx, -1)
448
456
  return {
449
457
  fxIndex = fx_idx,
450
458
  fxName = actual_name,
@@ -462,11 +470,14 @@ function handlers.remove_fx(params)
462
470
  local track = reaper.GetTrack(0, idx)
463
471
  if not track then return nil, "Track " .. idx .. " not found" end
464
472
 
473
+ reaper.Undo_BeginBlock()
465
474
  local ok = reaper.TrackFX_Delete(track, fx_idx)
466
475
  if not ok then
476
+ reaper.Undo_EndBlock("MCP: Remove FX (failed)", -1)
467
477
  return nil, "Failed to remove FX " .. fx_idx .. " from track " .. idx
468
478
  end
469
479
 
480
+ reaper.Undo_EndBlock("MCP: Remove FX " .. fx_idx .. " from track " .. idx, -1)
470
481
  return { success = true, trackIndex = idx, fxIndex = fx_idx }
471
482
  end
472
483
 
@@ -518,11 +529,14 @@ function handlers.set_fx_parameter(params)
518
529
  local track = reaper.GetTrack(0, idx)
519
530
  if not track then return nil, "Track " .. idx .. " not found" end
520
531
 
532
+ reaper.Undo_BeginBlock()
521
533
  local ok = reaper.TrackFX_SetParam(track, fx_idx, param_idx, value)
522
534
  if not ok then
535
+ reaper.Undo_EndBlock("MCP: Set FX parameter (failed)", -1)
523
536
  return nil, "Failed to set param " .. param_idx .. " on FX " .. fx_idx
524
537
  end
525
538
 
539
+ reaper.Undo_EndBlock("MCP: Set FX parameter", -1)
526
540
  return { success = true, trackIndex = idx, fxIndex = fx_idx, paramIndex = param_idx, value = value }
527
541
  end
528
542
 
@@ -749,8 +763,10 @@ function handlers.get_fx_preset_list(params)
749
763
 
750
764
  -- TrackFX_GetPresetIndex returns current index and total count
751
765
  -- We iterate by cycling through presets
766
+ -- Wrap in undo block to prevent intermediate preset changes from polluting undo history
752
767
  local _, total = reaper.TrackFX_GetPresetIndex(track, fx_idx)
753
768
  if total and total > 0 then
769
+ reaper.Undo_BeginBlock()
754
770
  for i = 0, total - 1 do
755
771
  reaper.TrackFX_SetPresetByIndex(track, fx_idx, i)
756
772
  local _, preset_name = reaper.TrackFX_GetPreset(track, fx_idx)
@@ -758,6 +774,7 @@ function handlers.get_fx_preset_list(params)
758
774
  end
759
775
  -- Restore original preset
760
776
  reaper.TrackFX_SetPresetByIndex(track, fx_idx, preset_count)
777
+ reaper.Undo_EndBlock("MCP: Get FX preset list", -1)
761
778
  else
762
779
  -- Plugin doesn't report preset count; return current preset only
763
780
  local _, current_preset = reaper.TrackFX_GetPreset(track, fx_idx)
@@ -781,12 +798,15 @@ function handlers.set_fx_preset(params)
781
798
  if not track then return nil, "Track " .. track_idx .. " not found" end
782
799
 
783
800
  -- TrackFX_SetPreset returns true on success
801
+ reaper.Undo_BeginBlock()
784
802
  local ok = reaper.TrackFX_SetPreset(track, fx_idx, preset_name)
785
803
  if not ok then
804
+ reaper.Undo_EndBlock("MCP: Set FX preset (failed)", -1)
786
805
  return nil, "Preset not found: " .. preset_name
787
806
  end
788
807
 
789
808
  local _, applied = reaper.TrackFX_GetPreset(track, fx_idx)
809
+ reaper.Undo_EndBlock("MCP: Set FX preset '" .. (applied or preset_name) .. "'", -1)
790
810
  return { success = true, trackIndex = track_idx, fxIndex = fx_idx, presetName = applied or preset_name }
791
811
  end
792
812
 
@@ -884,6 +904,9 @@ function handlers.snapshot_restore(params)
884
904
  -- Restore each track state
885
905
  local restored = 0
886
906
  local state = snapshot.mixerState
907
+
908
+ reaper.Undo_BeginBlock()
909
+
887
910
  if state.tracks then
888
911
  for _, track_state in ipairs(state.tracks) do
889
912
  local track = reaper.GetTrack(0, track_state.index)
@@ -907,6 +930,8 @@ function handlers.snapshot_restore(params)
907
930
  end
908
931
  end
909
932
 
933
+ reaper.Undo_EndBlock("MCP: Restore snapshot '" .. name .. "'", -1)
934
+
910
935
  reaper.TrackList_AdjustWindows(false)
911
936
  reaper.UpdateArrange()
912
937
 
@@ -2157,7 +2182,9 @@ function handlers.set_fx_enabled(params)
2157
2182
  if params.fxIndex >= fx_count then
2158
2183
  return nil, "FX index " .. params.fxIndex .. " out of range (track has " .. fx_count .. " FX)"
2159
2184
  end
2185
+ reaper.Undo_BeginBlock()
2160
2186
  reaper.TrackFX_SetEnabled(track, params.fxIndex, params.enabled == 1)
2187
+ reaper.Undo_EndBlock(params.enabled == 1 and "MCP: Enable FX" or "MCP: Disable FX", -1)
2161
2188
  return { trackIndex = params.trackIndex, fxIndex = params.fxIndex, enabled = params.enabled == 1 }
2162
2189
  end
2163
2190
 
@@ -2168,7 +2195,9 @@ function handlers.set_fx_offline(params)
2168
2195
  if params.fxIndex >= fx_count then
2169
2196
  return nil, "FX index " .. params.fxIndex .. " out of range (track has " .. fx_count .. " FX)"
2170
2197
  end
2198
+ reaper.Undo_BeginBlock()
2171
2199
  reaper.TrackFX_SetOffline(track, params.fxIndex, params.offline == 1)
2200
+ reaper.Undo_EndBlock(params.offline == 1 and "MCP: Set FX offline" or "MCP: Set FX online", -1)
2172
2201
  return { trackIndex = params.trackIndex, fxIndex = params.fxIndex, offline = params.offline == 1 }
2173
2202
  end
2174
2203
 
@@ -2201,7 +2230,9 @@ end
2201
2230
  function handlers.set_time_selection(params)
2202
2231
  if params.start == nil then return nil, "start required" end
2203
2232
  if params["end"] == nil then return nil, "end required" end
2233
+ reaper.Undo_BeginBlock()
2204
2234
  reaper.GetSet_LoopTimeRange(true, false, params.start, params["end"], false)
2235
+ reaper.Undo_EndBlock("MCP: Set time selection", -1)
2205
2236
  return { start = params.start, ["end"] = params["end"] }
2206
2237
  end
2207
2238
 
@@ -2251,8 +2282,13 @@ function handlers.add_marker(params)
2251
2282
  if params.position == nil then return nil, "position required" end
2252
2283
  local color = params.color or 0
2253
2284
  local name = params.name or ""
2285
+ reaper.Undo_BeginBlock()
2254
2286
  local idx = reaper.AddProjectMarker2(0, false, params.position, 0, name, -1, color)
2255
- if idx < 0 then return nil, "Failed to add marker" end
2287
+ if idx < 0 then
2288
+ reaper.Undo_EndBlock("MCP: Add marker (failed)", -1)
2289
+ return nil, "Failed to add marker"
2290
+ end
2291
+ reaper.Undo_EndBlock("MCP: Add marker", -1)
2256
2292
  return { index = idx, position = params.position, name = name }
2257
2293
  end
2258
2294
 
@@ -2261,23 +2297,38 @@ function handlers.add_region(params)
2261
2297
  if params["end"] == nil then return nil, "end required" end
2262
2298
  local color = params.color or 0
2263
2299
  local name = params.name or ""
2300
+ reaper.Undo_BeginBlock()
2264
2301
  local idx = reaper.AddProjectMarker2(0, true, params.start, params["end"], name, -1, color)
2265
- if idx < 0 then return nil, "Failed to add region" end
2302
+ if idx < 0 then
2303
+ reaper.Undo_EndBlock("MCP: Add region (failed)", -1)
2304
+ return nil, "Failed to add region"
2305
+ end
2306
+ reaper.Undo_EndBlock("MCP: Add region", -1)
2266
2307
  return { index = idx, start = params.start, ["end"] = params["end"], name = name }
2267
2308
  end
2268
2309
 
2269
2310
  function handlers.delete_marker(params)
2270
2311
  if params.markerIndex == nil then return nil, "markerIndex required" end
2312
+ reaper.Undo_BeginBlock()
2271
2313
  -- DeleteProjectMarker takes (proj, isrgn, markrgnindexnumber)
2272
2314
  local deleted = reaper.DeleteProjectMarker(0, false, params.markerIndex, false)
2273
- if not deleted then return nil, "Marker " .. params.markerIndex .. " not found" end
2315
+ if not deleted then
2316
+ reaper.Undo_EndBlock("MCP: Delete marker (failed)", -1)
2317
+ return nil, "Marker " .. params.markerIndex .. " not found"
2318
+ end
2319
+ reaper.Undo_EndBlock("MCP: Delete marker", -1)
2274
2320
  return { success = true }
2275
2321
  end
2276
2322
 
2277
2323
  function handlers.delete_region(params)
2278
2324
  if params.regionIndex == nil then return nil, "regionIndex required" end
2325
+ reaper.Undo_BeginBlock()
2279
2326
  local deleted = reaper.DeleteProjectMarker(0, true, params.regionIndex, false)
2280
- if not deleted then return nil, "Region " .. params.regionIndex .. " not found" end
2327
+ if not deleted then
2328
+ reaper.Undo_EndBlock("MCP: Delete region (failed)", -1)
2329
+ return nil, "Region " .. params.regionIndex .. " not found"
2330
+ end
2331
+ reaper.Undo_EndBlock("MCP: Delete region", -1)
2281
2332
  return { success = true }
2282
2333
  end
2283
2334
 
@@ -2391,8 +2442,10 @@ function handlers.insert_envelope_point(params)
2391
2442
  local env = reaper.GetTrackEnvelope(track, params.envelopeIndex)
2392
2443
  local shape = params.shape or 0
2393
2444
  local tension = params.tension or 0
2445
+ reaper.Undo_BeginBlock()
2394
2446
  reaper.InsertEnvelopePoint(env, params.time, params.value, shape, tension, false, true)
2395
2447
  reaper.Envelope_SortPoints(env)
2448
+ reaper.Undo_EndBlock("MCP: Insert envelope point", -1)
2396
2449
  local total = reaper.CountEnvelopePoints(env)
2397
2450
  return {
2398
2451
  trackIndex = params.trackIndex,
@@ -2417,7 +2470,9 @@ function handlers.delete_envelope_point(params)
2417
2470
  if params.pointIndex >= total then
2418
2471
  return nil, "Point index " .. params.pointIndex .. " out of range (envelope has " .. total .. " points)"
2419
2472
  end
2473
+ reaper.Undo_BeginBlock()
2420
2474
  reaper.DeleteEnvelopePointRange(env, params.pointIndex, params.pointIndex + 1)
2475
+ reaper.Undo_EndBlock("MCP: Delete envelope point", -1)
2421
2476
  return { success = true, totalPoints = reaper.CountEnvelopePoints(env) }
2422
2477
  end
2423
2478
 
@@ -2425,6 +2480,8 @@ function handlers.create_track_envelope(params)
2425
2480
  local track = reaper.GetTrack(0, params.trackIndex)
2426
2481
  if not track then return nil, "Track " .. params.trackIndex .. " not found" end
2427
2482
 
2483
+ reaper.Undo_BeginBlock()
2484
+
2428
2485
  local env = nil
2429
2486
  local env_name = nil
2430
2487
 
@@ -2506,6 +2563,8 @@ function handlers.create_track_envelope(params)
2506
2563
  end
2507
2564
  end
2508
2565
 
2566
+ reaper.Undo_EndBlock("MCP: Create envelope '" .. (env_name or "") .. "'", -1)
2567
+ reaper.MarkProjectDirty(0)
2509
2568
  reaper.TrackList_AdjustWindows(false)
2510
2569
  reaper.UpdateArrange()
2511
2570
 
@@ -2526,6 +2585,8 @@ function handlers.set_envelope_properties(params)
2526
2585
  end
2527
2586
  local env = reaper.GetTrackEnvelope(track, params.envelopeIndex)
2528
2587
 
2588
+ reaper.Undo_BeginBlock()
2589
+
2529
2590
  if reaper.BR_EnvAlloc then
2530
2591
  -- SWS extension available: use BR_Env* functions
2531
2592
  local br_env = reaper.BR_EnvAlloc(env, false)
@@ -2553,6 +2614,8 @@ function handlers.set_envelope_properties(params)
2553
2614
  reaper.SetEnvelopeStateChunk(env, chunk, false)
2554
2615
  end
2555
2616
 
2617
+ reaper.Undo_EndBlock("MCP: Set envelope properties", -1)
2618
+
2556
2619
  reaper.TrackList_AdjustWindows(false)
2557
2620
  reaper.UpdateArrange()
2558
2621
 
@@ -2584,8 +2647,10 @@ function handlers.clear_envelope(params)
2584
2647
  local env = reaper.GetTrackEnvelope(track, params.envelopeIndex)
2585
2648
  local prev_count = reaper.CountEnvelopePoints(env)
2586
2649
  -- Delete all points by using a range from -infinity to +infinity
2650
+ reaper.Undo_BeginBlock()
2587
2651
  reaper.DeleteEnvelopePointRange(env, -math.huge, math.huge)
2588
2652
  reaper.Envelope_SortPoints(env)
2653
+ reaper.Undo_EndBlock("MCP: Clear envelope", -1)
2589
2654
  return {
2590
2655
  trackIndex = params.trackIndex,
2591
2656
  envelopeIndex = params.envelopeIndex,
@@ -2603,8 +2668,10 @@ function handlers.remove_envelope_points(params)
2603
2668
  end
2604
2669
  local env = reaper.GetTrackEnvelope(track, params.envelopeIndex)
2605
2670
  local prev_count = reaper.CountEnvelopePoints(env)
2671
+ reaper.Undo_BeginBlock()
2606
2672
  reaper.DeleteEnvelopePointRange(env, params.timeStart, params.timeEnd)
2607
2673
  reaper.Envelope_SortPoints(env)
2674
+ reaper.Undo_EndBlock("MCP: Remove envelope points", -1)
2608
2675
  local new_count = reaper.CountEnvelopePoints(env)
2609
2676
  return {
2610
2677
  trackIndex = params.trackIndex,
@@ -2636,6 +2703,7 @@ function handlers.insert_envelope_points(params)
2636
2703
  if not points then return nil, "Failed to parse points JSON" end
2637
2704
 
2638
2705
  local inserted = 0
2706
+ reaper.Undo_BeginBlock()
2639
2707
  for _, pt in ipairs(points) do
2640
2708
  if pt.time and pt.value then
2641
2709
  local shape = pt.shape or 0
@@ -2646,6 +2714,7 @@ function handlers.insert_envelope_points(params)
2646
2714
  end
2647
2715
 
2648
2716
  reaper.Envelope_SortPoints(env)
2717
+ reaper.Undo_EndBlock("MCP: Insert " .. inserted .. " envelope points", -1)
2649
2718
  return {
2650
2719
  trackIndex = params.trackIndex,
2651
2720
  envelopeIndex = params.envelopeIndex,
@@ -2721,11 +2790,16 @@ local function process_command(filename)
2721
2790
  local response = {}
2722
2791
 
2723
2792
  if handler then
2724
- local data, err = handler(cmd.params or {})
2725
- if err then
2726
- response = { id = cmd.id, success = false, error = err, timestamp = os.time() * 1000 }
2793
+ local ok, data_or_err, err_msg = pcall(handler, cmd.params or {})
2794
+ if not ok then
2795
+ -- 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 }
2797
+ elseif err_msg then
2798
+ -- Handler returned (nil, errorString)
2799
+ response = { id = cmd.id, success = false, error = err_msg, timestamp = os.time() * 1000 }
2727
2800
  else
2728
- response = { id = cmd.id, success = true, data = data, timestamp = os.time() * 1000 }
2801
+ -- Handler returned (data, nil) success
2802
+ response = { id = cmd.id, success = true, data = data_or_err, timestamp = os.time() * 1000 }
2729
2803
  end
2730
2804
  else
2731
2805
  response = {