@mthines/reaper-mcp 0.13.0 → 0.14.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
@@ -1875,6 +1875,270 @@ function registerEnvelopeTools(server) {
1875
1875
  );
1876
1876
  }
1877
1877
 
1878
+ // apps/reaper-mcp-server/src/tools/batch.ts
1879
+ import { z as z15 } from "zod/v4";
1880
+ function registerBatchTools(server) {
1881
+ server.tool(
1882
+ "set_multiple_track_properties",
1883
+ "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.",
1884
+ {
1885
+ tracks: z15.array(z15.object({
1886
+ trackIndex: z15.coerce.number().int().min(0).describe("0-based track index"),
1887
+ volume: z15.coerce.number().optional().describe("Volume in dB (0 = unity gain)"),
1888
+ pan: z15.coerce.number().min(-1).max(1).optional().describe("Pan position -1.0 (left) to 1.0 (right)"),
1889
+ mute: z15.coerce.number().int().min(0).max(1).optional().describe("Mute state (0=unmuted, 1=muted)"),
1890
+ solo: z15.coerce.number().int().min(0).max(1).optional().describe("Solo state (0=unsolo, 1=solo)"),
1891
+ recordArm: z15.coerce.number().int().min(0).max(1).optional().describe("Record arm state (0=unarmed, 1=armed)"),
1892
+ phase: z15.coerce.number().int().min(0).max(1).optional().describe("Phase inversion (0=normal, 1=inverted)"),
1893
+ input: z15.coerce.number().int().optional().describe("REAPER input index (-1 = no input)")
1894
+ })).describe("Array of track property updates to apply")
1895
+ },
1896
+ async ({ tracks }) => {
1897
+ const res = await sendCommand("set_multiple_track_properties", { tracks });
1898
+ if (!res.success) {
1899
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
1900
+ }
1901
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
1902
+ }
1903
+ );
1904
+ server.tool(
1905
+ "setup_fx_chain",
1906
+ "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.",
1907
+ {
1908
+ trackIndex: z15.coerce.number().int().min(0).describe("0-based track index"),
1909
+ plugins: z15.array(z15.object({
1910
+ fxName: z15.string().describe('FX plugin name (partial match supported, e.g. "ReaEQ", "VST: Pro-Q 3")'),
1911
+ enabled: z15.coerce.number().int().min(0).max(1).optional().describe("Initial enabled state (1=enabled default, 0=bypassed)"),
1912
+ parameters: z15.array(z15.object({
1913
+ index: z15.coerce.number().int().min(0).describe("Parameter index"),
1914
+ value: z15.coerce.number().min(0).max(1).describe("Normalized parameter value 0.0\u20131.0")
1915
+ })).optional().describe("Initial parameter values to set after adding the plugin")
1916
+ })).describe("Array of FX plugins to add, in order")
1917
+ },
1918
+ async ({ trackIndex, plugins }) => {
1919
+ const res = await sendCommand("setup_fx_chain", { trackIndex, plugins });
1920
+ if (!res.success) {
1921
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
1922
+ }
1923
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
1924
+ }
1925
+ );
1926
+ server.tool(
1927
+ "set_multiple_fx_parameters",
1928
+ "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.",
1929
+ {
1930
+ updates: z15.array(z15.object({
1931
+ trackIndex: z15.coerce.number().int().min(0).describe("0-based track index"),
1932
+ fxIndex: z15.coerce.number().int().min(0).describe("0-based FX index in the chain"),
1933
+ paramIndex: z15.coerce.number().int().min(0).describe("0-based parameter index"),
1934
+ value: z15.coerce.number().min(0).max(1).describe("Normalized parameter value 0.0\u20131.0")
1935
+ })).describe("Array of FX parameter updates to apply")
1936
+ },
1937
+ async ({ updates }) => {
1938
+ const res = await sendCommand("set_multiple_fx_parameters", { updates });
1939
+ if (!res.success) {
1940
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
1941
+ }
1942
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
1943
+ }
1944
+ );
1945
+ }
1946
+
1947
+ // apps/reaper-mcp-server/src/tools/categories.ts
1948
+ import { z as z16 } from "zod/v4";
1949
+ var TOOL_CATEGORIES = {
1950
+ project: {
1951
+ name: "project",
1952
+ description: "Project-level information: name, tempo, time signature, sample rate, transport state",
1953
+ tools: ["get_project_info"]
1954
+ },
1955
+ tracks: {
1956
+ name: "tracks",
1957
+ description: "Track management: list, inspect, and set properties (volume, pan, mute, solo, record arm, phase, input). Includes batch set for multiple tracks at once.",
1958
+ tools: ["list_tracks", "get_track_properties", "set_track_property", "set_multiple_track_properties"]
1959
+ },
1960
+ fx: {
1961
+ name: "fx",
1962
+ 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.",
1963
+ tools: ["add_fx", "remove_fx", "get_fx_parameters", "set_fx_parameter", "set_fx_enabled", "set_fx_offline", "setup_fx_chain", "set_multiple_fx_parameters"]
1964
+ },
1965
+ transport: {
1966
+ name: "transport",
1967
+ description: "Transport control: play, stop, record, get transport state, set cursor position",
1968
+ tools: ["play", "stop", "record", "get_transport_state", "set_cursor_position"]
1969
+ },
1970
+ midi: {
1971
+ name: "midi",
1972
+ 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.",
1973
+ tools: [
1974
+ "create_midi_item",
1975
+ "list_midi_items",
1976
+ "get_midi_notes",
1977
+ "analyze_midi",
1978
+ "insert_midi_note",
1979
+ "insert_midi_notes",
1980
+ "edit_midi_note",
1981
+ "edit_midi_notes",
1982
+ "delete_midi_note",
1983
+ "get_midi_cc",
1984
+ "insert_midi_cc",
1985
+ "delete_midi_cc",
1986
+ "get_midi_item_properties",
1987
+ "set_midi_item_properties"
1988
+ ]
1989
+ },
1990
+ media: {
1991
+ name: "media",
1992
+ description: "Media item editing: list, inspect, set properties, split, delete, move, trim, stretch markers. Includes batch set for multiple items at once.",
1993
+ tools: [
1994
+ "list_media_items",
1995
+ "get_media_item_properties",
1996
+ "set_media_item_properties",
1997
+ "set_media_items_properties",
1998
+ "split_media_item",
1999
+ "delete_media_item",
2000
+ "move_media_item",
2001
+ "trim_media_item",
2002
+ "add_stretch_marker",
2003
+ "get_stretch_markers",
2004
+ "delete_stretch_marker"
2005
+ ]
2006
+ },
2007
+ selection: {
2008
+ name: "selection",
2009
+ description: "Selection and navigation: get selected tracks, get/set time selection range",
2010
+ tools: ["get_selected_tracks", "get_time_selection", "set_time_selection"]
2011
+ },
2012
+ markers: {
2013
+ name: "markers",
2014
+ description: "Markers and regions: list, add, delete markers and regions with optional names and colors",
2015
+ tools: ["list_markers", "list_regions", "add_marker", "add_region", "delete_marker", "delete_region"]
2016
+ },
2017
+ tempo: {
2018
+ name: "tempo",
2019
+ description: "Tempo map: get all tempo and time signature changes with positions, BPM, and linear/step flags",
2020
+ tools: ["get_tempo_map"]
2021
+ },
2022
+ envelopes: {
2023
+ name: "envelopes",
2024
+ description: "Automation envelopes: create, inspect, insert/delete points, clear, batch insert, set properties. Supports volume, pan, mute, width, trim volume, and FX parameter envelopes.",
2025
+ tools: [
2026
+ "create_track_envelope",
2027
+ "get_track_envelopes",
2028
+ "get_envelope_points",
2029
+ "insert_envelope_point",
2030
+ "insert_envelope_points",
2031
+ "delete_envelope_point",
2032
+ "clear_envelope",
2033
+ "remove_envelope_points",
2034
+ "set_envelope_properties"
2035
+ ]
2036
+ },
2037
+ analysis: {
2038
+ name: "analysis",
2039
+ description: "Audio metering and analysis: peak/RMS meters, FFT spectrum, LUFS loudness, stereo correlation, crest factor",
2040
+ tools: ["read_track_meters", "read_track_spectrum", "read_track_lufs", "read_track_correlation", "read_track_crest"]
2041
+ },
2042
+ discovery: {
2043
+ name: "discovery",
2044
+ description: "FX discovery and presets: list all installed plugins, fuzzy search by name, list and load presets",
2045
+ tools: ["list_available_fx", "search_fx", "get_fx_preset_list", "set_fx_preset"]
2046
+ },
2047
+ snapshots: {
2048
+ name: "snapshots",
2049
+ description: "Mixer state snapshots: save, restore, and list named snapshots of volumes, pans, FX, and mutes",
2050
+ tools: ["snapshot_save", "snapshot_restore", "snapshot_list"]
2051
+ },
2052
+ routing: {
2053
+ name: "routing",
2054
+ description: "Track routing: inspect sends, receives, parent/folder relationships for a track",
2055
+ tools: ["get_track_routing"]
2056
+ }
2057
+ };
2058
+ function registerCategoryTools(server) {
2059
+ server.tool(
2060
+ "list_tool_categories",
2061
+ "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.",
2062
+ {},
2063
+ async () => {
2064
+ const categories = Object.values(TOOL_CATEGORIES).map((cat) => ({
2065
+ name: cat.name,
2066
+ description: cat.description,
2067
+ toolCount: cat.tools.length,
2068
+ tools: cat.tools
2069
+ }));
2070
+ return {
2071
+ content: [{
2072
+ type: "text",
2073
+ text: JSON.stringify({ categories, totalTools: categories.reduce((sum, c) => sum + c.toolCount, 0) }, null, 2)
2074
+ }]
2075
+ };
2076
+ }
2077
+ );
2078
+ server.tool(
2079
+ "enable_tool_category",
2080
+ "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.",
2081
+ {
2082
+ category: z16.string().describe('Category name (e.g. "tracks", "fx", "midi", "media", "transport", "markers", "envelopes", "analysis", "discovery")')
2083
+ },
2084
+ async ({ category }) => {
2085
+ const cat = TOOL_CATEGORIES[category];
2086
+ if (!cat) {
2087
+ const available = Object.keys(TOOL_CATEGORIES).join(", ");
2088
+ return {
2089
+ content: [{
2090
+ type: "text",
2091
+ text: `Error: Unknown category "${category}". Available categories: ${available}`
2092
+ }],
2093
+ isError: true
2094
+ };
2095
+ }
2096
+ return {
2097
+ content: [{
2098
+ type: "text",
2099
+ text: JSON.stringify({
2100
+ category: cat.name,
2101
+ description: cat.description,
2102
+ toolCount: cat.tools.length,
2103
+ tools: cat.tools,
2104
+ status: "ready"
2105
+ }, null, 2)
2106
+ }]
2107
+ };
2108
+ }
2109
+ );
2110
+ server.tool(
2111
+ "disable_tool_category",
2112
+ "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.",
2113
+ {
2114
+ category: z16.string().describe('Category name (e.g. "tracks", "fx", "midi", "media", "transport", "markers", "envelopes", "analysis", "discovery")')
2115
+ },
2116
+ async ({ category }) => {
2117
+ const cat = TOOL_CATEGORIES[category];
2118
+ if (!cat) {
2119
+ const available = Object.keys(TOOL_CATEGORIES).join(", ");
2120
+ return {
2121
+ content: [{
2122
+ type: "text",
2123
+ text: `Error: Unknown category "${category}". Available categories: ${available}`
2124
+ }],
2125
+ isError: true
2126
+ };
2127
+ }
2128
+ return {
2129
+ content: [{
2130
+ type: "text",
2131
+ text: JSON.stringify({
2132
+ category: cat.name,
2133
+ status: "disabled",
2134
+ note: "Tools remain available if needed. Use enable_tool_category to re-activate."
2135
+ }, null, 2)
2136
+ }]
2137
+ };
2138
+ }
2139
+ );
2140
+ }
2141
+
1878
2142
  // apps/reaper-mcp-server/src/server.ts
