@mthines/reaper-mcp 0.6.0-beta.3.1 → 0.6.0-beta.3.2

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.6.0-beta.3.1",
3
+ "version": "0.6.0-beta.3.2",
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",
@@ -58,6 +58,30 @@ local function json_decode(str)
58
58
  return obj
59
59
  end
60
60
 
61
+ -- Fallback JSON array-of-objects parser for when CF_Json_Parse is unavailable.
62
+ -- Handles: [{"key":val,...}, {"key":val,...}, ...]
63
+ local function json_decode_array(str)
64
+ if reaper.CF_Json_Parse then
65
+ local ok, val = reaper.CF_Json_Parse(str)
66
+ if ok then return val end
67
+ end
68
+
69
+ -- Fallback: extract each {...} from the array and parse individually
70
+ local arr = {}
71
+ for obj_str in str:gmatch("%b{}") do
72
+ local obj = {}
73
+ for k, v in obj_str:gmatch('"([^"]+)"%s*:%s*"([^"]*)"') do
74
+ obj[k] = v
75
+ end
76
+ for k, v in obj_str:gmatch('"([^"]+)"%s*:%s*(-?%d+%.?%d*)') do
77
+ if not obj[k] then obj[k] = tonumber(v) end
78
+ end
79
+ arr[#arr + 1] = obj
80
+ end
81
+ if #arr > 0 then return arr end
82
+ return nil
83
+ end
84
+
61
85
  local function json_encode(obj)
62
86
  -- Simple JSON encoder for our response objects
63
87
  local parts = {}
@@ -1092,12 +1116,12 @@ function handlers.create_midi_item(params)
1092
1116
  local track = reaper.GetTrack(0, track_idx)
1093
1117
  if not track then return nil, "Track " .. track_idx .. " not found" end
1094
1118
 
1095
- reaper.Undo_BeginBlock()
1096
1119
  local item = reaper.CreateNewMIDIItemInProj(track, start_pos, end_pos)
1097
- reaper.Undo_EndBlock("MCP: Create MIDI item", -1)
1098
-
1099
1120
  if not item then return nil, "Failed to create MIDI item" end
1100
1121
 
1122
+ reaper.Undo_BeginBlock()
1123
+ reaper.Undo_EndBlock("MCP: Create MIDI item", -1)
1124
+
1101
1125
  -- Find the index of the new item on the track
1102
1126
  local item_count = reaper.CountTrackMediaItems(track)
1103
1127
  local new_idx = -1
@@ -1213,17 +1237,10 @@ function handlers.insert_midi_notes(params)
1213
1237
  local notes_str = params.notes
1214
1238
  if not notes_str or notes_str == "" then return nil, "notes JSON string required" end
1215
1239
 
1216
- -- Parse notes JSON array
1217
- local notes_data = json_decode(notes_str)
1218
- if not notes_data then return nil, "Failed to parse notes JSON" end
1219
-
1220
- -- Handle both array-style and object-style parsed data
1221
- local notes_list = {}
1222
- if notes_data[1] then
1223
- notes_list = notes_data
1224
- else
1225
- -- Try to extract from numbered keys (fallback parser)
1226
- return nil, "Notes must be a JSON array. Ensure REAPER 7+ with CF_Json_Parse for array support."
1240
+ -- Parse notes JSON array (uses dedicated array parser with fallback)
1241
+ local notes_list = json_decode_array(notes_str)
1242
+ if not notes_list or #notes_list == 0 then
1243
+ return nil, "Failed to parse notes JSON array. Expected: [{\"pitch\":60,\"velocity\":100,\"startPosition\":0,\"duration\":1}, ...]"
1227
1244
  end
1228
1245
 
1229
1246
  reaper.Undo_BeginBlock()
@@ -1318,7 +1335,6 @@ function handlers.delete_midi_note(params)
1318
1335
 
1319
1336
  reaper.Undo_BeginBlock()
1320
1337
  reaper.MIDI_DeleteNote(take, note_idx)
1321
- reaper.MIDI_Sort(take)
1322
1338
  reaper.Undo_EndBlock("MCP: Delete MIDI note " .. note_idx, -1)
1323
1339
 
1324
1340
  reaper.UpdateArrange()
@@ -1400,7 +1416,6 @@ function handlers.delete_midi_cc(params)
1400
1416
 
1401
1417
  reaper.Undo_BeginBlock()
1402
1418
  reaper.MIDI_DeleteCC(take, cc_idx)
1403
- reaper.MIDI_Sort(take)
1404
1419
  reaper.Undo_EndBlock("MCP: Delete MIDI CC " .. cc_idx, -1)
1405
1420
 
1406
1421
  reaper.UpdateArrange()
@@ -1514,7 +1529,8 @@ function handlers.get_media_item_properties(params)
1514
1529
  start_offset = reaper.GetMediaItemTakeInfo_Value(take, "D_STARTOFFS")
1515
1530
  local source = reaper.GetMediaItemTake_Source(take)
1516
1531
  if source then
1517
- source_file = reaper.GetMediaSourceFileName(source) or ""
1532
+ local _, src_fn = reaper.GetMediaSourceFileName(source, "")
1533
+ source_file = src_fn or ""
1518
1534
  end
1519
1535
  end
1520
1536
 
@@ -1634,21 +1650,26 @@ function handlers.move_media_item(params)
1634
1650
  local track, item, err = get_media_item(params)
1635
1651
  if err then return nil, err end
1636
1652
 
1637
- reaper.Undo_BeginBlock()
1638
-
1639
- if params.newPosition ~= nil then
1640
- reaper.SetMediaItemInfo_Value(item, "D_POSITION", params.newPosition)
1641
- end
1642
-
1653
+ -- Validate destination track before starting undo block
1643
1654
  if params.newTrackIndex ~= nil then
1644
1655
  local dest_track = reaper.GetTrack(0, params.newTrackIndex)
1645
1656
  if not dest_track then
1646
- reaper.Undo_EndBlock("MCP: Move media item (failed)", -1)
1647
1657
  return nil, "Destination track " .. params.newTrackIndex .. " not found"
1648
1658
  end
1659
+ end
1660
+
1661
+ reaper.Undo_BeginBlock()
1662
+
1663
+ -- Move track first, then set position (MoveMediaItemToTrack preserves position)
1664
+ if params.newTrackIndex ~= nil then
1665
+ local dest_track = reaper.GetTrack(0, params.newTrackIndex)
1649
1666
  reaper.MoveMediaItemToTrack(item, dest_track)
1650
1667
  end
1651
1668
 
1669
+ if params.newPosition ~= nil then
1670
+ reaper.SetMediaItemInfo_Value(item, "D_POSITION", params.newPosition)
1671
+ end
1672
+
1652
1673
  reaper.Undo_EndBlock("MCP: Move media item", -1)
1653
1674
  reaper.UpdateArrange()
1654
1675
 
@@ -1663,39 +1684,33 @@ function handlers.trim_media_item(params)
1663
1684
  local track, item, err = get_media_item(params)
1664
1685
  if err then return nil, err end
1665
1686
 
1666
- reaper.Undo_BeginBlock()
1667
-
1668
1687
  local pos = reaper.GetMediaItemInfo_Value(item, "D_POSITION")
1669
1688
  local len = reaper.GetMediaItemInfo_Value(item, "D_LENGTH")
1689
+
1690
+ -- Validate both trims upfront before applying either
1691
+ local trim_start = (params.trimStart ~= nil and params.trimStart ~= 0) and params.trimStart or 0
1692
+ local trim_end = (params.trimEnd ~= nil and params.trimEnd ~= 0) and params.trimEnd or 0
1693
+ local new_len = len - trim_start - trim_end
1694
+ if new_len <= 0 then
1695
+ return nil, "Trim would result in zero or negative length (current: " .. len .. "s, trimStart: " .. trim_start .. "s, trimEnd: " .. trim_end .. "s)"
1696
+ end
1697
+
1698
+ reaper.Undo_BeginBlock()
1699
+
1670
1700
  local take = reaper.GetActiveTake(item)
1671
1701
 
1672
- if params.trimStart ~= nil and params.trimStart ~= 0 then
1673
- local new_pos = pos + params.trimStart
1674
- local new_len = len - params.trimStart
1675
- if new_len <= 0 then
1676
- reaper.Undo_EndBlock("MCP: Trim media item (failed)", -1)
1677
- return nil, "trimStart would result in zero or negative length"
1678
- end
1679
- reaper.SetMediaItemInfo_Value(item, "D_POSITION", new_pos)
1680
- reaper.SetMediaItemInfo_Value(item, "D_LENGTH", new_len)
1702
+ if trim_start ~= 0 then
1703
+ pos = pos + trim_start
1704
+ reaper.SetMediaItemInfo_Value(item, "D_POSITION", pos)
1681
1705
  -- Adjust take start offset
1682
1706
  if take then
1683
1707
  local offset = reaper.GetMediaItemTakeInfo_Value(take, "D_STARTOFFS")
1684
- reaper.SetMediaItemTakeInfo_Value(take, "D_STARTOFFS", offset + params.trimStart)
1708
+ reaper.SetMediaItemTakeInfo_Value(take, "D_STARTOFFS", offset + trim_start)
1685
1709
  end
1686
- pos = new_pos
1687
- len = new_len
1688
1710
  end
1689
1711
 
1690
- if params.trimEnd ~= nil and params.trimEnd ~= 0 then
1691
- local new_len = len - params.trimEnd
1692
- if new_len <= 0 then
1693
- reaper.Undo_EndBlock("MCP: Trim media item (failed)", -1)
1694
- return nil, "trimEnd would result in zero or negative length"
1695
- end
1696
- reaper.SetMediaItemInfo_Value(item, "D_LENGTH", new_len)
1697
- len = new_len
1698
- end
1712
+ reaper.SetMediaItemInfo_Value(item, "D_LENGTH", new_len)
1713
+ len = new_len
1699
1714
 
1700
1715
  reaper.Undo_EndBlock("MCP: Trim media item", -1)
1701
1716
  reaper.UpdateArrange()