@mthines/reaper-mcp 0.10.0 → 0.10.1-beta.13.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
@@ -1703,6 +1703,270 @@ function registerEnvelopeTools(server) {
1703
1703
  );
1704
1704
  }
1705
1705
 
1706
+ // apps/reaper-mcp-server/src/tools/batch.ts
1707
+ import { z as z15 } from "zod/v4";
1708
+ function registerBatchTools(server) {
1709
+ server.tool(
1710
+ "set_multiple_track_properties",
1711
+ "Batch set properties (volume, pan, mute, solo, recordArm, phase, input) on multiple tracks in a single call. Much more efficient than calling set_track_property repeatedly. Pass an array of track update objects, each with trackIndex and any properties to change.",
1712
+ {
1713
+ tracks: z15.array(z15.object({
1714
+ trackIndex: z15.coerce.number().int().min(0).describe("0-based track index"),
1715
+ volume: z15.coerce.number().optional().describe("Volume in dB (0 = unity gain)"),
1716
+ pan: z15.coerce.number().min(-1).max(1).optional().describe("Pan position -1.0 (left) to 1.0 (right)"),
1717
+ mute: z15.coerce.number().int().min(0).max(1).optional().describe("Mute state (0=unmuted, 1=muted)"),
1718
+ solo: z15.coerce.number().int().min(0).max(1).optional().describe("Solo state (0=unsolo, 1=solo)"),
1719
+ recordArm: z15.coerce.number().int().min(0).max(1).optional().describe("Record arm state (0=unarmed, 1=armed)"),
1720
+ phase: z15.coerce.number().int().min(0).max(1).optional().describe("Phase inversion (0=normal, 1=inverted)"),
1721
+ input: z15.coerce.number().int().optional().describe("REAPER input index (-1 = no input)")
1722
+ })).describe("Array of track property updates to apply")
1723
+ },
1724
+ async ({ tracks }) => {
1725
+ const res = await sendCommand("set_multiple_track_properties", { tracks });
1726
+ if (!res.success) {
1727
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
1728
+ }
1729
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
1730
+ }
1731
+ );
1732
+ server.tool(
1733
+ "setup_fx_chain",
1734
+ "Add multiple FX plugins to a track with optional initial parameter values in a single call. Returns the FX chain indices of added plugins. More efficient than calling add_fx and set_fx_parameter repeatedly.",
1735
+ {
1736
+ trackIndex: z15.coerce.number().int().min(0).describe("0-based track index"),
1737
+ plugins: z15.array(z15.object({
1738
+ fxName: z15.string().describe('FX plugin name (partial match supported, e.g. "ReaEQ", "VST: Pro-Q 3")'),
1739
+ enabled: z15.coerce.number().int().min(0).max(1).optional().describe("Initial enabled state (1=enabled default, 0=bypassed)"),
1740
+ parameters: z15.array(z15.object({
1741
+ index: z15.coerce.number().int().min(0).describe("Parameter index"),
1742
+ value: z15.coerce.number().min(0).max(1).describe("Normalized parameter value 0.0\u20131.0")
1743
+ })).optional().describe("Initial parameter values to set after adding the plugin")
1744
+ })).describe("Array of FX plugins to add, in order")
1745
+ },
1746
+ async ({ trackIndex, plugins }) => {
1747
+ const res = await sendCommand("setup_fx_chain", { trackIndex, plugins });
1748
+ if (!res.success) {
1749
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
1750
+ }
1751
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
1752
+ }
1753
+ );
1754
+ server.tool(
1755
+ "set_multiple_fx_parameters",
1756
+ "Batch set parameters across multiple FX plugins, potentially on different tracks, in a single call. Much faster than repeated set_fx_parameter calls. Pass an array of update objects with trackIndex, fxIndex, paramIndex, and value.",
1757
+ {
1758
+ updates: z15.array(z15.object({
1759
+ trackIndex: z15.coerce.number().int().min(0).describe("0-based track index"),
1760
+ fxIndex: z15.coerce.number().int().min(0).describe("0-based FX index in the chain"),
1761
+ paramIndex: z15.coerce.number().int().min(0).describe("0-based parameter index"),
1762
+ value: z15.coerce.number().min(0).max(1).describe("Normalized parameter value 0.0\u20131.0")
1763
+ })).describe("Array of FX parameter updates to apply")
1764
+ },
1765
+ async ({ updates }) => {
1766
+ const res = await sendCommand("set_multiple_fx_parameters", { updates });
1767
+ if (!res.success) {
1768
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
1769
+ }
1770
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
1771
+ }
1772
+ );
1773
+ }
1774
+
1775
+ // apps/reaper-mcp-server/src/tools/categories.ts
1776
+ import { z as z16 } from "zod/v4";
1777
+ var TOOL_CATEGORIES = {
1778
+ project: {
1779
+ name: "project",
1780
+ description: "Project-level information: name, tempo, time signature, sample rate, transport state",
1781
+ tools: ["get_project_info"]
1782
+ },
1783
+ tracks: {
1784
+ name: "tracks",
1785
+ description: "Track management: list, inspect, and set properties (volume, pan, mute, solo, record arm, phase, input). Includes batch set for multiple tracks at once.",
1786
+ tools: ["list_tracks", "get_track_properties", "set_track_property", "set_multiple_track_properties"]
1787
+ },
1788
+ fx: {
1789
+ name: "fx",
1790
+ description: "FX chain management: add, remove, inspect, and set parameters. Includes batch setup of an entire FX chain and batch parameter updates across multiple plugins.",
1791
+ tools: ["add_fx", "remove_fx", "get_fx_parameters", "set_fx_parameter", "set_fx_enabled", "set_fx_offline", "setup_fx_chain", "set_multiple_fx_parameters"]
1792
+ },
1793
+ transport: {
1794
+ name: "transport",
1795
+ description: "Transport control: play, stop, record, get transport state, set cursor position",
1796
+ tools: ["play", "stop", "record", "get_transport_state", "set_cursor_position"]
1797
+ },
1798
+ midi: {
1799
+ name: "midi",
1800
+ description: "MIDI item editing: create items, insert/edit/delete notes and CC events, batch operations, analysis. Use analyze_midi first for large items; paginate with get_midi_notes offset/limit.",
1801
+ tools: [
1802
+ "create_midi_item",
1803
+ "list_midi_items",
1804
+ "get_midi_notes",
1805
+ "analyze_midi",
1806
+ "insert_midi_note",
1807
+ "insert_midi_notes",
1808
+ "edit_midi_note",
1809
+ "edit_midi_notes",
1810
+ "delete_midi_note",
1811
+ "get_midi_cc",
1812
+ "insert_midi_cc",
1813
+ "delete_midi_cc",
1814
+ "get_midi_item_properties",
1815
+ "set_midi_item_properties"
1816
+ ]
1817
+ },
1818
+ media: {
1819
+ name: "media",
1820
+ description: "Media item editing: list, inspect, set properties, split, delete, move, trim, stretch markers. Includes batch set for multiple items at once.",
1821
+ tools: [
1822
+ "list_media_items",
1823
+ "get_media_item_properties",
1824
+ "set_media_item_properties",
1825
+ "set_media_items_properties",
1826
+ "split_media_item",
1827
+ "delete_media_item",
1828
+ "move_media_item",
1829
+ "trim_media_item",
1830
+ "add_stretch_marker",
1831
+ "get_stretch_markers",
1832
+ "delete_stretch_marker"
1833
+ ]
1834
+ },
1835
+ selection: {
1836
+ name: "selection",
1837
+ description: "Selection and navigation: get selected tracks, get/set time selection range",
1838
+ tools: ["get_selected_tracks", "get_time_selection", "set_time_selection"]
1839
+ },
1840
+ markers: {
1841
+ name: "markers",
1842
+ description: "Markers and regions: list, add, delete markers and regions with optional names and colors",
1843
+ tools: ["list_markers", "list_regions", "add_marker", "add_region", "delete_marker", "delete_region"]
1844
+ },
1845
+ tempo: {
1846
+ name: "tempo",
1847
+ description: "Tempo map: get all tempo and time signature changes with positions, BPM, and linear/step flags",
1848
+ tools: ["get_tempo_map"]
1849
+ },
1850
+ envelopes: {
1851
+ name: "envelopes",
1852
+ description: "Automation envelopes: create, inspect, insert/delete points, clear, batch insert, set properties. Supports volume, pan, mute, width, trim volume, and FX parameter envelopes.",
1853
+ tools: [
1854
+ "create_track_envelope",
1855
+ "get_track_envelopes",
1856
+ "get_envelope_points",
1857
+ "insert_envelope_point",
1858
+ "insert_envelope_points",
1859
+ "delete_envelope_point",
1860
+ "clear_envelope",
1861
+ "remove_envelope_points",
1862
+ "set_envelope_properties"
1863
+ ]
1864
+ },
1865
+ analysis: {
1866
+ name: "analysis",
1867
+ description: "Audio metering and analysis: peak/RMS meters, FFT spectrum, LUFS loudness, stereo correlation, crest factor",
1868
+ tools: ["read_track_meters", "read_track_spectrum", "read_track_lufs", "read_track_correlation", "read_track_crest"]
1869
+ },
1870
+ discovery: {
1871
+ name: "discovery",
1872
+ description: "FX discovery and presets: list all installed plugins, fuzzy search by name, list and load presets",
1873
+ tools: ["list_available_fx", "search_fx", "get_fx_preset_list", "set_fx_preset"]
1874
+ },
1875
+ snapshots: {
1876
+ name: "snapshots",
1877
+ description: "Mixer state snapshots: save, restore, and list named snapshots of volumes, pans, FX, and mutes",
1878
+ tools: ["snapshot_save", "snapshot_restore", "snapshot_list"]
1879
+ },
1880
+ routing: {
1881
+ name: "routing",
1882
+ description: "Track routing: inspect sends, receives, parent/folder relationships for a track",
1883
+ tools: ["get_track_routing"]
1884
+ }
1885
+ };
1886
+ function registerCategoryTools(server) {
1887
+ server.tool(
1888
+ "list_tool_categories",
1889
+ "List all available tool categories with descriptions and tool names. Use this to discover what tools are available without loading all 72+ tool schemas. Categories: project, tracks, fx, transport, midi, media, selection, markers, tempo, envelopes, analysis, discovery, snapshots, routing.",
1890
+ {},
1891
+ async () => {
1892
+ const categories = Object.values(TOOL_CATEGORIES).map((cat) => ({
1893
+ name: cat.name,
1894
+ description: cat.description,
1895
+ toolCount: cat.tools.length,
1896
+ tools: cat.tools
1897
+ }));
1898
+ return {
1899
+ content: [{
1900
+ type: "text",
1901
+ text: JSON.stringify({ categories, totalTools: categories.reduce((sum, c) => sum + c.toolCount, 0) }, null, 2)
1902
+ }]
1903
+ };
1904
+ }
1905
+ );
1906
+ server.tool(
1907
+ "enable_tool_category",
1908
+ "Signal that you intend to use tools from a specific category. All tools are already registered and available \u2014 this call returns the full list of tools in the category so you know exactly which tool names to use. Use list_tool_categories first to see all available categories.",
1909
+ {
1910
+ category: z16.string().describe('Category name (e.g. "tracks", "fx", "midi", "media", "transport", "markers", "envelopes", "analysis", "discovery")')
1911
+ },
1912
+ async ({ category }) => {
1913
+ const cat = TOOL_CATEGORIES[category];
1914
+ if (!cat) {
1915
+ const available = Object.keys(TOOL_CATEGORIES).join(", ");
1916
+ return {
1917
+ content: [{
1918
+ type: "text",
1919
+ text: `Error: Unknown category "${category}". Available categories: ${available}`
1920
+ }],
1921
+ isError: true
1922
+ };
1923
+ }
1924
+ return {
1925
+ content: [{
1926
+ type: "text",
1927
+ text: JSON.stringify({
1928
+ category: cat.name,
1929
+ description: cat.description,
1930
+ toolCount: cat.tools.length,
1931
+ tools: cat.tools,
1932
+ status: "ready"
1933
+ }, null, 2)
1934
+ }]
1935
+ };
1936
+ }
1937
+ );
1938
+ server.tool(
1939
+ "disable_tool_category",
1940
+ "Signal that you are done using tools from a specific category. This is a semantic hint to help manage your context budget \u2014 the tools remain registered but you can use this to indicate you no longer need them in the current workflow.",
1941
+ {
1942
+ category: z16.string().describe('Category name (e.g. "tracks", "fx", "midi", "media", "transport", "markers", "envelopes", "analysis", "discovery")')
1943
+ },
1944
+ async ({ category }) => {
1945
+ const cat = TOOL_CATEGORIES[category];
1946
+ if (!cat) {
1947
+ const available = Object.keys(TOOL_CATEGORIES).join(", ");
1948
+ return {
1949
+ content: [{
1950
+ type: "text",
1951
+ text: `Error: Unknown category "${category}". Available categories: ${available}`
1952
+ }],
1953
+ isError: true
1954
+ };
1955
+ }
1956
+ return {
1957
+ content: [{
1958
+ type: "text",
1959
+ text: JSON.stringify({
1960
+ category: cat.name,
1961
+ status: "disabled",
1962
+ note: "Tools remain available if needed. Use enable_tool_category to re-activate."
1963
+ }, null, 2)
1964
+ }]
1965
+ };
1966
+ }
1967
+ );
1968
+ }
1969
+
1706
1970
  // apps/reaper-mcp-server/src/server.ts
