@mthines/reaper-mcp 0.9.0 → 0.9.1-beta.12.1

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
@@ -670,13 +670,15 @@ import { z as z7 } from "zod/v4";
670
670
  function registerSnapshotTools(server) {
671
671
  server.tool(
672
672
  "snapshot_save",
673
- "Save the current mixer state as a named snapshot. Uses SWS Snapshots if available, otherwise captures track volumes, pans, mutes, solos, and FX bypass states manually.",
673
+ 'Save the current mixer state as a named snapshot. Stored in .reaper-mcp/snapshots/ alongside the project file (falls back to global bridge dir for unsaved projects). Captures track volumes, pans, mutes, solos, FX bypass/offline states, full FX parameter values, preset names, track names/colors, and send levels. Response includes storageLocation ("project" or "global").',
674
674
  {
675
675
  name: z7.string().min(1).describe('Unique snapshot name (e.g. "before-compression", "v1-mix")'),
676
- description: z7.string().optional().describe("Optional human-readable description")
676
+ description: z7.string().optional().describe("Optional human-readable description"),
677
+ includeFxParams: z7.boolean().optional().default(true).describe("Include FX parameter values in snapshot (default true)"),
678
+ maxParamsPerFx: z7.coerce.number().int().min(1).max(2e3).optional().default(500).describe("Max parameters to capture per FX plugin (default 500)")
677
679
  },
678
- async ({ name, description }) => {
679
- const res = await sendCommand("snapshot_save", { name, description });
680
+ async ({ name, description, includeFxParams, maxParamsPerFx }) => {
681
+ const res = await sendCommand("snapshot_save", { name, description, includeFxParams, maxParamsPerFx });
680
682
  if (!res.success) {
681
683
  return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
682
684
  }
@@ -685,12 +687,14 @@ function registerSnapshotTools(server) {
685
687
  );
686
688
  server.tool(
687
689
  "snapshot_restore",
688
- "Restore a previously saved mixer snapshot by name",
690
+ "Restore a previously saved mixer snapshot by name. Reads from .reaper-mcp/snapshots/ alongside the project file (falls back to global bridge dir for unsaved projects). Restores volumes, pans, mutes, solos, FX states, and optionally FX parameters (if snapshot is v2). FX parameters are only restored when the plugin name matches the snapshot to prevent applying wrong values.",
689
691
  {
690
- name: z7.string().min(1).describe("Name of the snapshot to restore")
692
+ name: z7.string().min(1).describe("Name of the snapshot to restore"),
693
+ restoreTrackMeta: z7.boolean().optional().default(false).describe("Also restore track names and colors (default false)"),
694
+ restoreSendLevels: z7.boolean().optional().default(true).describe("Restore send volume/pan/mute for existing sends (default true)")
691
695
  },
692
- async ({ name }) => {
693
- const res = await sendCommand("snapshot_restore", { name });
696
+ async ({ name, restoreTrackMeta, restoreSendLevels }) => {
697
+ const res = await sendCommand("snapshot_restore", { name, restoreTrackMeta, restoreSendLevels });
694
698
  if (!res.success) {
695
699
  return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
696
700
  }
@@ -699,7 +703,7 @@ function registerSnapshotTools(server) {
699
703
  );
700
704
  server.tool(
701
705
  "snapshot_list",
702
- "List all saved mixer snapshots with names, descriptions, and timestamps",
706
+ 'List all saved mixer snapshots for the current project. Reads from .reaper-mcp/snapshots/ alongside the project file (falls back to global bridge dir for unsaved projects). Response includes storageLocation ("project" or "global").',
703
707
  {},
704
708
  async () => {
705
709
  const res = await sendCommand("snapshot_list", {});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mthines/reaper-mcp",
3
- "version": "0.9.0",
3
+ "version": "0.9.1-beta.12.1",
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",
@@ -795,9 +795,22 @@ end
795
795
  -- =============================================================================
796
796
 
797
797
  local function get_snapshot_dir()
798
+ local proj_path = reaper.GetProjectPath()
799
+ if proj_path and proj_path ~= "" then
800
+ return proj_path .. "/.reaper-mcp/snapshots/"
801
+ end
802
+ -- Fallback for unsaved projects
798
803
  return bridge_dir .. "snapshots/"
799
804
  end
800
805
 
806
+ local function get_snapshot_storage_location()
807
+ local proj_path = reaper.GetProjectPath()
808
+ if proj_path and proj_path ~= "" then
809
+ return "project"
810
+ end
811
+ return "global"
812
+ end
813
+
801
814
  local function ensure_snapshot_dir()
802
815
  reaper.RecursiveCreateDirectory(get_snapshot_dir(), 0)
803
816
  end
@@ -808,9 +821,13 @@ local function snapshot_path(name)
808
821
  return get_snapshot_dir() .. safe .. ".json"
809
822
  end
810
823
 
811
- local function capture_mixer_state()
812
- local state = { tracks = {} }
824
+ local function capture_mixer_state(params)
825
+ params = params or {}
826
+ local include_fx_params = params.includeFxParams ~= false -- default true
827
+ local max_params = params.maxParamsPerFx or 500
828
+ local state = { version = 2, tracks = {} }
813
829
  local count = reaper.CountTracks(0)
830
+
814
831
  for i = 0, count - 1 do
815
832
  local track = reaper.GetTrack(0, i)
816
833
  local _, name = reaper.GetTrackName(track)
@@ -818,22 +835,75 @@ local function capture_mixer_state()
818
835
  local pan = reaper.GetMediaTrackInfo_Value(track, "D_PAN")
819
836
  local mute = reaper.GetMediaTrackInfo_Value(track, "B_MUTE")
820
837
  local solo = reaper.GetMediaTrackInfo_Value(track, "I_SOLO")
838
+ local color = reaper.GetMediaTrackInfo_Value(track, "I_CUSTOMCOLOR")
821
839
 
822
- -- Capture FX bypass states
840
+ -- FX state with full parameter capture
823
841
  local fx_count = reaper.TrackFX_GetCount(track)
824
842
  local fx_states = {}
825
843
  for j = 0, fx_count - 1 do
826
- fx_states[#fx_states + 1] = reaper.TrackFX_GetEnabled(track, j)
844
+ local enabled = reaper.TrackFX_GetEnabled(track, j)
845
+ local offline = reaper.TrackFX_GetOffline(track, j)
846
+ local _, preset = reaper.TrackFX_GetPreset(track, j)
847
+ local _, fx_name = reaper.TrackFX_GetFXName(track, j)
848
+
849
+ local params_data = {}
850
+ if include_fx_params then
851
+ local param_count = reaper.TrackFX_GetNumParams(track, j)
852
+ local limit = math.min(param_count, max_params)
853
+ for p = 0, limit - 1 do
854
+ local val = reaper.TrackFX_GetParam(track, j, p)
855
+ params_data[p + 1] = val
856
+ end
857
+ end
858
+
859
+ fx_states[j + 1] = {
860
+ name = fx_name,
861
+ enabled = enabled,
862
+ offline = offline,
863
+ preset = preset or "",
864
+ params = params_data,
865
+ }
827
866
  end
828
867
 
829
- state.tracks[#state.tracks + 1] = {
868
+ -- Send levels (volume, pan, mute for existing sends)
869
+ local send_count = reaper.GetTrackNumSends(track, 0)
870
+ local sends = {}
871
+ for s = 0, send_count - 1 do
872
+ local dest_track = reaper.GetTrackSendInfo_Value(track, 0, s, "P_DESTTRACK")
873
+ local dest_idx = -1
874
+ local dest_name = ""
875
+ if dest_track then
876
+ dest_idx = reaper.GetMediaTrackInfo_Value(dest_track, "IP_TRACKNUMBER") - 1
877
+ local _, dname = reaper.GetTrackName(dest_track)
878
+ dest_name = dname or ""
879
+ end
880
+ sends[s + 1] = {
881
+ destTrackIndex = dest_idx,
882
+ destTrackName = dest_name,
883
+ volume = reaper.GetTrackSendInfo_Value(track, 0, s, "D_VOL"),
884
+ pan = reaper.GetTrackSendInfo_Value(track, 0, s, "D_PAN"),
885
+ muted = reaper.GetTrackSendInfo_Value(track, 0, s, "B_MUTE") ~= 0,
886
+ }
887
+ end
888
+
889
+ state.tracks[i + 1] = {
830
890
  index = i,
831
891
  name = name,
892
+ color = color,
832
893
  volume = vol,
833
894
  pan = pan,
834
895
  mute = mute ~= 0,
835
896
  solo = solo ~= 0,
836
- fxEnabled = fx_states,
897
+ fx = fx_states,
898
+ sends = sends,
899
+ -- Legacy compat: flat fxEnabled array (derived from fx_states)
900
+ fxEnabled = (function()
901
+ local arr = {}
902
+ for j = 1, #fx_states do
903
+ arr[j] = fx_states[j].enabled
904
+ end
905
+ return arr
906
+ end)(),
837
907
  }
838
908
  end
839
909
  return state
@@ -852,7 +922,10 @@ function handlers.snapshot_save(params)
852
922
  name = name,
853
923
  description = params.description or "",
854
924
  timestamp = timestamp,
855
- mixerState = capture_mixer_state(),
925
+ mixerState = capture_mixer_state({
926
+ includeFxParams = params.includeFxParams ~= false,
927
+ maxParamsPerFx = params.maxParamsPerFx or 500,
928
+ }),
856
929
  }
857
930
 
858
931
  local path = snapshot_path(name)
@@ -861,7 +934,7 @@ function handlers.snapshot_save(params)
861
934
  return nil, "Failed to write snapshot file: " .. path
862
935
  end
863
936
 
864
- return { success = true, name = name, timestamp = timestamp, path = path }
937
+ return { success = true, name = name, timestamp = timestamp, path = path, storageLocation = get_snapshot_storage_location() }
865
938
  end
866
939
 
867
940
  function handlers.snapshot_restore(params)
@@ -881,41 +954,107 @@ function handlers.snapshot_restore(params)
881
954
  return nil, "Invalid snapshot file for: " .. name
882
955
  end
883
956
 
884
- -- Restore each track state
885
- local restored = 0
886
957
  local state = snapshot.mixerState
958
+ local version = state.version or 1
959
+ local restore_meta = params.restoreTrackMeta == true -- default false
960
+ local restore_sends = params.restoreSendLevels ~= false -- default true
961
+ local warnings = {}
962
+
963
+ reaper.Undo_BeginBlock()
964
+
965
+ local restored = 0
887
966
  if state.tracks then
888
967
  for _, track_state in ipairs(state.tracks) do
889
968
  local track = reaper.GetTrack(0, track_state.index)
890
969
  if track then
970
+ -- Basic mixer state (all versions)
891
971
  reaper.SetMediaTrackInfo_Value(track, "D_VOL", track_state.volume)
892
972
  reaper.SetMediaTrackInfo_Value(track, "D_PAN", track_state.pan)
893
973
  reaper.SetMediaTrackInfo_Value(track, "B_MUTE", track_state.mute and 1 or 0)
894
974
  reaper.SetMediaTrackInfo_Value(track, "I_SOLO", track_state.solo and 1 or 0)
895
975
 
896
- -- Restore FX bypass states
897
- if track_state.fxEnabled then
976
+ -- Track metadata (v2, opt-in)
977
+ if restore_meta and version >= 2 then
978
+ if track_state.name then
979
+ reaper.GetSetMediaTrackInfo_String(track, "P_NAME", track_state.name, true)
980
+ end
981
+ if track_state.color and track_state.color ~= 0 then
982
+ reaper.SetMediaTrackInfo_Value(track, "I_CUSTOMCOLOR", track_state.color)
983
+ end
984
+ end
985
+
986
+ -- FX state (v2: full params; v1: bypass only)
987
+ if version >= 2 and track_state.fx then
988
+ for j, fx_state in ipairs(track_state.fx) do
989
+ local fx_idx = j - 1
990
+ if fx_idx < reaper.TrackFX_GetCount(track) then
991
+ -- Restore enabled/offline state
992
+ reaper.TrackFX_SetEnabled(track, fx_idx, fx_state.enabled)
993
+ if fx_state.offline ~= nil then
994
+ reaper.TrackFX_SetOffline(track, fx_idx, fx_state.offline)
995
+ end
996
+
997
+ -- Restore FX parameters only if plugin name matches
998
+ if fx_state.params and #fx_state.params > 0 then
999
+ local _, current_name = reaper.TrackFX_GetFXName(track, fx_idx)
1000
+ if current_name == fx_state.name then
1001
+ for p, val in ipairs(fx_state.params) do
1002
+ reaper.TrackFX_SetParam(track, fx_idx, p - 1, val)
1003
+ end
1004
+ else
1005
+ warnings[#warnings + 1] = "Track " .. track_state.index .. " FX " .. fx_idx .. ": name mismatch ('" .. (current_name or "?") .. "' vs '" .. (fx_state.name or "?") .. "'), skipped params"
1006
+ end
1007
+ end
1008
+ end
1009
+ end
1010
+ elseif track_state.fxEnabled then
1011
+ -- Legacy v1: bypass state only
898
1012
  for j, enabled in ipairs(track_state.fxEnabled) do
899
- local fx_idx = j - 1 -- convert to 0-based
1013
+ local fx_idx = j - 1
900
1014
  if fx_idx < reaper.TrackFX_GetCount(track) then
901
1015
  reaper.TrackFX_SetEnabled(track, fx_idx, enabled)
902
1016
  end
903
1017
  end
904
1018
  end
1019
+
1020
+ -- Send levels (v2, default on)
1021
+ if version >= 2 and restore_sends and track_state.sends then
1022
+ local send_count = reaper.GetTrackNumSends(track, 0)
1023
+ for _, saved_send in ipairs(track_state.sends) do
1024
+ for s = 0, send_count - 1 do
1025
+ local dest = reaper.GetTrackSendInfo_Value(track, 0, s, "P_DESTTRACK")
1026
+ if dest then
1027
+ local dest_idx = reaper.GetMediaTrackInfo_Value(dest, "IP_TRACKNUMBER") - 1
1028
+ if dest_idx == saved_send.destTrackIndex then
1029
+ reaper.SetTrackSendInfo_Value(track, 0, s, "D_VOL", saved_send.volume)
1030
+ reaper.SetTrackSendInfo_Value(track, 0, s, "D_PAN", saved_send.pan)
1031
+ reaper.SetTrackSendInfo_Value(track, 0, s, "B_MUTE", saved_send.muted and 1 or 0)
1032
+ break
1033
+ end
1034
+ end
1035
+ end
1036
+ end
1037
+ end
1038
+
905
1039
  restored = restored + 1
906
1040
  end
907
1041
  end
908
1042
  end
909
1043
 
1044
+ reaper.Undo_EndBlock("MCP: Restore snapshot '" .. name .. "'", -1)
1045
+
910
1046
  reaper.TrackList_AdjustWindows(false)
911
1047
  reaper.UpdateArrange()
912
1048
 
913
- return {
1049
+ local result = {
914
1050
  success = true,
915
1051
  name = name,
916
1052
  timestamp = snapshot.timestamp,
917
1053
  tracksRestored = restored,
1054
+ version = version,
918
1055
  }
1056
+ if #warnings > 0 then result.warnings = warnings end
1057
+ return result
919
1058
  end
920
1059
 
921
1060
  function handlers.snapshot_list(params)
@@ -946,7 +1085,7 @@ function handlers.snapshot_list(params)
946
1085
  -- Sort by timestamp descending (newest first)
947
1086
  table.sort(snapshots, function(a, b) return a.timestamp > b.timestamp end)
948
1087
 
949
- return { snapshots = snapshots, total = #snapshots }
1088
+ return { snapshots = snapshots, total = #snapshots, storageLocation = get_snapshot_storage_location() }
950
1089
  end
951
1090
 
952
1091
  -- =============================================================================