@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 +13 -9
- package/package.json +1 -1
- package/reaper/mcp_bridge.lua +154 -15
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
|
-
|
|
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
|
-
|
|
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
package/reaper/mcp_bridge.lua
CHANGED
|
@@ -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
|
-
|
|
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
|
-
--
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
--
|
|
897
|
-
if
|
|
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
|
|
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
|
-
|
|
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
|
-- =============================================================================
|