1879
2143
  function instrumentToolHandlers(server) {
1880
2144
  const originalTool = server.tool.bind(server);
@@ -1932,6 +2196,8 @@ function createServer() {
1932
2196
  registerMarkerTools(server);
1933
2197
  registerTempoTools(server);
1934
2198
  registerEnvelopeTools(server);
2199
+ registerBatchTools(server);
2200
+ registerCategoryTools(server);
1935
2201
  return server;
1936
2202
  }
1937
2203
 
@@ -2091,7 +2357,15 @@ var MCP_TOOL_NAMES = [
2091
2357
  "create_track_envelope",
2092
2358
  "set_envelope_properties",
2093
2359
  "clear_envelope",
2094
- "remove_envelope_points"
2360
+ "remove_envelope_points",
2361
+ // composite batch tools
2362
+ "set_multiple_track_properties",
2363
+ "setup_fx_chain",
2364
+ "set_multiple_fx_parameters",
2365
+ // progressive discovery
2366
+ "list_tool_categories",
2367
+ "enable_tool_category",
2368
+ "disable_tool_category"
2095
2369
  ];
2096
2370
  function ensureClaudeSettings(settingsPath) {
2097
2371
  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.13.0",
3
+ "version": "0.14.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",
@@ -2352,6 +2352,196 @@ function handlers.set_fx_offline(params)
2352
2352
  return { trackIndex = params.trackIndex, fxIndex = params.fxIndex, offline = params.offline == 1 }
2353
2353
  end
2354
2354
 
2355
+ -- =============================================================================
2356
+ -- Composite batch tools
2357
+ -- =============================================================================
2358
+
2359
+ function handlers.set_multiple_track_properties(params)
2360
+ local tracks_input = params.tracks
2361
+ if not tracks_input then return nil, "tracks array required" end
2362
+
2363
+ local tracks = nil
2364
+ if type(tracks_input) == "string" then
2365
+ tracks = json_decode(tracks_input)
2366
+ elseif type(tracks_input) == "table" then
2367
+ tracks = tracks_input
2368
+ end
2369
+ if not tracks then return nil, "Failed to parse tracks array" end
2370
+
2371
+ local updated = 0
2372
+ local errors = {}
2373
+
2374
+ reaper.Undo_BeginBlock()
2375
+
2376
+ for _, t in ipairs(tracks) do
2377
+ local track_idx = t.trackIndex
2378
+ if track_idx == nil then
2379
+ errors[#errors + 1] = "track entry missing trackIndex"
2380
+ else
2381
+ local track = reaper.GetTrack(0, track_idx)
2382
+ if not track then
2383
+ errors[#errors + 1] = "Track " .. track_idx .. " not found"
2384
+ else
2385
+ if t.volume ~= nil then
2386
+ reaper.SetMediaTrackInfo_Value(track, "D_VOL", from_db(t.volume))
2387
+ end
2388
+ if t.pan ~= nil then
2389
+ reaper.SetMediaTrackInfo_Value(track, "D_PAN", t.pan)
2390
+ end
2391
+ if t.mute ~= nil then
2392
+ reaper.SetMediaTrackInfo_Value(track, "B_MUTE", t.mute)
2393
+ end
2394
+ if t.solo ~= nil then
2395
+ reaper.SetMediaTrackInfo_Value(track, "I_SOLO", t.solo)
2396
+ end
2397
+ if t.recordArm ~= nil then
2398
+ reaper.SetMediaTrackInfo_Value(track, "I_RECARM", t.recordArm)
2399
+ end
2400
+ if t.phase ~= nil then
2401
+ reaper.SetMediaTrackInfo_Value(track, "B_PHASE", t.phase)
2402
+ end
2403
+ if t.input ~= nil then
2404
+ reaper.SetMediaTrackInfo_Value(track, "I_RECINPUT", t.input)
2405
+ end
2406
+ updated = updated + 1
2407
+ end
2408
+ end
2409
+ end
2410
+
2411
+ reaper.Undo_EndBlock("MCP: Batch set " .. updated .. " track properties", -1)
2412
+
2413
+ local result = { success = true, updated = updated, total = #tracks }
2414
+ if #errors > 0 then result.errors = errors end
2415
+ return result
2416
+ end
2417
+
2418
+ function handlers.setup_fx_chain(params)
2419
+ local track_idx = params.trackIndex
2420
+ if track_idx == nil then return nil, "trackIndex required" end
2421
+
2422
+ local plugins_input = params.plugins
2423
+ if not plugins_input then return nil, "plugins array required" end
2424
+
2425
+ local plugins = nil
2426
+ if type(plugins_input) == "string" then
2427
+ plugins = json_decode(plugins_input)
2428
+ elseif type(plugins_input) == "table" then
2429
+ plugins = plugins_input
2430
+ end
2431
+ if not plugins then return nil, "Failed to parse plugins array" end
2432
+
2433
+ local track = reaper.GetTrack(0, track_idx)
2434
+ if not track then return nil, "Track " .. track_idx .. " not found" end
2435
+
2436
+ local added = 0
2437
+ local errors = {}
2438
+ local added_plugins = {}
2439
+
2440
+ reaper.Undo_BeginBlock()
2441
+
2442
+ for i, plugin in ipairs(plugins) do
2443
+ local fx_name = plugin.fxName
2444
+ if not fx_name or fx_name == "" then
2445
+ errors[#errors + 1] = "plugin[" .. (i - 1) .. "] missing fxName"
2446
+ else
2447
+ local fx_idx = reaper.TrackFX_AddByName(track, fx_name, false, -1)
2448
+ if fx_idx < 0 then
2449
+ errors[#errors + 1] = "FX not found: " .. fx_name
2450
+ else
2451
+ -- Set enabled state if specified (default is enabled)
2452
+ if plugin.enabled ~= nil then
2453
+ reaper.TrackFX_SetEnabled(track, fx_idx, plugin.enabled == 1)
2454
+ end
2455
+
2456
+ -- Set initial parameters if provided
2457
+ local param_errors = {}
2458
+ if plugin.parameters and type(plugin.parameters) == "table" then
2459
+ for _, p in ipairs(plugin.parameters) do
2460
+ if p.index ~= nil and p.value ~= nil then
2461
+ local ok = reaper.TrackFX_SetParam(track, fx_idx, p.index, p.value)
2462
+ if not ok then
2463
+ param_errors[#param_errors + 1] = "Failed to set param " .. p.index
2464
+ end
2465
+ end
2466
+ end
2467
+ end
2468
+
2469
+ local _, actual_name = reaper.TrackFX_GetFXName(track, fx_idx)
2470
+ added_plugins[#added_plugins + 1] = {
2471
+ pluginIndex = i - 1,
2472
+ fxName = actual_name or fx_name,
2473
+ fxIndex = fx_idx,
2474
+ }
2475
+ if #param_errors > 0 then
2476
+ for _, pe in ipairs(param_errors) do
2477
+ errors[#errors + 1] = fx_name .. ": " .. pe
2478
+ end
2479
+ end
2480
+ added = added + 1
2481
+ end
2482
+ end
2483
+ end
2484
+
2485
+ reaper.Undo_EndBlock("MCP: Setup FX chain (" .. added .. " plugins) on track " .. track_idx, -1)
2486
+
2487
+ local result = {
2488
+ success = true,
2489
+ trackIndex = track_idx,
2490
+ added = added,
2491
+ total = #plugins,
2492
+ plugins = added_plugins,
2493
+ }
2494
+ if #errors > 0 then result.errors = errors end
2495
+ return result
2496
+ end
2497
+
2498
+ function handlers.set_multiple_fx_parameters(params)
2499
+ local updates_input = params.updates
2500
+ if not updates_input then return nil, "updates array required" end
2501
+
2502
+ local updates = nil
2503
+ if type(updates_input) == "string" then
2504
+ updates = json_decode(updates_input)
2505
+ elseif type(updates_input) == "table" then
2506
+ updates = updates_input
2507
+ end
2508
+ if not updates then return nil, "Failed to parse updates array" end
2509
+
2510
+ local updated = 0
2511
+ local errors = {}
2512
+
2513
+ reaper.Undo_BeginBlock()
2514
+
2515
+ for _, u in ipairs(updates) do
2516
+ local track_idx = u.trackIndex
2517
+ local fx_idx = u.fxIndex
2518
+ local param_idx = u.paramIndex
2519
+ local value = u.value
2520
+
2521
+ if track_idx == nil or fx_idx == nil or param_idx == nil or value == nil then
2522
+ errors[#errors + 1] = "update entry missing trackIndex, fxIndex, paramIndex, or value"
2523
+ else
2524
+ local track = reaper.GetTrack(0, track_idx)
2525
+ if not track then
2526
+ errors[#errors + 1] = "Track " .. track_idx .. " not found"
2527
+ else
2528
+ local ok = reaper.TrackFX_SetParam(track, fx_idx, param_idx, value)
2529
+ if not ok then
2530
+ errors[#errors + 1] = "Failed to set track " .. track_idx .. " fx " .. fx_idx .. " param " .. param_idx
2531
+ else
2532
+ updated = updated + 1
2533
+ end
2534
+ end
2535
+ end
2536
+ end
2537
+
2538
+ reaper.Undo_EndBlock("MCP: Batch set " .. updated .. " FX parameters", -1)
2539
+
2540
+ local result = { success = true, updated = updated, total = #updates }
2541
+ if #errors > 0 then result.errors = errors end
2542
+ return result
2543
+ end
2544
+
2355
2545
  -- =============================================================================
2356
2546
  -- Selection & Navigation
2357
2547
  -- =============================================================================