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