@mthines/reaper-mcp 0.11.0 → 0.12.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
@@ -838,13 +838,15 @@ import { z as z7 } from "zod/v4";
838
838
  function registerSnapshotTools(server) {
839
839
  server.tool(
840
840
  "snapshot_save",
841
- "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.",
841
+ '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").',
842
842
  {
843
843
  name: z7.string().min(1).describe('Unique snapshot name (e.g. "before-compression", "v1-mix")'),
844
- description: z7.string().optional().describe("Optional human-readable description")
844
+ description: z7.string().optional().describe("Optional human-readable description"),
845
+ includeFxParams: z7.boolean().optional().default(true).describe("Include FX parameter values in snapshot (default true)"),
846
+ maxParamsPerFx: z7.coerce.number().int().min(1).max(2e3).optional().default(500).describe("Max parameters to capture per FX plugin (default 500)")
845
847
  },
846
- async ({ name, description }) => {
847
- const res = await sendCommand("snapshot_save", { name, description });
848
+ async ({ name, description, includeFxParams, maxParamsPerFx }) => {
849
+ const res = await sendCommand("snapshot_save", { name, description, includeFxParams, maxParamsPerFx });
848
850
  if (!res.success) {
849
851
  return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
850
852
  }
@@ -853,12 +855,14 @@ function registerSnapshotTools(server) {
853
855
  );
854
856
  server.tool(
855
857
  "snapshot_restore",
856
- "Restore a previously saved mixer snapshot by name",
858
+ "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.",
857
859
  {
858
- name: z7.string().min(1).describe("Name of the snapshot to restore")
860
+ name: z7.string().min(1).describe("Name of the snapshot to restore"),
861
+ restoreTrackMeta: z7.boolean().optional().default(false).describe("Also restore track names and colors (default false)"),
862
+ restoreSendLevels: z7.boolean().optional().default(true).describe("Restore send volume/pan/mute for existing sends (default true)")
859
863
  },
860
- async ({ name }) => {
861
- const res = await sendCommand("snapshot_restore", { name });
864
+ async ({ name, restoreTrackMeta, restoreSendLevels }) => {
865
+ const res = await sendCommand("snapshot_restore", { name, restoreTrackMeta, restoreSendLevels });
862
866
  if (!res.success) {
863
867
  return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
864
868
  }
@@ -867,7 +871,7 @@ function registerSnapshotTools(server) {
867
871
  );
868
872
  server.tool(
869
873
  "snapshot_list",
870
- "List all saved mixer snapshots with names, descriptions, and timestamps",
874
+ '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").',
871
875
  {},
872
876
  async () => {
873
877
  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.11.0",
3
+ "version": "0.12.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",
@@ -831,9 +831,22 @@ end
831
831
  -- =============================================================================
832
832
 
833
833
  local function get_snapshot_dir()
834
+ local proj_path = reaper.GetProjectPath()
835
+ if proj_path and proj_path ~= "" then
836
+ return proj_path .. "/.reaper-mcp/snapshots/"
837
+ end
838
+ -- Fallback for unsaved projects
834
839
  return bridge_dir .. "snapshots/"
835
840
  end
836
841
 
842
+ local function get_snapshot_storage_location()
843
+ local proj_path = reaper.GetProjectPath()
844
+ if proj_path and proj_path ~= "" then
845
+ return "project"
846
+ end
847
+ return "global"
848
+ end
849
+
837
850
  local function ensure_snapshot_dir()
838
851
  reaper.RecursiveCreateDirectory(get_snapshot_dir(), 0)
839
852
  end
@@ -844,9 +857,13 @@ local function snapshot_path(name)
844
857
  return get_snapshot_dir() .. safe .. ".json"
845
858
  end
846
859
 
847
- local function capture_mixer_state()
848
- local state = { tracks = {} }
860
+ local function capture_mixer_state(params)
861
+ params = params or {}
862
+ local include_fx_params = params.includeFxParams ~= false -- default true
863
+ local max_params = params.maxParamsPerFx or 500
864
+ local state = { version = 2, tracks = {} }
849
865
  local count = reaper.CountTracks(0)
866
+
850
867
  for i = 0, count - 1 do
851
868
  local track = reaper.GetTrack(0, i)
852
869
  local _, name = reaper.GetTrackName(track)
@@ -854,22 +871,75 @@ local function capture_mixer_state()
854
871
  local pan = reaper.GetMediaTrackInfo_Value(track, "D_PAN")
855
872
  local mute = reaper.GetMediaTrackInfo_Value(track, "B_MUTE")
856
873
  local solo = reaper.GetMediaTrackInfo_Value(track, "I_SOLO")
874
+ local color = reaper.GetMediaTrackInfo_Value(track, "I_CUSTOMCOLOR")
857
875
 
858
- -- Capture FX bypass states
876
+ -- FX state with full parameter capture
859
877
  local fx_count = reaper.TrackFX_GetCount(track)
860
878
  local fx_states = {}
861
879
  for j = 0, fx_count - 1 do
862
- fx_states[#fx_states + 1] = reaper.TrackFX_GetEnabled(track, j)
880
+ local enabled = reaper.TrackFX_GetEnabled(track, j)
881
+ local offline = reaper.TrackFX_GetOffline(track, j)
882
+ local _, preset = reaper.TrackFX_GetPreset(track, j)
883
+ local _, fx_name = reaper.TrackFX_GetFXName(track, j)
884
+
885
+ local params_data = {}
886
+ if include_fx_params then
887
+ local param_count = reaper.TrackFX_GetNumParams(track, j)
888
+ local limit = math.min(param_count, max_params)
889
+ for p = 0, limit - 1 do
890
+ local val = reaper.TrackFX_GetParam(track, j, p)
891
+ params_data[p + 1] = val
892
+ end
893
+ end
894
+
895
+ fx_states[j + 1] = {
896
+ name = fx_name,
897
+ enabled = enabled,
898
+ offline = offline,
899
+ preset = preset or "",
900
+ params = params_data,
901
+ }
863
902
  end
864
903
 
865
- state.tracks[#state.tracks + 1] = {
904
+ -- Send levels (volume, pan, mute for existing sends)
905
+ local send_count = reaper.GetTrackNumSends(track, 0)
906
+ local sends = {}
907
+ for s = 0, send_count - 1 do
908
+ local dest_track = reaper.GetTrackSendInfo_Value(track, 0, s, "P_DESTTRACK")
909
+ local dest_idx = -1
910
+ local dest_name = ""
911
+ if dest_track then
912
+ dest_idx = reaper.GetMediaTrackInfo_Value(dest_track, "IP_TRACKNUMBER") - 1
913
+ local _, dname = reaper.GetTrackName(dest_track)
914
+ dest_name = dname or ""
915
+ end
916
+ sends[s + 1] = {
917
+ destTrackIndex = dest_idx,
918
+ destTrackName = dest_name,
919
+ volume = reaper.GetTrackSendInfo_Value(track, 0, s, "D_VOL"),
920
+ pan = reaper.GetTrackSendInfo_Value(track, 0, s, "D_PAN"),
921
+ muted = reaper.GetTrackSendInfo_Value(track, 0, s, "B_MUTE") ~= 0,
922
+ }
923
+ end
924
+
925
+ state.tracks[i + 1] = {
866
926
  index = i,
867
927
  name = name,
928
+ color = color,
868
929
  volume = vol,
869
930
  pan = pan,
870
931
  mute = mute ~= 0,
871
932
  solo = solo ~= 0,
872
- fxEnabled = fx_states,
933
+ fx = fx_states,
934
+ sends = sends,
935
+ -- Legacy compat: flat fxEnabled array (derived from fx_states)
936
+ fxEnabled = (function()
937
+ local arr = {}
938
+ for j = 1, #fx_states do
939
+ arr[j] = fx_states[j].enabled
940
+ end
941
+ return arr
942
+ end)(),
873
943
  }
874
944
  end
875
945
  return state
@@ -888,7 +958,10 @@ function handlers.snapshot_save(params)
888
958
  name = name,
889
959
  description = params.description or "",
890
960
  timestamp = timestamp,
891
- mixerState = capture_mixer_state(),
961
+ mixerState = capture_mixer_state({
962
+ includeFxParams = params.includeFxParams ~= false,
963
+ maxParamsPerFx = params.maxParamsPerFx or 500,
964
+ }),
892
965
  }
893
966
 
894
967
  local path = snapshot_path(name)
@@ -897,7 +970,7 @@ function handlers.snapshot_save(params)
897
970
  return nil, "Failed to write snapshot file: " .. path
898
971
  end
899
972
 
900
- return { success = true, name = name, timestamp = timestamp, path = path }
973
+ return { success = true, name = name, timestamp = timestamp, path = path, storageLocation = get_snapshot_storage_location() }
901
974
  end
902
975
 
903
976
  function handlers.snapshot_restore(params)
@@ -917,30 +990,89 @@ function handlers.snapshot_restore(params)
917
990
  return nil, "Invalid snapshot file for: " .. name
918
991
  end
919
992
 
920
- -- Restore each track state
921
- local restored = 0
922
993
  local state = snapshot.mixerState
994
+ local version = state.version or 1
995
+ local restore_meta = params.restoreTrackMeta == true -- default false
996
+ local restore_sends = params.restoreSendLevels ~= false -- default true
997
+ local warnings = {}
923
998
 
924
999
  reaper.Undo_BeginBlock()
925
1000
 
1001
+ local restored = 0
1002
+
926
1003
  if state.tracks then
927
1004
  for _, track_state in ipairs(state.tracks) do
928
1005
  local track = reaper.GetTrack(0, track_state.index)
929
1006
  if track then
1007
+ -- Basic mixer state (all versions)
930
1008
  reaper.SetMediaTrackInfo_Value(track, "D_VOL", track_state.volume)
931
1009
  reaper.SetMediaTrackInfo_Value(track, "D_PAN", track_state.pan)
932
1010
  reaper.SetMediaTrackInfo_Value(track, "B_MUTE", track_state.mute and 1 or 0)
933
1011
  reaper.SetMediaTrackInfo_Value(track, "I_SOLO", track_state.solo and 1 or 0)
934
1012
 
935
- -- Restore FX bypass states
936
- if track_state.fxEnabled then
1013
+ -- Track metadata (v2, opt-in)
1014
+ if restore_meta and version >= 2 then
1015
+ if track_state.name then
1016
+ reaper.GetSetMediaTrackInfo_String(track, "P_NAME", track_state.name, true)
1017
+ end
1018
+ if track_state.color and track_state.color ~= 0 then
1019
+ reaper.SetMediaTrackInfo_Value(track, "I_CUSTOMCOLOR", track_state.color)
1020
+ end
1021
+ end
1022
+
1023
+ -- FX state (v2: full params; v1: bypass only)
1024
+ if version >= 2 and track_state.fx then
1025
+ for j, fx_state in ipairs(track_state.fx) do
1026
+ local fx_idx = j - 1
1027
+ if fx_idx < reaper.TrackFX_GetCount(track) then
1028
+ -- Restore enabled/offline state
1029
+ reaper.TrackFX_SetEnabled(track, fx_idx, fx_state.enabled)
1030
+ if fx_state.offline ~= nil then
1031
+ reaper.TrackFX_SetOffline(track, fx_idx, fx_state.offline)
1032
+ end
1033
+
1034
+ -- Restore FX parameters only if plugin name matches
1035
+ if fx_state.params and #fx_state.params > 0 then
1036
+ local _, current_name = reaper.TrackFX_GetFXName(track, fx_idx)
1037
+ if current_name == fx_state.name then
1038
+ for p, val in ipairs(fx_state.params) do
1039
+ reaper.TrackFX_SetParam(track, fx_idx, p - 1, val)
1040
+ end
1041
+ else
1042
+ warnings[#warnings + 1] = "Track " .. track_state.index .. " FX " .. fx_idx .. ": name mismatch ('" .. (current_name or "?") .. "' vs '" .. (fx_state.name or "?") .. "'), skipped params"
1043
+ end
1044
+ end
1045
+ end
1046
+ end
1047
+ elseif track_state.fxEnabled then
1048
+ -- Legacy v1: bypass state only
937
1049
  for j, enabled in ipairs(track_state.fxEnabled) do
938
- local fx_idx = j - 1 -- convert to 0-based
1050
+ local fx_idx = j - 1
939
1051
  if fx_idx < reaper.TrackFX_GetCount(track) then
940
1052
  reaper.TrackFX_SetEnabled(track, fx_idx, enabled)
941
1053
  end
942
1054
  end
943
1055
  end
1056
+
1057
+ -- Send levels (v2, default on)
1058
+ if version >= 2 and restore_sends and track_state.sends then
1059
+ local send_count = reaper.GetTrackNumSends(track, 0)
1060
+ for _, saved_send in ipairs(track_state.sends) do
1061
+ for s = 0, send_count - 1 do
1062
+ local dest = reaper.GetTrackSendInfo_Value(track, 0, s, "P_DESTTRACK")
1063
+ if dest then
1064
+ local dest_idx = reaper.GetMediaTrackInfo_Value(dest, "IP_TRACKNUMBER") - 1
1065
+ if dest_idx == saved_send.destTrackIndex then
1066
+ reaper.SetTrackSendInfo_Value(track, 0, s, "D_VOL", saved_send.volume)
1067
+ reaper.SetTrackSendInfo_Value(track, 0, s, "D_PAN", saved_send.pan)
1068
+ reaper.SetTrackSendInfo_Value(track, 0, s, "B_MUTE", saved_send.muted and 1 or 0)
1069
+ break
1070
+ end
1071
+ end
1072
+ end
1073
+ end
1074
+ end
1075
+
944
1076
  restored = restored + 1
945
1077
  end
946
1078
  end
@@ -951,12 +1083,15 @@ function handlers.snapshot_restore(params)
951
1083
  reaper.TrackList_AdjustWindows(false)
952
1084
  reaper.UpdateArrange()
953
1085
 
954
- return {
1086
+ local result = {
955
1087
  success = true,
956
1088
  name = name,
957
1089
  timestamp = snapshot.timestamp,
958
1090
  tracksRestored = restored,
1091
+ version = version,
959
1092
  }
1093
+ if #warnings > 0 then result.warnings = warnings end
1094
+ return result
960
1095
  end
961
1096
 
962
1097
  function handlers.snapshot_list(params)
@@ -987,7 +1122,7 @@ function handlers.snapshot_list(params)
987
1122
  -- Sort by timestamp descending (newest first)
988
1123
  table.sort(snapshots, function(a, b) return a.timestamp > b.timestamp end)
989
1124
 
990
- return { snapshots = snapshots, total = #snapshots }
1125
+ return { snapshots = snapshots, total = #snapshots, storageLocation = get_snapshot_storage_location() }
991
1126
  end
992
1127
 
993
1128
  -- =============================================================================