1707
1971
  function instrumentToolHandlers(server) {
1708
1972
  const originalTool = server.tool.bind(server);
@@ -1760,6 +2024,8 @@ function createServer() {
1760
2024
  registerMarkerTools(server);
1761
2025
  registerTempoTools(server);
1762
2026
  registerEnvelopeTools(server);
2027
+ registerBatchTools(server);
2028
+ registerCategoryTools(server);
1763
2029
  return server;
1764
2030
  }
1765
2031
 
@@ -1919,7 +2185,15 @@ var MCP_TOOL_NAMES = [
1919
2185
  "create_track_envelope",
1920
2186
  "set_envelope_properties",
1921
2187
  "clear_envelope",
1922
- "remove_envelope_points"
2188
+ "remove_envelope_points",
2189
+ // composite batch tools
2190
+ "set_multiple_track_properties",
2191
+ "setup_fx_chain",
2192
+ "set_multiple_fx_parameters",
2193
+ // progressive discovery
2194
+ "list_tool_categories",
2195
+ "enable_tool_category",
2196
+ "disable_tool_category"
1923
2197
  ];
1924
2198
  function ensureClaudeSettings(settingsPath) {
1925
2199
  const allowList = MCP_TOOL_NAMES.map((t) => `mcp__reaper__${t}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mthines/reaper-mcp",
3
- "version": "0.10.0",
3
+ "version": "0.10.1-beta.13.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",
@@ -2201,6 +2201,196 @@ function handlers.set_fx_offline(params)
2201
2201
  return { trackIndex = params.trackIndex, fxIndex = params.fxIndex, offline = params.offline == 1 }
2202
2202
  end
2203
2203
 
2204
+ -- =============================================================================
2205
+ -- Composite batch tools
2206
+ -- =============================================================================
2207
+
2208
+ function handlers.set_multiple_track_properties(params)
2209
+ local tracks_input = params.tracks
2210
+ if not tracks_input then return nil, "tracks array required" end
2211
+
2212
+ local tracks = nil
2213
+ if type(tracks_input) == "string" then
2214
+ tracks = json_decode(tracks_input)
2215
+ elseif type(tracks_input) == "table" then
2216
+ tracks = tracks_input
2217
+ end
2218
+ if not tracks then return nil, "Failed to parse tracks array" end
2219
+
2220
+ local updated = 0
2221
+ local errors = {}
2222
+
2223
+ reaper.Undo_BeginBlock()
2224
+
2225
+ for _, t in ipairs(tracks) do
2226
+ local track_idx = t.trackIndex
2227
+ if track_idx == nil then
2228
+ errors[#errors + 1] = "track entry missing trackIndex"
2229
+ else
2230
+ local track = reaper.GetTrack(0, track_idx)
2231
+ if not track then
2232
+ errors[#errors + 1] = "Track " .. track_idx .. " not found"
2233
+ else
2234
+ if t.volume ~= nil then
2235
+ reaper.SetMediaTrackInfo_Value(track, "D_VOL", from_db(t.volume))
2236
+ end
2237
+ if t.pan ~= nil then
2238
+ reaper.SetMediaTrackInfo_Value(track, "D_PAN", t.pan)
2239
+ end
2240
+ if t.mute ~= nil then
2241
+ reaper.SetMediaTrackInfo_Value(track, "B_MUTE", t.mute)
2242
+ end
2243
+ if t.solo ~= nil then
2244
+ reaper.SetMediaTrackInfo_Value(track, "I_SOLO", t.solo)
2245
+ end
2246
+ if t.recordArm ~= nil then
2247
+ reaper.SetMediaTrackInfo_Value(track, "I_RECARM", t.recordArm)
2248
+ end
2249
+ if t.phase ~= nil then
2250
+ reaper.SetMediaTrackInfo_Value(track, "B_PHASE", t.phase)
2251
+ end
2252
+ if t.input ~= nil then
2253
+ reaper.SetMediaTrackInfo_Value(track, "I_RECINPUT", t.input)
2254
+ end
2255
+ updated = updated + 1
2256
+ end
2257
+ end
2258
+ end
2259
+
2260
+ reaper.Undo_EndBlock("MCP: Batch set " .. updated .. " track properties", -1)
2261
+
2262
+ local result = { success = true, updated = updated, total = #tracks }
2263
+ if #errors > 0 then result.errors = errors end
2264
+ return result
2265
+ end
2266
+
2267
+ function handlers.setup_fx_chain(params)
2268
+ local track_idx = params.trackIndex
2269
+ if track_idx == nil then return nil, "trackIndex required" end
2270
+
2271
+ local plugins_input = params.plugins
2272
+ if not plugins_input then return nil, "plugins array required" end
2273
+
2274
+ local plugins = nil
2275
+ if type(plugins_input) == "string" then
2276
+ plugins = json_decode(plugins_input)
2277
+ elseif type(plugins_input) == "table" then
2278
+ plugins = plugins_input
2279
+ end
2280
+ if not plugins then return nil, "Failed to parse plugins array" end
2281
+
2282
+ local track = reaper.GetTrack(0, track_idx)
2283
+ if not track then return nil, "Track " .. track_idx .. " not found" end
2284
+
2285
+ local added = 0
2286
+ local errors = {}
2287
+ local added_plugins = {}
2288
+
2289
+ reaper.Undo_BeginBlock()
2290
+
2291
+ for i, plugin in ipairs(plugins) do
2292
+ local fx_name = plugin.fxName
2293
+ if not fx_name or fx_name == "" then
2294
+ errors[#errors + 1] = "plugin[" .. (i - 1) .. "] missing fxName"
2295
+ else
2296
+ local fx_idx = reaper.TrackFX_AddByName(track, fx_name, false, -1)
2297
+ if fx_idx < 0 then
2298
+ errors[#errors + 1] = "FX not found: " .. fx_name
2299
+ else
2300
+ -- Set enabled state if specified (default is enabled)
2301
+ if plugin.enabled ~= nil then
2302
+ reaper.TrackFX_SetEnabled(track, fx_idx, plugin.enabled == 1)
2303
+ end
2304
+
2305
+ -- Set initial parameters if provided
2306
+ local param_errors = {}
2307
+ if plugin.parameters and type(plugin.parameters) == "table" then
2308
+ for _, p in ipairs(plugin.parameters) do
2309
+ if p.index ~= nil and p.value ~= nil then
2310
+ local ok = reaper.TrackFX_SetParam(track, fx_idx, p.index, p.value)
2311
+ if not ok then
2312
+ param_errors[#param_errors + 1] = "Failed to set param " .. p.index
2313
+ end
2314
+ end
2315
+ end
2316
+ end
2317
+
2318
+ local _, actual_name = reaper.TrackFX_GetFXName(track, fx_idx)
2319
+ added_plugins[#added_plugins + 1] = {
2320
+ pluginIndex = i - 1,
2321
+ fxName = actual_name or fx_name,
2322
+ fxIndex = fx_idx,
2323
+ }
2324
+ if #param_errors > 0 then
2325
+ for _, pe in ipairs(param_errors) do
2326
+ errors[#errors + 1] = fx_name .. ": " .. pe
2327
+ end
2328
+ end
2329
+ added = added + 1
2330
+ end
2331
+ end
2332
+ end
2333
+
2334
+ reaper.Undo_EndBlock("MCP: Setup FX chain (" .. added .. " plugins) on track " .. track_idx, -1)
2335
+
2336
+ local result = {
2337
+ success = true,
2338
+ trackIndex = track_idx,
2339
+ added = added,
2340
+ total = #plugins,
2341
+ plugins = added_plugins,
2342
+ }
2343
+ if #errors > 0 then result.errors = errors end
2344
+ return result
2345
+ end
2346
+
2347
+ function handlers.set_multiple_fx_parameters(params)
2348
+ local updates_input = params.updates
2349
+ if not updates_input then return nil, "updates array required" end
2350
+
2351
+ local updates = nil
2352
+ if type(updates_input) == "string" then
2353
+ updates = json_decode(updates_input)
2354
+ elseif type(updates_input) == "table" then
2355
+ updates = updates_input
2356
+ end
2357
+ if not updates then return nil, "Failed to parse updates array" end
2358
+
2359
+ local updated = 0
2360
+ local errors = {}
2361
+
2362
+ reaper.Undo_BeginBlock()
2363
+
2364
+ for _, u in ipairs(updates) do
2365
+ local track_idx = u.trackIndex
2366
+ local fx_idx = u.fxIndex
2367
+ local param_idx = u.paramIndex
2368
+ local value = u.value
2369
+
2370
+ if track_idx == nil or fx_idx == nil or param_idx == nil or value == nil then
2371
+ errors[#errors + 1] = "update entry missing trackIndex, fxIndex, paramIndex, or value"
2372
+ else
2373
+ local track = reaper.GetTrack(0, track_idx)
2374
+ if not track then
2375
+ errors[#errors + 1] = "Track " .. track_idx .. " not found"
2376
+ else
2377
+ local ok = reaper.TrackFX_SetParam(track, fx_idx, param_idx, value)
2378
+ if not ok then
2379
+ errors[#errors + 1] = "Failed to set track " .. track_idx .. " fx " .. fx_idx .. " param " .. param_idx
2380
+ else
2381
+ updated = updated + 1
2382
+ end
2383
+ end
2384
+ end
2385
+ end
2386
+
2387
+ reaper.Undo_EndBlock("MCP: Batch set " .. updated .. " FX parameters", -1)
2388
+
2389
+ local result = { success = true, updated = updated, total = #updates }
2390
+ if #errors > 0 then result.errors = errors end
2391
+ return result
2392
+ end
2393
+
2204
2394
  -- =============================================================================
2205
2395
  -- Selection & Navigation
2206
2396
  -- =============================================================================