# Changelog All notable changes to ReaperMCP will be documented in this file. ## [Unreleased] ### Fixed - **The stock-plugin fallback (ReaComp/ReaLimit/ReaEQ, used by `engine_mix`/ `engine_master` whenever FabFilter isn't installed) wrote several parameters to the wrong index or the wrong scale, producing a mix with no compression anywhere, a master ~12dB too quiet, and EQ cuts/boosts off by a growing margin from what was asked for** (issue #41, reported with an exceptionally thorough live measurement of REAPER 7.82's real parameter behavior). All of the following were confirmed independently this session via `fx_get_params` read-back (name + REAPER-formatted display) against a real install, not just the report: - **ReaComp**: `Ratio` and `Pre-comp` were swapped (index 1 was written as pre-comp-off, index 4 as ratio — the real plugin has them the other way round), so the compressor's ratio silently never moved off 1:1. `Knee` was written to index 5 (an unused/reserved slot); the real Knee is at index 14. Index 6/7 were treated as "RMS size"/"makeup", but are actually the detector's own sidechain lowpass/highpass filters — left at the old formula's values, they put the detector behind a ~200Hz lowpass and an ~11kHz highpass, so it heard almost nothing regardless of the ratio fix. Makeup gain is now correctly written to index 11 ("Wet"), confirmed live to be a plain amplitude control (`10**(dB/20)`) — the original report left this unconfirmed; this session resolved it with a direct live test. - **ReaLimit**: threshold and ceiling were both normalized over the same -60..0dB range, but the real ranges are -60..+12dB (threshold) and -24..0dB (ceiling) — confirmed live. An intended 0dB threshold actually landed at +12dB and an intended -1dB ceiling landed at -0.4dB, cutting the whole master by the ~12.4dB difference between them. - **ReaEQ gain**: the formula was linear in dB (`0.25 + db/24`); the real scale is exponential (`10**(db/20)/4`), confirmed live. The error grows with distance from 0dB — an intended -2dB cut actually landed at -3.5dB. - **ReaEQ bandwidth**: Q was written directly as the BW param (`q / 5.0`), but BW increases with the param value on the real plugin, so a *higher* (narrower-intended) Q produced a *wider* band — backwards from what Q means. Now uses the standard Q<->bandwidth relationship. - **A 3rd+ EQ bell band was silently dropped** with no indication in the response. ReaEQ only has 2 generic bell slots; band 1/4 are hard-wired to Low Shelf/High Shelf in the plugin itself (confirmed live via their REAPER parameter names, literally `Freq-Low Shelf` / `Freq-High Shelf 4`, not a switchable type) — so reusing a free shelf slot for an extra bell was considered and rejected, since it would silently apply a shelf-shaped curve instead of a bell. Dropped bands are now logged instead. - **`master.py` had its own, separately-calibrated (and equally wrong) copies** of the ReaEQ frequency/gain formulas and the ReaComp index map, instead of reusing `plugins.py`'s versions. Both now import/delegate to the single corrected implementation. - Also fixed: the master-bus tonal EQ (a separate ReaEQ instance from the subtractive stage) never explicitly zeroed its own band 5 (high-pass), which defaults to an unconfirmed ~100Hz on at least one install — it's now explicitly set to 20Hz/0dB regardless, harmless either way. - **Known remaining gap, lower severity, not yet fixed**: ReaVerbate's `room_size` parameter isn't the 1:1 pass-through the code assumes — confirmed live it's non-linear (0.25→"47", 0.5→"64", 0.75→"81", not a clean sqrt/log fit from those points either). `dampening` *is* a clean 1:1 pass-through, confirmed the same way. This affects the reverb's "size" feel, not whether it's audible — left for a follow-up with a proper multi-point sweep. - Pinned regression tests added in `tests/test_mix_engine_formulas.py` for every corrected formula, using the exact live-confirmed value/display pairs above. - **`fx_scan_params` crashed on any plugin on at least one install** — `reaper.TrackFX_GetParameterStepCount` isn't a real REAPER API function there (confirmed live: calling it throws "attempt to call a nil value"), so the whole scan crashed before reaching the existing fallback for continuous params. The call is now guarded so a missing function falls through to that fallback instead. Covered by `tests/test_fx_scan_params_lua.py`. ## [0.8.3] - 2026-10-20 ### Fixed - **`track_set_input(input_index=-1)` cleared the track's input instead of selecting all MIDI inputs.** The tool documents and accepts `-1` as "MIDI all", but the Lua handler wrote it straight to `I_RECINPUT`, where REAPER reads any negative value as "no input". The handler now translates `-1` to `6112` (MIDI, all inputs, all channels); every other value is passed through unchanged. The tool's description also had the ranges wrong (`0` is the first mono input, not "none"); it now gives the correct ranges. Tests run the real handler under `lupa` with a stubbed `reaper`. - **`envelope_add_points` accepted an all-zero batch on the Volume envelope and reported success, silently writing −∞ dB (total silence) to the track.** This is the failure mode described in issue #39: AI clients that receive the "must be linear gain (≥ 0)" error for a negative dB value "fix" it by clamping every point to `0.0`, which passes per-point validation but produces an all-silence envelope. The server now rejects any batch of 2+ points where every value is `0.0` on a dB-based envelope (`Volume`, `Volume (Pre-FX)`) with a `VALUE_OUT_OF_RANGE` error that explicitly says "do not clamp — convert: `value = 10 ** (db / 20)`". Single zero-value points (legitimate fade-to-silence points) are still allowed. `Width`/`Width (Pre-FX)` are deliberately excluded — Width isn't dB-based, so there's no equivalent conversion mistake, and an all-zero Width batch (collapsed to mono) is an ordinary intentional request, not silence. The error message on negative values and the docstrings for `_validate_envelope_value` and `envelope_add_points` also now carry an explicit "NEVER clamp negative dB to 0.0" warning. Covered by new tests in `tests/test_envelope_validation.py`. ## [0.8.2] - 2026-09-23 ### Added - **`uvx xdarkzx-reaper-mcp` now works.** The PyPI package name (`xdarkzx-reaper-mcp`) and the console command (`reaper-mcp`) never matched, so `uvx`/`npx`-style ephemeral launchers — which assume the command name equals the package name — couldn't find it (`uvx` reported "An executable named `xdarkzx-reaper-mcp` is not provided by package `xdarkzx-reaper-mcp`"). A second console-script entry aliases the package name to the same entry point, so both `reaper-mcp` and `uvx xdarkzx-reaper-mcp` work; existing `"command": "reaper-mcp"` configs are unaffected. Documented as a zero-install option in `docs/INSTALLATION.md`. ### Fixed - **`set_fx_preferences` had no effect on the mix and master pipelines.** Saving a preference (say ReaEQ as the EQ) changed what `fx_list_installed` reported, but `engine_mix`, `engine_master`, `engine_fix_mix`, `setup_drum_bus`, `setup_vocal_chain` and `setup_parallel_compression` chose their plugins from a single "FabFilter or stock" flag (`detect_plugins`) that ignored preferences, so Pro-Q 3 was placed anyway. They now choose a plugin family **per category** (eq, compressor, reverb, limiter) from the inventory and the preferences, and the master chain builds each stage from its own category's family. A preference for one category no longer changes the others. - **A preference the engine can't honor is now reported, not silently dropped.** The engine has parameter maps for two families, REAPER stock and FabFilter (Pro-Q 3 / Pro-C 2 / Pro-R / Pro-L 2). For a preference outside those, or for a plugin that isn't installed, it uses the best supported plugin for that category and lists the reason under `preferences_ignored` in the pipeline's result. `set_fx_preferences` now says up front which of your preferences the engine can use (`engine_support`), and every pipeline result carries `plugin_families`. - **REAPER's stock plugins are now a last resort.** For each category the engine uses ReaEQ / ReaComp / ReaVerbate / ReaLimit only when no supported third-party plugin (FabFilter) is installed for it, even if a saved preference names the stock plugin; that preference is reported under `preferences_ignored`. - **The docs claimed the pipelines add other brands' plugins with fuzzy parameter matching.** They don't; corrected in the architecture notes and the AI instructions. ### Removed - **`detect_plugins`**, the single-flag "FabFilter or stock" detector, superseded by the per-category selection built on the plugin inventory. It was internal, not a tool, so nothing calling the MCP tools is affected. ## [0.8.1] - 2026-09-21 ### Added - **`project_undo` and `project_redo` take a `steps` count (1-100).** Undo just the last two or three commands of a ten-command tool in one call. The response lists every step undone, most recent first, and stops early if the history runs out. A plain call still undoes one step. - **`project_undo_group`** — undo every consecutive step of the most recent multi-command tool run (say, everything `engine_mix` just did) in one call. It stops at the first step that belongs to something else, leaves earlier runs of the same tool alone, and refuses, changing nothing, when the last step didn't come from such a run. ### Fixed - **Undoing an item, MIDI or take edit restored nothing.** Live testing showed that undoing `item_create_midi` reported the step as undone while the item stayed on the track, and the same held for note inserts and the other item, MIDI and take handlers (20 of them, including `midi_insert_notes_batch`, `items_apply`, `item_duplicate`, `item_split_at_positions`, `take_set_pitch` and `wipe_all_midi`). Cause: REAPER's undo blocks do not capture this data when called from the bridge, so the step they leave is empty or missing. These handlers' steps are now recorded by the dispatcher instead, and their own blocks stay out of REAPER. Each command is recorded with exactly one REAPER call, chosen by what it changes: the call that captures items, MIDI and takes for those (`Undo_OnStateChange2`), and the flagged call for everything else (track, FX, send, marker, tempo). Calling both for one command was tried and rejected, because REAPER syncs item state lazily and a tool sending commands back to back sometimes left two identical steps. Tests fail if a command that changes items or MIDI is left to an undo block or recorded with the wrong call. - **Simple edits had no undo step of their own, so undo skipped past them.** Batch and structural tools wrap their work in a named undo block, so one Ctrl+Z reverses a whole call. But a fader, pan, mute, send, marker, item or tempo edit did not: REAPER's undo skipped over it and rolled back an earlier step instead. The dispatcher now records one named step, `MCP: `, after each of 75 such commands succeeds. Failed, read-only, transport, selection, file and undo/redo commands add no step (recording a point after an undo would erase the redo stack). REAPER already records its own step for plugin bypass and reordering, and those stay a single step. - **A multi-command tool's undo steps couldn't be told apart or undone together.** `engine_mix`, `engine_master`, `engine_fix_mix`, `chop_pipeline`, `stack_chop_layers`, `compose_arrangement`, `demo_edm_project`, `load_loops`, `setup_vocal_chain`, `setup_drum_bus`, `setup_parallel_compression`, `setup_sidechain`, `create_drum_pattern`, `create_chord_progression` and `wipe_all_midi` leave one undo step per command they run, which is what lets you take back just the last few. But those steps carried no sign of which tool made them, so undoing "what engine_mix just did" meant counting. These tools now run inside an undo group that labels each step `MCP: # > `. A group only labels and holds nothing open in REAPER (REAPER drops an undo block held open between bridge ticks). It closes even if the tool fails, is skipped harmlessly by an older bridge script, and stops labelling by itself after 60 seconds idle if the server process dies. - **A new command can't silently miss an undo step.** Every handler that changes the project is classified either as recorded or as deliberately exempt with a reason, and a test fails when a handler is added without a decision. Verified on live REAPER with the release code, checking the actual state after each undo. A fader move, item creation, MIDI notes, an item move, a marker and a tempo marker each undo as one step that restores the state. A sequence of 16 changes (three grouped `create_drum_pattern` runs plus item, notes, move, fader, marker, tempo, send and plugin edits) left exactly 16 steps with no duplicates; undoing all 16 (`project_undo(steps=16)`) removed every trace, and redoing all 16 restored the tracks, items, notes, volume, send, plugin, marker and tempo marker exactly. `project_undo_group` undoes a grouped run's steps together and leaves earlier steps alone, and block-based handlers (`setup_fx_chain`, batch track delete, batch plugin removal) still undo cleanly. ## [0.8.0] - 2026-09-21 ### Added - **Master track is addressable as `track_index=-1`.** REAPER keeps the master outside the numbered track list (`GetTrack` never returns it, `GetMasterTrack` does), so the FX tools could not reach it at all. The Lua `get_track` helper now maps `-1` to the master, and every `fx_*` tool that takes a `track_index` accepts it — chain inspection, parameters and scanning, enable/disable, presets, move, rename, UI, and pin mappings. Anything below `-1` is still rejected. - **`setup_fx_chain` and `configure_tracks` accept `-1` too**, so a full mastering chain (EQ, compressor, saturation, limiter — with parameters) can be built in the same call as track FX, without going through `engine_master`, which replaces the master chain. This applies to those two batch entry points only; no other tool gained master access. - **`fx_remove_batch`** — remove one FX, several, or an entire chain, across one or many tracks, in one call. Each entry names a `track_index` (`-1` for master) and exactly one of `all`, `fx_index`, or `fx_indices`. Indices are resolved against every chain as it stands when the call starts and deleted highest-first, so overlapping or out-of-order entries cannot shift an index mid-batch. A bad track or index is reported in the response's `errors` array without aborting the rest, and the whole call is a single undo step. Follows the same pattern as `items_apply` and `markers_apply`. - **`track_delete_batch`** — delete one track, several, or every track in one call. Each entry has exactly one of `all`, `track_index` or `track_indices`. Targets are resolved against the numbering at the start of the call and deleted highest-first, so entry order and overlapping entries can't shift an index. The master track is refused per entry, a bad index is reported in `errors` without aborting the rest, and the response lists what was deleted by original index and name. Takes the usual pre-action project backup and is a single undo step. ### Changed - **Upgrading: update the Lua script together with the package.** The new batch tools depend on handlers in `reaper_scripts/reaper_mcp_server.lua` (`fx_remove_batch`, `track_delete_batch`) and on the master-track and safety changes below. A 0.8.0 server talking to an older script reports "Unknown command" for them. If you installed from PyPI, re-download the script from the repo and restart REAPER. - **`track_delete` (single track) is removed in favour of `track_delete_batch`.** A batch of one entry does everything the old tool did (`[{"track_index": 3}]`). Anything calling `track_delete` by name must switch. The mix engine's cleanup of its own `MIX:` reverb buses now deletes them with one batch call instead of one call per bus. - **`fx_remove` (single FX) is removed in favour of `fx_remove_batch`.** A batch of one entry does everything the old tool did. Anything calling `fx_remove` by name must switch to `fx_remove_batch` (`[{"track_index": 2, "fx_index": 3}]`). The mix engine's cleanup of its own `[MIX]`-tagged FX now removes them with one batch call per track instead of one call per FX. - **The publish workflow now runs `ruff check .` before building.** Lint was a gate only in the CI workflow, so a version tag could publish a commit whose tests passed but whose static analysis had not run. The `build` job now needs both `lint` and `test`. - **Documentation corrected to match the code.** The README's headline said 182 tools; the default profile registers 180 across 26 modules. The module count in `docs/TOOLS.md` was also off by one (it counted a helper module that registers no tools). The instruction-size column in the profile table in `docs/TOOLS.md` is refreshed for the instruction text added in this release. `docs/ARCHITECTURE.md` gains sections on failure containment and on track addressing and the master track. ### Security - **Destructive track handlers refuse the master track in Lua, not just in Python.** Widening `get_track` to resolve `-1` meant ~40 handlers could reach the master track, with only the Python guards in front deciding which. `track_delete_batch` (per entry), `track_freeze`, `track_unfreeze`, `track_set_folder`, `track_set_input`, `track_set_record_arm` and `track_set_state_chunk` now use a `get_numbered_track` helper that keeps refusing `-1` if a Python guard is later loosened. Tests run the real helpers under `lupa` with a stubbed `reaper` to check both behaviours. ### Fixed - **A command that failed part-way could leave REAPER's UI frozen and an undo block open.** Eight handlers (`setup_fx_chain`, `setup_effect_bus`, `setup_sidechain`, `item_clone_to_position`, `item_duplicate`, both `item_split_*` handlers and `take_set_reversed`) hold `PreventUIRefresh` while they work, and about 40 open an undo block, closing both on their last lines. A malformed entry that threw in between was caught by the dispatcher, but the closing calls never ran. Only `setup_master_chain` was protected. The bridge now counts every open and close, and after each command — success or failure — closes whatever it left open, recording partial work as a single undo point and noting it in REAPER's console. One safeguard in the dispatcher covers every handler, including future ones. - **FX tools reported success for an FX index that doesn't exist.** `fx_enable`, `fx_disable`, `fx_move`, `fx_get_params`, `fx_get_preset`, `fx_navigate_preset`, `fx_rename`, `fx_scan_params`, `fx_set_param`, `fx_set_param_by_name`, `fx_set_preset` and `fx_show_ui` called REAPER without checking the index, and REAPER quietly does nothing for one that isn't there — so `fx_enable(track, 99)` came back as a success with the chain unchanged. They now return `fx_index N out of range (chain has M)`. `fx_move` checks its `new_index` the same way. - **`setup_fx_chain` validated almost nothing before reaching REAPER.** A non-object entry raised a raw `TypeError`, a chain item with neither a `name` nor an `fx_index` was silently skipped (reading as a success for something that never happened), and entry counts were unbounded. It now checks entry and chain shapes, `track_index` type and range, `add_mode`, `params` / `params_by_index` keys and finite values, and `preset`, with per-entry errors and caps of 200 entries, 50 FX per track and 1000 parameters per FX entry. - **`safe_path`'s system-directory blocklist missed macOS symlinked directories.** `/etc`, `/tmp` and `/var` are symlinks into `/private/...` on macOS, so a resolved path compared against the unresolved blocklist entry slipped through. Both sides are now resolved before comparing. ## [0.7.1] - 2026-09-16 ### Added - **New `test_lua_syntax.py`** — actually compiles `reaper_mcp_server.lua` with `lupa` in CI. Every existing Lua test checks the source as text, none of them parse it, so a real syntax error could go unnoticed until someone tried to run it in REAPER. ### Fixed - **`production` profile loaded the composition instructions without the tools they reference.** It includes the `composition` instruction pack (which tells the AI to call `get_track_instruments`/`analyze_score`/ `compose_arrangement`) but its `include_modules` never listed `compose_tools`, the module those tools live in — same shape as the earlier `setup_fx_chain`/`compose_edit_tools` gap. Added `compose_tools` to the profile; added a regression test asserting every profile that loads the `composition` pack also includes the module. - **`item_get_all`'s `max_results` did nothing on the actual cost of a large project.** Its Python wrapper validated the cap but never forwarded it to Lua — every call enumerated and JSON-encoded *every* item in the project, wrote the full result to the IPC file, and only then sliced it down to size in Python, discarding the rest. Unlike `midi_get_notes`/`envelope_get_points` (confirmed to already stop enumerating in Lua once the cap is hit), the cap only ever trimmed the final response — none of the real per-call cost. Lua now stops enumerating at `max_results` directly, and reports a `truncated` flag. - **`item_get_all`'s track-filtered branch resolved each item's global index with a per-item linear rescan of the whole project** (O(items on track × total items)) instead of the one-pass pointer-to-index map `item_split_at_transients` already uses elsewhere in this same file for the identical problem. Switched to the same one-pass map. - **`marker_get_all` had no result cap at all** — unlike every sibling bulk-read tool (`item_get_all`, `midi_get_notes`, `envelope_get_points`), a marker/region-heavy project (podcast edits, game-audio cue points) could return an unbounded response. Added `max_results` (default 500, hard ceiling 5000), enumerated with an early stop in Lua the same way, with a `truncated` flag. - **`fx_get_params` had no absolute cap** — it already filters known junk (MIDI CC, internal params, unused FabFilter bands), which handles the common large-plugin case, but a plugin with a genuinely large number of real, non-junk params had no backstop. Added `max_results` (default 300, hard ceiling 2000) with the same early-stop-in-Lua + `truncated` pattern; the underlying `build_fx_params` also gets a default so `fx_set_preset`'s opt-in `include_params` path is protected too, not just the one caller that now passes `max_results` explicitly. ### Security - **`load_loops`'s `file_path` had zero path-safety validation.** It calls the same underlying `item_insert_media` command its own tool wrapper does, but directly, skipping the wrapper's `safe_path` check entirely — the one path parameter in the codebase confirmed to have this gap after auditing every `client.execute` call site that passes a path-like argument. Not a code-execution risk (REAPER decodes media, it doesn't execute it), but inconsistent with every other path-accepting tool. Now validated the same way. - **Recursive folder-scan sandbox escape via symlink/junction.** `safe_path` only validates the top-level path a caller supplies, once — it was never re-checked against anything a recursive walk actually descended into. Verified live: a plain NTFS junction placed inside an otherwise-legitimate, already-validated folder made both `scan_audio_folder` (pathlib `rglob`) and `list_audio_subfolders` (`os.walk`) return files from a completely separate, never-validated directory — a complete bypass of the system-directory blocklist, since escaping via the walk isn't restricted to it at all once outside the validated root. Fixed by re-validating every directory a walk is about to descend into (`prune_unsafe_subdirs`, new shared helper in `path_safety.py`): each subdirectory's real path (after resolving symlinks/junctions) must still resolve inside the original root, or it's pruned before descent. `scan_audio_folder`'s walk moved from `rglob` to `os.walk` to get this pruning hook (`rglob` has no equivalent). - **Added a hardcoded, non-configurable exclude list** for recursive folder scans: known secrets-adjacent directory names (`.ssh`, `.git`, `.gnupg`, `.aws`, `.azure`, `.kube`, `.docker`, ...) are always pruned before descent, and common credential/secret file basenames (`.env`, `id_rsa`, `known_hosts`, `.netrc`, ...) are always excluded from scan results — defense-in-depth on top of the sandbox-escape fix, not user-overridable. - **`safe_path`'s "Path must be absolute" check was dead code** — it ran on the value *after* `os.path.realpath()`, which resolves a relative path against the server process's cwd and makes it absolute, so the check could never fire. A relative path was silently accepted and resolved against whatever directory the server happened to be launched from, instead of being rejected. Now checked on the original input before resolution. (The similar-looking literal `".."` traversal check was confirmed, by contrast, to be genuinely redundant rather than broken — normpath/realpath already collapse traversal sequences before it runs, and the resulting canonical path is what the blocklist check below actually catches; verified live that a traversal path built to land on the real system directory is still correctly blocked.) ## [0.7.0] - 2026-09-11 ### Added - **`list_audio_subfolders`**: fast folder-tree discovery for a large audio library — directory names and a cheap per-folder audio-file count, no per-file metadata work. Use before `scan_audio_folder` on a library of thousands of files to see its shape (genre/category folders are almost always real subfolders) instead of scanning everything blindly. - **`scan_audio_folder` gains `query`/`min_bpm`/`max_bpm`/`role`/`key` filters**, all combinable. `query` is multi-term AND matching against the full path (covers genre/style words and specific naming alike, e.g. "techno kick" or "laugh"). With any filter active, `max_files` caps matches found, not files visited, so the walk searches past non-matching files instead of stopping after the first batch of them — verified live against a real 300+ file, 31-subfolder library (`query="attack"` found 52 matches across the whole tree in one call). - **`loops_tools` (audio search: `scan_audio_folder`, `list_audio_subfolders`, `detect_common_bpm`, `load_loops`) is now available in every tool profile** (`mixing`, `analysis`, `minimal`, `production` were missing it) — finding and importing audio isn't specific to any one workflow. ### Fixed - **`marker_add`/`marker_add_region`/`add_markers_batch` returned a marker/region's reusable display NUMBER mislabeled as (or, in `add_markers_batch`, literally named) an INDEX.** REAPER reuses a freed number once its marker is deleted, so the two diverge as soon as any earlier marker is deleted — verified live: deleting the 1st of 3 markers then adding a 4th returned number=1 (reused) while its real enumeration index was 2. Passing that value into a follow-up marker_delete/marker_edit/markers_apply call would silently target whatever marker actually sits at that index instead. All three now resolve the real index by scanning after the insert (matching `item_duplicate`'s fix for the same class of bug); `add_markers_batch` resolves every entry's index in one pass after all inserts are done, not per-entry, since a later entry positioned earlier in time than one already added would shift that earlier entry's index out from under a per-entry resolution — verified with exactly that scenario. - **`_parse_key` (loop filename key detection) didn't recognize a bare major-key letter** (e.g. "D" with no accidental or major/minor suffix) at all — only keys with an accidental (`F#`) or quality (`Dm`, `Cmaj`) were matched. Real files like `Sub_Bass_D_130.wav` parsed as `key: null`. Now matches bare letters too (accepted tradeoff: a stray single-letter token that means something else, e.g. a take/version/mic label, can occasionally false-match as a key). ## [0.6.9] - 2026-09-11 ### Removed - **`fx_add`, `fx_set_param`, and `fx_set_param_by_name` are no longer exposed as tools.** `setup_fx_chain` already covered every case these handled — including a single plugin on a single track — with strictly more safety: it applies params in the order some plugins require (confirmed: FabFilter Pro-Q 3/Pro-C 2 need a band's Used/Enabled flag written before its other params, or the write has no audible effect). Traced live: an AI session used the single-shot tools directly, hit exactly that ordering gap on a fresh Pro-Q 3 band, and misdiagnosed the silent no-op as "needs the plugin UI open" — it doesn't; headless writes work fine, verified against a real REAPER instance. Also fixed the `production` tool profile, which was missing `compose_edit_tools` (the module `setup_fx_chain`/`setup_effect_bus` live in) despite its own description promising FX-chain support — that gap is what put the AI session on the single-shot tools in the first place. The underlying Lua handlers are untouched (still used internally, e.g. by `demo_tools`). ### Fixed - **`item_clone_to_position` and `chops_create_virtual_slice` had the same two defects reported and fixed in `item_duplicate` (#30/#31)**, found in a hardening pass after that fix landed: both returned a pre-insert `CountMediaItems()` as the new item's index, which is wrong as soon as there's an item on any later track — exactly the scenario `chop_pipeline` and `stack_chop_layers` create by placing chops and harmony layers across multiple tracks in a loop. `item_clone_to_position` also copied the source item's state chunk verbatim, so a pitch-shifted MIDI harmony layer from `stack_chop_layers` would silently alias the source's `POOLEDEVTS` MIDI event pool — editing one layer's notes would edit them all. Both now resolve their index from a post-insert scan, and `item_clone_to_position` reuses the `reguid_item_chunk()` helper `item_duplicate`'s fix introduced. - **`track_template_apply` left two tracks sharing one `TRACKID`**: same identity class of bug as above, one level up. `track_set_state_chunk` pasted a saved track's full state chunk onto the target track verbatim, including the source track's own `TRACKID`. Verified live: saving track A's state and applying it to track B left track B with track A's exact `TRACKID` — with track A still in the project, two live tracks shared one identity (the field REAPER's region render matrix and other per-track GUID references use to tell tracks apart). `track_set_state_chunk` now regenerates `TRACKID` before applying, the same way item chunks are reguided; verified live again after the fix that the target track gets a fresh id matching neither its own original nor the source's. ## [0.6.8] - 2026-08-29 ### Added - **`project_export_audio` now does real, configurable rendering**: previously it silently ignored its own `path`/`format` arguments and just opened REAPER's raw render-dialog action — nothing on disk reflected what was asked for. Now wires up `RENDER_FILE`/`RENDER_PATTERN`/`RENDER_FORMAT`/ `RENDER_SETTINGS` for real, triggered headlessly (no blocking dialog). Two new modes: `source="master"` (one mixed-down file, as before) and `source="stems"` (one file per track in `track_indices`, e.g. for per-layer game-audio exports into FMOD/Wwise). Verified end-to-end against a real project, including a REAPER-native edge case where a track name containing `/` or `\` silently splits `$track`-pattern output into a subfolder instead of one file — now caught with a clear error naming the offending track instead of producing broken output. ### Fixed - **`midi_insert_notes_batch` was O(n²) for large batches**: the Lua handler called `MIDI_InsertNote(..., noSortIn=false)` inside the per-note loop, forcing REAPER to re-sort the take's entire note buffer after every single note instead of once at the end — every other bulk-insert site in the Lua bridge (`compose_arrangement`, `compose_single_track`, `edit_section`) already passed `noSortIn=true`; this one didn't. A few minutes of MIDI across multiple tracks could take 5+ minutes to write. Also routed the Python-side tool through the 600s `execute_long` budget instead of the 30s `execute` default — `MAX_NOTES_PER_TRACK` (10000 notes/call) was always large enough to legitimately exceed 30s, and a client-side timeout doesn't cancel the Lua-side insert, it just stops listening for the response. - **Same O(n²) resort bug in two more places**: `midi_delete_all_notes` and `edit_section`'s range-delete (notes and CCs) called `MIDI_DeleteNote`/ `MIDI_DeleteCC` in a loop with no `MIDI_DisableSort` — those two functions have no `noSortIn` parameter at all, so `MIDI_DisableSort` is the only way to avoid a full resort after every single delete. Both now wrap their delete loops in `MIDI_DisableSort` + one trailing `MIDI_Sort`, matching the pattern already used everywhere else in the file. - **IPC responses had no request/response correlation**: `command.json` / `response.json` were matched purely by being "the next file there," with no id tying a response to the command that asked for it. REAPER's Lua dispatch loop is single-threaded and fully synchronous per command — if the Python client times out and gives up on a slow command, the Lua side keeps running it to completion regardless, and its eventual response could get silently misread as the answer to a *later, unrelated* command sent in the meantime. Commands now carry a UUID the Lua bridge echoes back; responses whose id doesn't match the in-flight request are discarded as stale rather than accepted. Backward-compatible: a response with no id at all (an old, not-yet-reloaded Lua bridge) is still accepted unconditionally. ## [0.6.7] - 2026-08-24 ### Added - **Exact Tool Allowlists & Custom Profiles (Issue #24)**: Extended the profile system from module-level filtering to exact tool-level allowlists/blocklists via `ToolProfile` (`include_tools`, `exclude_tools`, `include_modules`, `exclude_modules`). Introduced `ToolFilterProxy` to dynamically intercept and filter `@mcp.tool()` registrations during module loading. - **Profile Configuration Files & Overrides (Issue #24)**: Added support for loading custom profiles from local `.toml` or `.json` files via `REAPER_MCP_PROFILE_FILE`, plus fine-grained environment variable overrides: `REAPER_MCP_INCLUDE_TOOLS`, `REAPER_MCP_EXCLUDE_TOOLS`, `REAPER_MCP_INCLUDE_MODULES`, and `REAPER_MCP_EXCLUDE_MODULES`. - **Profile-Scoped Instruction Packs (Issue #24)**: Split the monolithic instruction file into 8 composable markdown packs (`00_critical_rules.md`, `10_composition.md`, `20_automation.md`, `30_mixing.md`, `40_editing.md`, `50_postproduction.md`, `60_bbc_spitfire.md`, `70_style_cheat_sheet.md`). Profiles now only load the instructions relevant to their active tools, cutting instruction context by up to ~85% for narrow profiles while keeping safety rules intact. - **Profile Introspection CLI (Issue #24)**: Added `reaper-mcp --profile-info [--json]` and `describe_profile(...)` to measure registered tool counts, tool schema size, and instruction token footprints. - **Read-Only FX Pin Mappings Inspection (Issue #23)**: Added `fx_get_pin_mappings` tool wrapping REAPER's `TrackFX_GetPinMappings` to inspect input and output pin configurations (channel lists and raw 32-bit bitmasks) non-destructively before automating sidechain or multi-channel parameters. - **Extended Send Inspection & Channel Mapping (Issue #23)**: `send_get_all` now reports `send_mode` (`raw` integer and human-readable `name` such as `post_fader`, `pre_fader_post_fx`) and `audio` routing (`source_channel_raw`, `destination_channel_raw`, and human-readable `interpreted` string e.g. `1/2 -> 3/4`). - **Track Channel Count (Issue #23)**: `track_get_all` and `track_get_info` now include `channel_count` (`I_NCHAN`), normalizing REAPER's `0` default sentinel to `2` so callers always receive a concrete channel count. ## [0.6.6] - 2026-08-22 ### Fixed - **`infer_curve` returned `"unknown"` for every bipolar parameter** (EQ band gain, compressor makeup, pan, trim) instead of `"linear"`, because `_NUMBER_RE` only accepted a leading `-`, not `+` — plugins format bipolar controls with an explicit `+` above zero, so the sample above zero failed to parse and `numeric_count` dropped below `len(samples)`. Reported with an exact repro against a real FabFilter Pro-Q 4 band-gain sweep by **[@SNChicago](https://github.com/SNChicago)** in [#22](https://github.com/xDarkzx/Reaper-MCP/issues/22), including the root cause, the one-character fix, and why the existing test suite (unipolar only) couldn't catch it. Thank you! ## [0.6.5] - 2026-08-22 ### Fixed - **The `mixing` tool profile excluded `setup_fx_chain`/`setup_effect_bus`** (they live in `compose_edit_tools.py`, a module-naming/boundary mismatch, not deliberate) — real, confirmed impact: an MCP client scoped to `mixing` for FX/mix work couldn't reach the batch FX-setup tools at all, silently pushed back toward many individual fx_add/fx_set_param calls instead, exactly the per-call overhead the profile system exists to avoid. `mixing` now includes `compose_edit_tools`: 82 tools (was ~71), still a 55%+ cut from `full`'s 181. Corrected the profile table's tool counts in `docs/TOOLS.md` too (were stale approximations). - **`setup_fx_chain` (batch add/configure FX across tracks — has existed since 2026-07-17) had two real robustness gaps that plausibly explain why callers avoided it in favor of many individual fx_add/fx_set_param calls despite it already covering that case:** a single bad `track_index` aborted the *entire* batch with a hard error and no partial results, inconsistent with a bad FX name/index within a track (already handled gracefully, per-entry); and a `params`/ `params_by_index` entry that didn't match any real parameter was silently dropped with no error at all. Both now report per-item in the response's `summary` instead — a bad item fails on its own, the rest of the batch still completes. Also rewrote the tool's docstring with concrete examples (same plugin to several tracks in one call; batch- editing an already-existing FX's params by index) — the underlying capability wasn't new, just not discoverable enough. - **`fx_scan_params` only sampled 3 fixed points (0.0/0.5/1.0), which is useless for stepped/categorical parameters** like Pro-Q 3's "Shape" dropdown (~8 real values: Bell, Low Shelf, Low Cut, High Shelf, High Cut, Notch, Band Pass, Flat Tilt) — only ever revealing 3 of them and forcing manual binary-search guessing for the rest, a real, reported pain point. Now uses REAPER's own `TrackFX_GetParameterStepCount` to detect genuinely stepped parameters and sample *every* discrete value exactly, and samples continuous parameters (Frequency, Gain, etc.) at 9 points instead of 3 for meaningfully better interpolation precision. Each sample now also reports its exact normalized value alongside the formatted display string, not just the display string alone. `infer_curve` (in `reaper_mcp_shared/plugin_cache.py`) now trusts REAPER's step-count signal directly instead of guessing from sample patterns, and works with however many points were actually sampled (was hardcoded to exactly 3). - The per-machine VST/AU parameter scan cache (`reaper_mcp_shared/ plugin_maps/`) was never gitignored — a real risk of accidentally shipping one machine's specific-plugin-version scan results to every other user. Now ignored; deleted the one stale (old 3-point-sampling format) cache file already present. ## [0.6.4] - 2026-08-12 ### Fixed - **`bounce_stems` could render silent/blank stems** — a real, confirmed incident, not a hypothetical. Its underlying REAPER action (40892) is actually named "Track: Render tracks to stereo stem tracks, obeying time selection" — with no active time selection (or a stale one left over from something unrelated), the render can come back blank even though the new stem tracks and audio items are created. `bounce_stems` now computes the real extent of the source tracks' own items and sets that as the time selection before invoking the render, so it always covers the actual content regardless of whatever time selection happened to be active beforehand. The extent computation is a small, directly-tested pure function (`compute_items_extent`) rather than inline logic, so this doesn't need REAPER/IPC mocking to verify. - **Installers now guard against a real package-rename collision.** This project's PyPI distribution name changed twice (`reaper-mcp` → `reapermcp` → `xdarkzx-reaper-mcp`) while the console command stayed `reaper-mcp` throughout. A machine that already had an old-named install present could end up with a broken, half-replaced `reaper-mcp` launcher that invoked the wrong code entirely after updating — a real, confirmed incident. `install.bat`/`install.sh` now uninstall any old-named package first, so existing users updating can't hit this on a future pull. ### Added - **`fx_scan_params`**: an on-demand fallback tool that sweeps a plugin's parameters to learn their real range/units/curve shape, for the cases where reasoning from general knowledge genuinely isn't enough (obscure/ freeware plugins, surprising parameter behavior) — not a routine "understand this plugin" tool. Backed by an on-disk plugin param cache with curve inference, so a scanned plugin doesn't need re-scanning. ### CI and Repository Updates None of this changes the package itself. - Added `ruff` to CI, scoped to the `F` (real bugs) and `S` (security-relevant patterns) rule categories, not style or formatting. Cleaned up everything it found: 19 dead imports, 1 dead variable, restructured two `__init__.py` files' re-exports to the explicit `X as X` form, documented 4 existing intentional non-fatal exception handlers and 3 seeded-random-for-reproducibility cases that were false positives, and switched 4 temp-file-path spots to the standard `tempfile.gettempdir()`. - Branch protection on `main`: merging requires CI to pass and at least one review. - Dependabot enabled for Python dependencies and GitHub Actions versions, with automatic security-update PRs and vulnerability alerts. - Added `SECURITY.md` and enabled GitHub's private vulnerability reporting. - GitHub Actions bumped to their latest majors: `checkout` and `setup-python` to v7, `upload-artifact` to v7, `download-artifact` to v8. ## [0.6.3] - 2026-08-02 ### Fixed - **REAPER crashing (STATUS_ACCESS_VIOLATION) during `engine_mix`/ `engine_master`, confirmed via minidump analysis to be FabFilter Pro-Q 3/Pro-C 2 crashing on parameter writes.** `setup_fx_chain` applied FX params via plain Lua `pairs()`, which has no guaranteed order for string-keyed tables — a Pro-Q 3 band's Frequency/Gain/Q could get written before its Used/Enabled flags, hitting internal plugin state that isn't initialized yet. Two independent crash dumps hit the identical code offset inside Pro-Q 3, the signature of a deterministic trigger rather than random overload. Params are now applied in a fixed order (numeric ascending for indexed params, so Used/Enabled always come first; alphabetical for named params) at all three call sites — track, bus, and master FX chains. - **A crash mid-command could leave the client stalled for the full timeout (up to 600s on `execute_long`) instead of surfacing the failure.** The mid-poll crash check tested whether the heartbeat lock file still *existed*, but a crash doesn't delete that file — it just stops updating it, so the check never actually fired. Now checks heartbeat *staleness* (same logic already used pre-flight), so a crash surfaces in roughly the heartbeat-stale window instead of the full command timeout. ## [0.6.2] - 2026-08-02 ### Fixed - **Rock mix profiles (`classic_rock`, `alt_rock`, `hard_rock`, `pop_rock`, `punk`, `post_rock`) had no role for a single consolidated "Drums" track** — only kick/snare/hats/toms/cymbals as separate tracks. A project using one drums track (e.g. a single multi-articulation drum VSTi) silently got no EQ/comp/reverb from `engine_mix` at all. Added a `drums` role to all six profiles, reusing the existing (previously unwired) `drums_bus()` / `comp_drums_bus()` bus-glue treatment, tuned per subgenre to match each profile's existing kick/snare character. - **`lead_guitar` and `clean_guitar` roles didn't match their own natural track names.** Their alias lists only had abbreviated forms ("lead gtr", "clean gtr"), so a track literally named "Lead Guitar" or "Clean Guitar" resolved to no role and got skipped by every genre profile that uses them, not just rock. Added the full-word spellings alongside the abbreviations. ## [0.6.1] - 2026-07-28 ### Fixed - **`script_list` froze REAPER's UI for minutes on large Scripts installations** (thousands of community scripts, e.g. the ReaTeam pack). It parsed every matching script's header (a real file open/read) before checking the filter, so a filtered search never hit the 300-result cap and ended up opening every single file. Now filters against the path first and only parses headers for matches — filter no longer matches against description text, only path, which is the trade-off that makes this fast. Also skips hidden/VCS directories (`.git`, `.svn`, ...) during the scan, since community packs are often git checkouts. (issue #4) ## [0.6.0] - 2026-07-25 ### Added - **`project_get_change_count`** — cheap check for whether the project has changed since you last looked (wraps REAPER's `GetProjectStateChangeCount`), so the AI can decide whether to re-fetch heavier data instead of blindly re-querying every turn. (issue #4) - **`items_apply`** — batch set position/length/volume_db/mute/fade, or delete, across many items in one call. Non-delete changes apply first; deletes apply last in descending item_index order regardless of input order, so a delete never shifts the indices of items processed later in the same batch. Per-entry error collection — a bad item_index doesn't abort the rest of the batch. (issue #4) - **ReaScript integration** — `script_list` discovers `.lua`/`.eel` scripts in REAPER's own Scripts folder (never an AI-supplied path); `script_run` registers and runs one, optionally waiting up to 30s for a result the script reports via a documented `ExtState` convention. Lets the AI trigger scripts users have already written, instead of reimplementing that logic as new MCP tools one at a time. (issue #4) - **Edit-oriented QC analysis** — `analyze_silence` and `analyze_peaks` find silence and click/pop candidates in a rendered WAV file; `analyze_region_qc` runs both scoped to each region from `marker_get_all()`, producing a per-region punch list for post-production review. These flag *candidates* for human review, not certainties. Breath detection is deliberately not included — reliably telling a breath apart from quiet room tone needs spectral/ML classification, not an amplitude heuristic; it's left for a real ML-based approach later rather than shipping something unreliable under the same name. This whole feature needs real-world testing with post-production users before its output should be trusted as more than a rough first pass. (issue #4) - **`markers_apply`** — batch edit (name/position/start/end/color) or delete markers and regions in one call, closing the other half of issue #4's batch-combinator request (`items_apply` covered media items; this covers markers/regions — creation was already batched via `add_markers_batch`, only edit/delete were still one-at-a-time). Also extends marker editing to support a region's `end` and `color`, which the existing single-marker `marker_edit` never supported. Non-delete changes apply first; deletes apply last in descending marker_index order regardless of input order, so a delete never shifts the indices of markers processed later in the same batch. Per-entry error collection — a bad marker_index doesn't abort the rest of the batch. (issue #4) - **`project_get_overview`** — one cheap call bundling track/item/marker counts, the region list, `change_count`, and a lightweight selection summary (indices/counts, not full detail). Closes a gap found re-auditing issue #4 item 1: the original request asked for this as a cheap single read, but the shipped implementation required composing up to 6 separate calls to assemble the same picture. Purely additive — every existing granular tool (`project_get_info`, `marker_get_all`, `selection_get_*`) is unchanged. (issue #4) ## [0.5.0] - 2026-07-20 ### Added - **LM Studio support.** `install.sh`/`install.bat` now also configure LM Studio's `mcp.json` (`~/.lmstudio/mcp.json` on macOS/Linux, `%USERPROFILE%\.lmstudio\mcp.json` on Windows) — same `{"mcpServers": {...}}` shape as Claude Desktop's config, just a different file. Documented in `docs/INSTALLATION.md` and added to the README's supported-clients list. - README: git-availability check before the clone instructions — a fresh Windows install doesn't ship with git. Added a `winget install Git.Git` step (with a direct git-scm.com fallback if winget itself isn't present) ahead of `git clone`, and a one-line check/install note for macOS/Linux. - **Automatic pre-action backup for destructive operations.** `wipe_all_midi`, `track_delete`, `item_delete`, and `engine_mix`/`engine_master` with `clean=True` (which removes existing FX before applying new ones) now save a timestamped snapshot copy of the project — once per project per session — before the destructive action runs, via a new shared `reaper_mcp/safety.py` helper. This is on top of, not a replacement for, REAPER's own undo history (already wrapped around every MCP action) — a durable on-disk copy survives a crash or an undo-depth limit that in-memory undo doesn't. A failed backup attempt logs a warning and lets the requested action proceed rather than blocking it. - `project_backup(path)` — new tool: save a snapshot copy without changing the project's active file (unlike `project_save_as`, the next `project_save` still targets the original file). Powers the auto-backup above; also directly usable. - `project_get_info` now returns `file_path` — the actual `.rpp` project file path (empty if never saved). The pre-existing `path` field is the recording/media directory, not the project file, and was easy to mistake for one. - `00_core.md`: new guidance to describe destructive/hard-to-reverse actions on an existing project and wait for confirmation before running them, unless the user already gave clear specific instruction to do exactly that. Additive work (new tracks, MIDI on empty tracks, adding FX) proceeds directly as before. - **REAPER auto-start on launch.** `install.sh`/`install.bat` now drop a `__startup.lua` into REAPER's `Scripts` resource folder — REAPER runs any script with that exact name automatically on every launch, natively, no Action-list registration needed. Removes the "load the Lua script every time REAPER opens" manual step for anyone who used the installer. If a `__startup.lua` already exists for something else, the installer appends to it (backed up as `__startup.lua.bak`) instead of overwriting it. Falls back to the old manual-load instructions if REAPER's resource folder doesn't exist yet (i.e. REAPER has never been run) — re-running the installer after REAPER's first launch picks it up. - `engine_mix`'s v2 pipeline now returns `unmatched_tracks` — tracks whose name didn't match any role alias got no volume/EQ/comp, and any sidechain depending on them (e.g. kick→bass) silently never fired, with nothing in the response indicating why. A track named "Drums" (very normal for a single combined drum-machine track) never matches the "kick" alias list (`kick`/`bd`/`bass drum`/`kik`), so a trap mix could report full success with 0 sidechains applied and no clue what happened. - `tests/test_patterns_tools.py` and `tests/test_safety.py` — 16 new tests covering the item-placement/pattern-tiling fixes and the auto-backup decision logic below. Full suite: 91 passing (was 75). - **Real cross-process mutex around the REAPER IPC bridge.** Two reaper-mcp server processes (e.g. Claude Desktop and a separate terminal session both driving the same REAPER instance) previously had no protection against racing on the shared `command.json`/`response.json` files — one server's response could get read by the other's request. `_ipc_mutex` in `reaper_mcp/reaper_client.py` now holds a real OS-level lock (`msvcrt.locking` on Windows) for the duration of each full command round-trip, so a second process just waits its turn instead of racing or silently reading the wrong response. - **Superseded-server self-retirement.** When a client reconnects and spawns a replacement server without stopping the old one, the old process now notices and exits on its own. Each server writes its own PID to a small per-parent-PID marker file on startup (`GENERATION_DIR`); every server (including itself) watches that file, and if it ever reads back a PID that isn't its own, it retires — self-directed only, no process ever terminates another one. Verified with a direct claim/read/re-claim test of the marker file, not just read through. - `item_get_all`/`item_get_info` now return `source_file` (full path) and `source_filename` (basename) for audio items, and `track_get_all`/ `track_get_info` now return `sample_filenames` (distinct audio filenames across a track's items, capped at 20). Previously the only exposed name was the take's editable display name, which sample-pack vendors (Splice, etc.) don't reliably preserve — vendors commonly embed BPM/key in the actual filename itself ("Karra_Vocal_Loop_120bpm_Cmin.wav"), which was invisible without a filesystem-level check outside the AI's reach. The underlying source-resolution logic already existed internally for `chop_pipeline`; this surfaces it through the general track/item inspection tools instead of leaving it chop-pipeline-only. - `reaper_client.py`: diagnostic stage-by-stage timing log (`%TEMP%/reaper_mcp/slow_commands.log`) for any command taking over 1s — check_server / mutex_wait / cleanup / write / poll / poll_iters, per command. Exists to diagnose real slow requests from an actual long-running server process, which isolated fresh-process timing tests can't reproduce. - `00_core.md`: new Automation/Envelopes section — `envelope_clear_range` (deletes points) vs. writing new points are not interchangeable ways to "clear" automation, a worked dB→linear-gain conversion table, and a reminder to verify writes with `envelope_get_points` rather than assume. - **`create_drum_pattern`/`create_chord_progression`/`load_loops` read the auto-created item/track index from the wrong place in the bridge response.** Bridge responses nest their payload under `data` (`{"success":true,"data":{"index":N,...}}`), but these three call sites read `index`/`item_index`/`track_index` from the top level, which is always absent — so they silently fell back to `0`/`None`. In practice: pattern tools always targeted project item 0 regardless of track (only worked by accident on an empty project's first item; any multi-track use wrote to the wrong item or failed with "Item is not on specified track"), and `load_loops`' track-create path (the common case — loading into a project without a pre-existing matching track name) silently loaded 0 loops. Fixed to read from `result["data"]` with a flat-dict fallback, matching the pattern `chops_tools.py` already used. (PR #5) - **`project_save_as` never actually saved to the given path.** Its Lua handler called `reaper.Main_SaveProject(0, path)` — that function's second argument is a boolean ("force the Save-As dialog"), not a filename. Passing a string there is truthy in Lua, so every call just popped REAPER's interactive Save-As dialog and silently ignored the path entirely, which would hang any non-interactive caller waiting on a result. Switched to `Main_SaveProjectEx(0, path, 8)`, the actual non-interactive save-to-path API (option bit 8 = make this the active project file going forward, matching normal "Save As" semantics). - **`create_drum_pattern`/`create_chord_progression`: auto-created items always landed at project position 0, ignoring `start_qn`.** Since `midi_insert_notes_batch` writes notes at absolute project time (not item-relative), a second call for a later section (e.g. a verse at bar 5 after an intro at bar 1) created an item that overlapped the first one at position 0 instead of sitting after it — reproduced live: two 4-bar sections both landed with their item boundary at t=0. Item position now equals `start_qn` converted to seconds. - **A drum-pattern lane shorter than `steps_per_bar * bar_count` silently only filled the first bar, leaving the rest empty — no warning.** Easy to trigger by assuming a 1-bar line auto-repeats across `bar_count` bars (a reasonable assumption the tool didn't honor). Now a line exactly `steps_per_bar` long auto-tiles across all bars; any other mismatched length raises an error instead of quietly truncating. - The first fix above initially added a small trailing pad to auto-created item lengths as a safety margin; that pad itself caused a smaller overlap between two sections placed back-to-back with no gap (confirmed proportional: a 0.5s pad overlapped by 0.5s, a 0.05s pad by 0.05s). Removed entirely — each drum note already ends before the pattern's nominal end (see the `gate` shortening), and a chord's last note ends exactly at the same point the item does, so no pad was ever needed. - `compose_arrangement` had a `bpm` parameter, documented as "Project BPM (default 120)", that was never referenced anywhere in the function body — a dead, misleading parameter. Shorthand note/CC times are raw seconds, entirely tempo-independent (unlike `create_drum_pattern`/ `create_chord_progression`, which take quarter-notes and convert using the live project BPM). Removed the parameter; docstring now states the seconds-vs-quarter-notes split explicitly so the two conventions aren't mixed up. - **Server self-terminated on an ambiguous parent-liveness check.** The parent-watchdog thread (`_watch_parent`/`_parent_alive` in `reaper_mcp/main.py`) treated a failed `OpenProcess()` handle as proof the parent had exited — but that call can fail for reasons unrelated to the parent's actual state, e.g. the parent running in a different Windows security context (an MSIX/AppContainer sandbox, which is how Claude Desktop's Windows Store build runs its children, vs. an unsandboxed terminal session). That false "dead" reading killed a perfectly healthy server within ~3s of startup. Now fails open — an inconclusive check means "assume alive," not "assume dead" — and requires 3 consecutive confirmed-dead readings (not one) before actually exiting. Verified directly against a nonexistent PID, a live PID, and a genuinely-exited PID; all three now return the correct result. - **The kill-based single-instance guard was removed entirely** — `_claim_single_instance()` tried to prevent IPC cross-talk by `TerminateProcess`-ing whatever PID it found in a shared claim file on every server startup. Destructive, and unreliable across the same security-context boundary noted above. The actual shared resource is now protected properly (see the cross-process mutex above) instead of fighting over who gets to exist. - **Server crashed on effectively every connection, surfacing to the client as "Server disconnected" with no further detail.** Python's default stdio encoding on Windows follows the OS's legacy ANSI codepage (`cp1252` on the machine this was found on), not UTF-8, unless the system has opted into "Use Unicode UTF-8 for worldwide language support" (off by default) or the process explicitly requests it. MCP's stdio transport requires UTF-8 JSON-RPC framing, and several tool docstrings use non-ASCII characters (em dashes, arrows like "kick→bass" in the sidechain tools). `tools/list` — one of the first calls any client makes on connect — sends every registered tool's description in one response; the instant that response tried to write a character outside `cp1252`'s range to stdout, it raised an unhandled `UnicodeEncodeError` deep in the transport layer and killed the process. Reproduced directly: a `tools/list` call against the unpatched server crashed it every time; the same call against the patched server returned all 164 tools successfully and the process stayed alive. Fix: `sys.stdout`/`stdin`/`stderr` are explicitly reconfigured to UTF-8 at the very top of `reaper_mcp/main.py`, before anything else touches stdio, so behavior no longer depends on the parent process's or OS's codepage. Marked unverified pending a live Claude Desktop retest — this note gets the caveat removed once confirmed. - **Batch coloring silently painted everything black instead of erroring on a malformed `color`.** The same buggy pattern — `entry.color[1]` indexing with no shape validation, defaulting any missing/wrong-typed component to 0 — was duplicated across 5 places in the Lua bridge (`track_set_color`, `marker_add`, `marker_add_region`, `configure_tracks`, `add_markers_batch`, `setup_effect_bus`'s `bus_color`). Neither side documented or validated the `[r,g,b]` array shape, so a caller passing an `{"r":..,"g":..,"b":..}` object, a hex string, or 0.0-1.0 normalized floats (a reasonable guess, since other params in this API use that scale) got every track quietly set to black with no error. Separately, `setup_effect_bus`'s Python-side `bus_color` handling had its own bug: a malformed value was silently dropped (`except: pass`) rather than erroring or applying. Consolidated into shared, validating helpers on both sides — Lua: `native_color`/`native_color_or_none`/`native_color_from_array`; Python: `_validate_color_array` in `compose_edit_tools.py` (paired with the pre-existing `marker_tools._validate_color` for the separate-scalar-args tools). Malformed input now raises a clear error naming the expected `[r, g, b]` (0-255 whole numbers) shape instead of silently going black; verified directly against every malformed shape above, including the 0-1 normalized-float case specifically. - **`envelope_add_points` accepted raw dB values (which are negative, e.g. -6, -138) as a gain-envelope `value` with no validation, silently writing invalid/nonsensical automation instead of erroring.** Volume/Width track envelope values are linear gain (1.0 = unity/0dB) and can never be negative — reproduced live: asking the AI to reset a track's volume automation resulted in points literally valued -138, corrupting the track instead of resetting it. Root cause was two-fold: nothing rejected an invalid value at the tool boundary, and `00_core.md` never documented the dB→linear-gain conversion or that clearing automation means `envelope_clear_range` (deletes points), not writing new ones over the old ones. Fixed both — `envelope_add_points` now rejects negative gain values, out-of-range Pan (outside -1..1), non-0/1 Mute, and out-of-range FX-param values with an error naming the correct conversion; `0.0` itself is still accepted since literal silence is a legitimate value, only invalid/negative values are rejected. Verified directly against the exact -138 case plus every other invalid shape. - **The UTF-8 stdio fix (above) was needlessly gated to `sys.platform == "win32"`.** The underlying requirement — MCP's stdio transport needs UTF-8 JSON-RPC framing — isn't Windows-specific; a minimal/Docker Linux image or any system still defaulting to the POSIX/C locale (ASCII-only, not UTF-8) hits the identical crash. Reconfiguring stdout/stdin/stderr to UTF-8 is a harmless no-op on a system that's already UTF-8, so there's no reason to gate it — now applied unconditionally on every platform. Verified end-to-end (not just compiled): a fresh server handled initialize → `tools/list` (all 164 tools, arrow character intact) → a real `tools/call`, stayed alive throughout, zero malformed protocol lines. Run twice for consistency. - **Full security audit** — no code-execution primitives found anywhere (`subprocess`/`os.system`/`eval`/`exec`/`pickle` all absent from the Python side; the Lua JSON parser is a genuine hand-written recursive-descent parser, not a `load()`/`loadstring()` shortcut, so a crafted tool argument can't reach Lua code execution). No network surface at all — this is 100% local file-based IPC, nothing binds a port or makes outbound calls. Three real findings closed: - `analyze_loudness`/`analyze_clipping`/`analyze_frequency_spectrum`/ `analyze_stereo_field`'s `wav_path` and `scan_audio_folder`'s `path` were the only path parameters in the codebase not routed through the system-directory/traversal guard every write-capable tool (`project_open`, `project_save_as`, `project_backup`, `project_export_audio`, `item_insert_media`) already used. Low risk in practice (both read-only; `analyze_*` additionally constrained by `soundfile.read()` rejecting anything that isn't a real audio file), but inconsistent. The guard itself was also duplicated verbatim between `item_tools.py` and `project_tools.py` — consolidated both into a new shared `reaper_mcp_shared/path_safety.py`, now used by all four modules. - The Lua `os.execute` `mkdir` fallback (only reachable on very old REAPER builds lacking `RecursiveCreateDirectory`, which the primary path already uses instead) stripped embedded double-quotes from the path before shelling out, but that denylist didn't block `$(...)` / backtick command substitution, which bash still evaluates inside double quotes. Switched to an allowlist — only path-legal characters (alphanumeric, `-`, `.`, space, `:`, `/`, `\`) survive, everything else becomes `_` — closing the gap regardless of which specific character would have been dangerous. Exploiting the original version would have required an attacker who could already set the `TMPDIR` env var for the REAPER process, at which point they don't need this bug — low real-world risk, still worth closing properly. - On a shared multi-user Unix machine, the IPC temp directory (falls back to a bare `/tmp/reaper_mcp` when `TMPDIR` isn't set) wasn't isolated per-user — a different local user could potentially read or write into it. New `ensure_private_dir()` helper in `reaper_mcp_shared/constants.py` creates the directory and best-effort `chmod`s it to `0700` (owner-only); if a different user already owns it, the chmod fails harmlessly and subsequent access attempts then fail with a normal permission error instead of silently sharing state. Deliberately did NOT go with per-user directory naming (e.g. embedding the username in the path) — that would require Python and Lua to independently resolve the same username and agree, and any divergence between the two (missing env vars, edge-case locale) would silently break the IPC bridge entirely, a worse outcome than the issue being fixed. Not a Windows concern — `%TEMP%` is already per-user there by default. All three verified directly (permission bits after `ensure_private_dir`, system-dir rejection on the two previously-unguarded tools, legitimate paths still accepted). 91/91 tests pass. ## [0.4.0] - 2026-07-20 ### Added - **10 new mix-engine styles across 3 new genre families — Jazz, Orchestral, Funk/Soul (25 → 35 styles).** - **Jazz** (`swing_jazz`, `jazz_fusion`, `latin_jazz`): near-zero bus compression, wide dynamic range (-13 to -16 LUFS), no sidechain — dynamics are the performance, not something to flatten. New shared presets: `upright_bass`, `piano_jazz`, `ride_cymbal`, `vocal_jazz`, `comp_jazz_gentle`. - **Orchestral** (`classical_chamber`, `cinematic_trailer`, `ambient_orchestral`): pairs with the existing BBC Spitfire CC guidance in `00_core.md`. Minimal-to-zero bus compression, -16 to -20 LUFS targets (closer to a classical/broadcast master than streaming loudness — intentional). These are **consolidated-section** profiles (one "strings"/"brass"/"woodwinds"/"choir" track each) — for a full per-instrument multi-mic template (separate Violin 1/2, Viola, Cello, individual winds/brass), the pre-existing legacy orchestral path in `mix_engine/profiles.py` is still the finer-grained option and is what any style name outside the v2 catalog routes to (empty string included) — this was already true before this change and still is. New shared presets: `strings_section`, `brass_orchestral`, `woodwinds`, `choir`, `timpani_perc`, `comp_orchestral_glue`. - **Funk/Soul** (`classic_funk`, `motown_soul`, `neo_soul`, `disco_funk`): fast transient-catching compression instead of loudness-flattening compression. No kick↔bass sidechain in any of them — funk bass and kick play in rhythmic unison, so ducking one under the other fights the pocket instead of serving it. `neo_soul` uses a genuinely-pro technique instead — a subtle vocal→keys/horns duck (amount 0.2-0.25, slow release) so the vocal breathes room without riding a fader through every phrase; `disco_funk` uses a light kick→strings duck, since disco sits closer to dance music than the other three. New shared presets: `slap_bass`, `electric_piano`, `horns_section`, `vocal_soul`, `comp_funk_punch`, `comp_soul_vocal`, `comp_horns`. - `engine_mix`/`engine_master`'s docstrings and `00_core.md`'s style cheat sheet updated with per-family mixing tips for all three. - **`chop_pipeline` — end-to-end vocal-chop arrangement (Phase 3)**. One tool call produces a real chopped vocal on a NEW track — not sliced-in-place, not pitched-in-place. Creates a "Vocal Chops" track, reads the source WAV behind a vocal item, generates 1/16-note candidate slices, applies a style-specific rhythmic pattern (which slots get chops vs. gaps), pulls slices from a SHUFFLED source-offset order so the arrangement is truly reordered (not sequential playback), pitches each placement to a chord tone, optionally stutters and harmony-stacks, adds 5 ms fades on every chop edge to prevent clicks, and mutes the original vocal. Styles encode chop-production craft so the AI doesn't have to re-derive it per call: `chillstep` (sparse 6/16), `future_bass` (dense 11/16 + 40% harmony stacks), `porter` (syncopated 9/16 with stutter clusters), `trap` (percussive 7/16). Two new Lua handlers back it: `item_get_source_info` (resolves the source file path + offset behind an item) and `chops_create_virtual_slice` (creates an item on a target track that references a specific time range of a source WAV — no audio bouncing required). - **Vocal-chop Phase 2 helpers** (3 more tools on top of Phase 1 primitives): - `analyze_chop_set(item_indices)` — calls `item_get_info` per chop, classifies duration as `hit` / `staccato` / `syllable` / `sustain` so the AI can pick chops appropriate to each musical role without doing audio content analysis. - `arrange_chops_to_chord_tones(item_indices, chord_progression, beats_per_chord, bpm, layout, source_root)` — high-level pitch arranger. Walks chops in playback order, computes which chord each falls into (based on time + `beats_per_chord` + project tempo), picks a chord tone per `layout` (`follow` / `ascending` / `porter` / `root`), and applies pitch shifts via `take_set_pitch`. The Porter Robinson signature pattern (root → 5th → octave → 5th) is built in. - `stack_chop_layers(item_indices, intervals_semitones)` — for each chop, creates overlay clones on the same track at parallel pitch intervals. Default `[7, 12]` = perfect 5th + octave (the future-bass harmonized stack). Capped at 50 source chops to avoid mixer overload. - New Lua handler `item_clone_to_position` to support `stack_chop_layers` overlay placement; existing `item_duplicate` enhanced to return new item indices in its response. - Tool surface: 159 → 162 across 25 modules. - **Vocal chops primitives** (`chops_tools.py`, 6 tools) — building blocks for slicing, pitching, time-stretching, reversing, and duplicating audio items. Style-agnostic; the AI brings the artistic decisions, these tools do the mechanical work. Designed for the workflow where the user loads a vocal manually onto a REAPER track and the AI inspects it via `track_get_all` + `item_get_all`, then chops: - `item_split_at_transients(item_index)` — REAPER's native transient-split action wrapped to return the resulting chops in playback order with their indices and offsets. - `item_split_at_positions(item_index, positions)` — manual split at a JSON list of absolute project-time positions. For grid-based chopping or hand-picked slice points. - `take_set_pitch(item_index, semitones, take_index=-1)` — per-take pitch shift in semitones via `D_PITCH`. Float, range -60..60. The core of "tune a chop to a chord tone". - `take_set_playrate(item_index, rate, preserve_pitch=True)` — time-stretch via `D_PLAYRATE`. With `preserve_pitch=True` (default) audio plays slower/faster without pitch change. - `take_set_reversed(item_index)` — reverse via REAPER's "Reverse items as new take" action. Original take preserved. - `item_duplicate(item_index, count, spacing_sec=0)` — copy an item N times via `SetItemStateChunk` (preserves all take properties). Default spacing = item length for back-to-back placement. - Tool surface: 153 → 159 across 25 modules. - **Loop-library pipeline** (`loops_tools.py`, 3 tools) — point the AI at a sample-pack folder (Prime Loops, Splice, Loopmasters, Native Instruments Expansions) and it builds a working REAPER session from the loops it finds: - `scan_audio_folder(path, recursive=True, max_files=500)` — walks a folder, parses BPM / key / role from each filename via regex (`Kick_140BPM_Am_01.wav` → `bpm=140, key="Am", role="kick"`). Returns distribution summary so the AI sees the dominant tempo / key cluster at a glance. Handles `.wav`, `.mp3`, `.flac`, `.aif`, `.aiff`, `.ogg`, `.m4a`. - `detect_common_bpm(file_paths)` — given a JSON array of paths, returns the most common BPM with per-value vote counts and a confidence score. - `load_loops(loops, project_bpm=0)` — batch-load loops onto auto-created tracks. Finds or creates a track by name for each entry, sets project BPM if provided, returns per-entry errors without aborting the batch. - Uses only stdlib + (optional) soundfile for duration; no librosa / heavy DSP required. - Typical pipeline: `scan_audio_folder` → Claude picks loops → `transport_set_bpm` → `load_loops` → `engine_mix(style)` → `engine_master(style)`. Tool surface: 150 → 153. - **`fx_rename(track_index, fx_index, new_name)`** — new tool + Lua handler (`TrackFX_SetNamedConfigParm("renamed_name", …)`) for renaming an FX's display label. Used internally by the mix engine to tag its own FX with the `[MIX] ` prefix so cleanup can distinguish them from user-added FX. Requires REAPER 6.37+. Tool surface: 149 → 150. - **High-affordance pattern tools** (`patterns_tools.py`, 2 tools) — dedicated MCP tools for the most common MIDI writing tasks, so the AI reaches for them directly without having to learn `compose_arrangement`'s shorthand grammar: - `create_drum_pattern(track_index, pattern, …)` — multi-lane step-sequencer notation (k/s/h/o/c/r/t/l/i/p/b for GM drums, `.` for rests, 16 steps per bar by default). Auto-creates the MIDI item, defaults to GM drum channel 9. - `create_chord_progression(track_index, chords, …)` — parses chord names like `"Cm7, Fm7, Bb7, Eb"` or `"Am - F - C - G"` into voiced MIDI. Supports major/minor/dim/aug/sus2/sus4/6/7/maj7/m7/9/m9/maj9/11/13/add9/dim7/7sus4. - **Tool profiles** via `REAPER_MCP_PROFILE` env var — trim the tool surface down to a workflow-specific subset so it fits under LLM tool-count limits (Groq Llama 3 = 128, smaller models lower). Profiles: `full` (default), `composition`, `mixing`, `analysis`, `minimal`. Invalid values log a warning and fall back to `full`. Startup banner writes the active profile and module count to stderr. - **Audio analysis tools** (`analysis_tools.py`, 4 tools) — objective mix metrics from a rendered WAV, designed to pair with `project_export_audio` + `engine_master` for a `measure → correct` loop: - `analyze_loudness(wav_path, reference)` — integrated LUFS (pyloudnorm), true peak, RMS, crest factor, delta against streaming / broadcast / cinema / club targets. - `analyze_clipping(wav_path, threshold_db)` — per-channel and total sample-clip counts at a configurable threshold. - `analyze_frequency_spectrum(wav_path)` — 7-band energy split (sub / bass / low-mid / mid / high-mid / presence / brilliance), spectral centroid, tonal-balance hint. - `analyze_stereo_field(wav_path)` — phase correlation, mid / side RMS, side-to-mid ratio, mono-compatibility hint. - **Optional `[analysis]` extras** — `numpy`, `soundfile`, `pyloudnorm`. Install with `pip install -e ".[analysis]"`. Tools degrade silently and log a one-line hint to stderr if deps are missing, so the server stays up. - **`install.sh` used the wrong Python on Macs with both Intel and Apple Silicon Homebrew installed.** The PATH setup always let `/usr/local/bin` (Intel) win over `/opt/homebrew/bin` (Apple Silicon), regardless of the user's own shell PATH order — so `pip install` silently ran against the wrong Python, producing `ModuleNotFoundError: No module named 'hatchling'` even with hatchling correctly installed for the right Python. Now only adds a Homebrew's `bin`/`sbin` to PATH if it isn't already there, so an already-correct shell PATH is never overridden. Also verifies after install that the `reaper-mcp` command on PATH actually points at the Python just used — catches the general version of this problem (a stale install from a different Python shadowing the new one) instead of just the Homebrew-specific case. - **The MCP server gave a generic timeout with no explanation when run inside WSL.** REAPER always runs as a native Windows app — WSL has no GUI support for it — so if the Python server is launched from inside a WSL shell instead of native Windows Python, it looks for IPC files in WSL's own `/tmp`, a completely separate filesystem from the real `%TEMP%` REAPER's Lua side writes to. Every command timed out with a generic "server not running" message, even though REAPER and the Lua script were both genuinely running. Now detects WSL and raises a specific error explaining the actual cause instead. - **Removed `reaper_scripts/reaper_mcp_server.eel`** — a 1700-line abandoned TCP-socket-on-port-9876 prototype of the server from before the project settled on file-based IPC. Zero references anywhere in the codebase, touched once at the v0.3.0 commit. Sitting in the same folder as the real `reaper_mcp_server.lua` with a near-identical name, it was a real footgun — loading the wrong script into REAPER produced an inexplicable silent failure with no indication why. - **`envelope_add_points` and `midi_get_notes` had hardcoded size caps duplicating undocumented values.** Same drift risk as the `MAX_NOTES_PER_TRACK` fix below — a bare number with no named constant. Added `MAX_ENVELOPE_POINTS_PER_CALL` and `MAX_NOTES_READ_RESULTS` to `reaper_mcp_shared/constants.py` and wired both call sites to them. - **`create_drum_pattern` / `create_chord_progression` mistimed everything off 60 BPM.** Both built note `start`/`end` in quarter-notes and handed them straight to `midi_insert_notes_batch`, which expects seconds (it calls `MIDI_GetPPQPosFromProjTime`, a project-seconds API). The auto-created item's *length* was correctly converted QN→seconds; the individual note positions inside it never were. At 120 BPM a pattern played back 2x further apart than written (a 16th-note kick pattern landed like 8th notes); only exactly 60 BPM happened to work, which is presumably why this went uncaught. Both tools now convert every note's start/end through `* 60.0 / bpm` before building the batch. - **`item_split`'s reported `right_item.index` was wrong.** The Lua handler assumed `SplitMediaItem` inserts the new piece at `item_index + 1`, but like every other item-creation call in this file it actually appends to the end of REAPER's global item list — the file already knew this and worked around it correctly in `item_split_at_transients`, `item_split_at_positions`, and `item_duplicate`; `item_split` was the one handler still guessing. On any project with more than a couple of items, a caller trusting the returned index would silently mutate an unrelated item instead of the actual new split piece. Now resolves the real index by pointer lookup, matching the other handlers. - **`stack_chop_layers` always read the source chop's pitch as 0.** `item_get_info` never returned a `pitch` field — `build_item_info` (the Lua struct backing every item-info response) simply didn't include the take's `D_PITCH`. Since the documented workflow always pitches a chop to a chord tone before stacking harmonies, every harmony layer came out detuned from the lead by however much the lead had been shifted — not an edge case, the normal path. Added `pitch` (and `playrate`, same call, same gap) to `build_item_info`'s return. - **`item_get_source_info` / `chops_create_virtual_slice` resolved the wrong file for reversed or section-wrapped takes.** A SECTION or reversed take wraps the real audio source; reading the filename off the immediate take source could resolve to the wrapper object instead of the actual file on disk. Now walks up to the root PCM source via `GetMediaSourceParent` first. Also added optional `playrate` pass-through to `chops_create_virtual_slice` so chops stay in-tempo when the source material's native tempo differs from the project's. - **`midi_insert_notes_batch` didn't validate per-note `channel`.** Every other numeric field (pitch, velocity, start/end) was checked; a note with `"channel": 20` (or negative) passed straight through to REAPER. Added the same 0-15 clamp `midi_insert_note` and `midi_insert_cc` already have. - **`midi_insert_notes_batch`'s size cap didn't use the shared limit.** It hardcoded `50000` instead of importing `MAX_NOTES_PER_TRACK` (10000) from `reaper_mcp_shared/constants.py`, whose own comment says "per-track limit on notes in a single batch insert" — this call *is* that per-track batch insert. `compose_arrangement` already wires up all three shared limits correctly; this was the one path still using a disconnected magic number 5x more permissive than the documented ceiling. - **Stale docstring on `_stereo_width_fx`** (`mix_engine/master.py`) claimed a 0.8-1.4 → 0.5-0.85 mapping the formula doesn't implement (the code is self-consistent with its own "1.0 = neutral" comment; the one-line docstring above it was leftover from an earlier version). Corrected the comment to describe what the formula actually computes. - **`item_get_all` and `fx_list_installed` had no output cap.** A chop-heavy project can have hundreds of items; a power user's plugin folder can have 500+ entries. `fx_list_installed` is documented as a call the AI should make before every mix pass, so its `all_installed` dump compounded on every mixing session. Both now cap by default (`item_get_all` via a new `max_results` param, 200 default/2000 ceiling, matching the existing `midi_get_notes` convention; `fx_list_installed`'s `all_installed` caps at 150 with a `full_list=True` opt-out) instead of dumping unbounded lists into the calling model's context on every call. - **Unused `mido` dependency removed** from `pyproject.toml` — nothing in the codebase imports it; all MIDI work goes through the REAPER-side Lua bridge, not a Python MIDI library. - **`analysis_tools`'s missing-dependency fallback was invisible at the registry level.** The module already degrades gracefully when `numpy`/ `soundfile`/`pyloudnorm` aren't installed (skips registering its 4 tools instead of crashing), but `register()` still returns normally, so `tool_registry.py` counted it as a normal success with no signal that 4 tools quietly vanished. Added a `degraded` check that surfaces this in the startup banner. - **Composition/mix tool modules were silently excluded from every published copy of the repo.** The `.gitignore` rule added in the 0.3.0 release (`compose_*.py` / `mix_*.py`, meant for scratchwork) was broad enough to also catch the real, shipped `compose_tools.py`, `compose_edit_tools.py`, `mix_tools.py`, and `compose_helpers.py` — 15 tools including `wipe_all_midi`, `compose_arrangement`, `get_track_instruments`, `analyze_score`, `edit_section`, `engine_mix`, `engine_master`. The files existed on disk in the working tree (so local testing never caught it) but had never once been committed, so every clone of the repo shipped with `00_core.md`'s CRITICAL-rule tools missing entirely — `tool not found` for clients, while the forbidden fallbacks (`item_delete`, `midi_delete_all_notes`) were the only things that actually worked. Narrowed the `.gitignore` rule and committed the four files. Tool surface unchanged (163 across 25 modules) — these tools already counted, they just weren't shipping. - **`wipe_all_midi` deleted audio items.** The Lua handler deleted every media item on the targeted tracks with no `TakeIsMIDI` check, so calling it on a track holding recorded audio silently destroyed it. Now skips items whose active take isn't MIDI; audio items are left in place. - **`tool_registry.py` couldn't detect a missing module under the default `full` profile.** The existing "profile references a module that isn't on disk" sanity check only ran for restricted profiles (`composition`/ `mixing`/`analysis`/`minimal`) — `full` has `allowed=None` (register whatever `pkgutil` finds on disk), so it had nothing to compare against and would have stayed silent through the exact bug above. Added `_EXPECTED_MODULES`, an explicit list of every module that should exist, checked against the registered set for every profile including `full`. A future `.gitignore`-style regression now prints a loud startup warning instead of shipping quietly incomplete. - **`_safe_path` system-directory blocklist was Windows-only.** `item_tools.py` and `project_tools.py` guarded against reading/writing REAPER paths under `C:\Windows` / `Program Files`, but only when `sys.platform == "win32"` — on macOS/Linux there was no equivalent check at all, so `/etc`, `/usr`, `/bin`, `/boot`, `/proc`, `/sys`, `/dev`, `/System`, `/Library` were unguarded. Added a matching `_BLOCKED_DIRS_NIX` list with a boundary-safe prefix check. - **Mix cleanup no longer nukes user-added FX.** The mix engine now tags every FX it adds via `setup_fx_chain` / `setup_master_chain` with a `[MIX] ` prefix. Cleanup matches the prefix first; it only falls back to the old substring-name match if no tagged FX are found on a track (backward compat for projects from before this change or REAPER < 6.37). Users can now freely keep their own `ReaEQ` / `ReaComp` / `Pro-Q 3` / `Pro-C 2` instances on tracks they're also running the mix engine on. - **Lua JSON encoder preserves full float precision.** The previous `"%.6f"` format truncated MIDI positions to microsecond granularity (roughly 48 samples at 48 kHz), which caused quiet drift on sub-sample batch inserts. Now `"%.17g"` — full IEEE-754 double round-trip. - **Hardening pass following a full code audit.** No API changes — all behaviour fixes, validation improvements, and bug fixes: - `mix_engine/plugins.py` — ReaVerbate dry gain was hardcoded to 1.0 on reverb return buses, meaning the dry signal passed through the bus alongside the wet tail, doubling the return level (~3 dB too loud). Now 0.0. - `mix_engine/plugins.py` — Pro-R `Decay` parameter was being fed `room_size`, which collapsed the decay tail when room_size was small and duplicated Space otherwise. Removed the redundant Decay mapping; Pro-R's Space already encodes decay behaviour. - `mix_engine/master.py` — FabFilter Pro-L 2 `Output Level` formula was `0.5 + true_peak_db / 60.0`, which for a target of -1 dBTP produced a ceiling at ~-31 dBTP (limiter barely engaged). Correct formula is `1.0 + true_peak_db / 60.0`, giving a ceiling at -1 dBTP as intended. - `mix_engine/__init__.py` — cleanup now logs every FX it removes (at INFO level) plus a summary count, so users can audit what `clean=True` did. - `reaper_client.py` — JSON parse-failure retries now require the response file's mtime to CHANGE before counting another failure. Previously a partial mid-write response would hit max_parse_failures in 150 ms even when the real response was still on its way, rejecting legitimate responses under heavy write contention. - `reaper_client.py` — `UnicodeDecodeError` is now caught and treated as a parse failure (same retry path), instead of propagating as an unhandled exception if Lua wrote a byte that wasn't valid UTF-8. - `reaper_mcp_server.lua` — `\uXXXX` JSON escapes are now decoded properly to UTF-8 (was being dropped to `?`). Track / plugin names with accented or CJK characters now round-trip correctly. - `reaper_mcp_server.lua` — directory creation no longer uses `os.execute` with a shell-concatenated path. Uses REAPER's native `RecursiveCreateDirectory` API when available (safer against adversarial TMPDIR values), with a defensive-quoted shell fallback for very old REAPER. - `reaper_mcp_server.lua` — added `Undo_BeginBlock` / `Undo_EndBlock` wrapping for every MIDI and item/track mutation that lacked it, so Undo history in REAPER shows readable `MCP: …` labels for every change the AI made. - `tools/patterns_tools.py` — `start_qn` was being applied twice (once to the auto-created item's project position and again to every note inside the item), so patterns appeared at 2 × start_qn instead of start_qn. Now notes are placed relative to the item and the item always starts at project 0 when auto-creating. - `tools/patterns_tools.py` — chord regex now accepts lowercase roots (`"cm7"` parses the same as `"Cm7"`). Previously lowercase chord names silently returned None and were listed under `failed_chords`. - `tools/analysis_tools.py` — `analyze_loudness` guards against silent audio (previously returned non-JSON `-inf` LUFS); `analyze_frequency_spectrum` guards against divide-by-zero when magnitudes sum to 0; `analyze_stereo_field` guards against divide-by-zero when either channel is constant/silent (was returning `NaN`, which breaks JSON serialisation). - `tools/midi_tools.py` — `midi_insert_notes_batch` now validates the `notes` JSON client-side: parses it, checks shape is a list, enforces a note-count ceiling, validates every note has pitch (0-127), velocity (1-127), numeric start/end with end > start. - `tools/compose_edit_tools.py` — the "all" sentinel for `tracks=` is now case-insensitive and tolerates surrounding whitespace/quotes. - `tool_registry.py` — warns if a profile references a module that isn't importable (profile definition out of sync with disk) — later extended to cover the default `full` profile too (see above). ## [0.3.0] - 2026-04-17 ### Added - **25 style profiles** across EDM, Rock, Pop, and Electronic families — each with per-instrument EQ + compression curves, sidechain relationships, reverb bus configuration, and mastering targets (LUFS, true peak, stereo width, limiter character): - EDM (11): melodic_dubstep, big_room, future_bass, future_house, deep_house, tech_house, progressive_house, dubstep, trap, drum_and_bass, trance - Rock (6): alt_rock, classic_rock, pop_rock, hard_rock, punk, post_rock - Pop (4): modern_pop, dance_pop, indie_pop, rnb_pop - Electronic (4): synthwave, lofi, ambient, hiphop - **`engine_master(style, clean=True)`** — master-bus mastering chain: HP 25 Hz → bus glue compression → tonal shelf EQ → stereo width → brick-wall limiter. Auto-detects FabFilter Pro-L 2 / Pro-C 2 / Pro-Q 3 or falls back to REAPER stock (ReaLimit / ReaComp / ReaEQ). - **`engine_fix_mix(style="", include_master=True)`** — non-destructive mix repair. Re-runs the mix pipeline on an existing session, preserving user tweaks where possible. - **`engine_mix(style)`** now dispatches to all 25 style profiles across EDM / Rock / Pop / Electronic in addition to the existing orchestral path. - **`setup_sidechain(source, target, amount, ...)`** — professional sidechain compression. Creates an aux send on channels 3/4, pin-maps the compressor's sidechain inputs, and tunes attack / release / ratio / threshold from a single 0–1 amount dial. Auto-adds ReaComp or targets existing FabFilter Pro-C 2. - **Bus pipelines** (`pipeline_tools.py`, 4 tools): - `setup_parallel_compression(source_tracks, bus_name, ...)` — New-York-style parallel compression bus. - `setup_drum_bus(source_tracks, bus_name, ...)` — drum bus with glue compression and optional saturation. - `setup_vocal_chain(track_index, hp_freq, ...)` — pro vocal chain: HP → de-esser → two-stage comp → EQ → saturation. - `bounce_stems(track_indices, output_dir, ...)` — render each selected track individually to a WAV stem. - **FX inventory** (`inventory_tools.py`, 2 tools): - `fx_list_installed(category)` — discovers every installed plugin and picks the best-available EQ / compressor / reverb / limiter / de-esser / gate / saturator / multiband / stereo processor across FabFilter, Waves, iZotope, Valhalla, Softube, TDR, Slate, Melda, Soundtoys, Airwindows, and REAPER stock. Detects rack hosts (Waves StudioRack, Blue Cat PatchWork, Kilohearts Snap Heap). - `set_fx_preferences(preferences)` — pin category → plugin mapping. Stored at `%APPDATA%/reaper_mcp/fx_prefs.json` (or `~/.config/reaper_mcp/` on *nix). - **Envelope automation** (`envelope_tools.py`, 3 tools): `envelope_get_points`, `envelope_add_points`, `envelope_clear_range` — read/write automation envelopes on tracks, items, and FX parameters. - **Tempo map** (`tempo_tools.py`, 4 tools): `tempo_list_markers`, `tempo_add_marker`, `tempo_delete_marker`, `tempo_clear_all`. - **Track templates** (`template_tools.py`, 4 tools): `track_template_save`, `track_template_apply`, `track_template_list`, `track_template_delete`. - **Takes** (`take_tools.py`, 4 tools): `item_take_list`, `item_take_add`, `item_take_set_active`, `item_take_delete_active`. - **Track freeze/unfreeze** (in `track_tools.py`): `track_freeze`, `track_unfreeze` — bounce FX to audio for CPU relief. - **MIDI timing utilities** (`quantize_tools.py`, 3 tools): `midi_quantize`, `midi_humanize`, `project_set_ripple_mode`. - **Composition editing expansion** (`compose_edit_tools.py`): `reset_composition`, `add_markers_batch`, `rewrite_cc`, `setup_fx_chain`, `setup_effect_bus` alongside the existing `wipe_all_midi`, `configure_tracks`, `setup_routing`, `edit_section`. - **Demo** (`demo_tools.py`): `demo_edm_project(clean_first=True, bpm=140.0)` — build a full demo EDM project end-to-end as a smoke test and reference. - **`send_get_routing_diagram`** — ASCII routing diagram for the whole project. - **Lua handlers**: `setup_sidechain`, `setup_master_chain`, `setup_parallel_compression`, `setup_drum_bus`, `setup_vocal_chain`, `bounce_stems`, `fx_list_installed`, envelope and tempo-marker handlers — all support pin mappings and fuzzy FX param-name matching. - **Heartbeat protocol** — the IPC client detects a stale REAPER (`server.lock` older than 60 s) and raises a typed `CONNECTION_LOST` error instead of hanging. - **docs/TOOLS.md** — complete tool reference with every signature and one-line descriptions. - **docs/ARCHITECTURE.md** — IPC protocol, mix-engine pipeline, and design-decision notes. ### Changed - **AI-driven composition** — the AI writes every note, rhythm, CC curve, and keyswitch itself using the granular MIDI + FX tools. A single `00_core.md` instruction file provides the tool surface, shorthand notation, BBC Spitfire CC reference, and per-family mixing tips. - `compose_arrangement` is now explicitly scoped to small edits (≤ 2 tracks, ≤ 30 notes); larger writes use `midi_insert_notes_batch` or split into chunks. - README + CHANGELOG updated to reflect the current tool surface (143 tools across 21 modules). - `pyproject.toml` bumped to `0.3.0`; description corrected from "TCP socket communication" to file-based IPC. ### Removed - Internal composition generator and supporting modules (relocated outside the repo). - `polish_track` and `add_smart_cc` tools — the AI does post-processing manually via `midi_set_note` / `midi_insert_cc`. - `engine_compose`, `engine_next_layer`, `get_engine_options` — replaced by the AI-driven composition model. - `midi_get_ccs` — reading CC curves across a full arrangement would blow the AI's context window. Use `midi_count_events` for a cheap count, or ask the user to describe their intent. ## [0.1.0] - 2026-03-16 ### Added - Initial release — 100+ MCP tools across 11 modules: - **Transport** (11): play, stop, pause, record, get state, set position, BPM, time signature, toggle repeat/metronome, set playrate - **Track** (15): get all/info, create, delete, rename, set volume/pan/mute/solo/record arm/color/input/folder, select, mixer state - **Project** (11): get info, new, open, save, save as, export audio, undo/redo, get/set notes, set grid - **FX** (14): add, remove, get chain, get/set params, params by name, enable/disable, show UI, preset get/set/navigate, get instrument, move - **Items** (13): get all/info, select, split, delete, move, set length/volume/mute/fade, insert media, create MIDI, move to track - **MIDI** (14): insert note (single/batch), get/set/delete notes, select notes, delete all, insert/get/delete CC, note names, count events, sort, set item extents - **Markers** (7): get all, add marker/region (single + batch), delete, edit, go to - **Selection** (9): get/set time selection, set loop, select/deselect all items/tracks, get selected - **Sends** (7): create, remove, get all, set volume/pan/mute, routing diagram - **Composition utility** (3): `get_track_instruments`, `analyze_score`, `compose_arrangement` - **Batch setup** (5): `edit_section`, `configure_tracks`, `setup_routing`, `setup_fx_chain`, `setup_effect_bus` - Lua IPC bridge (`reaper_scripts/reaper_mcp_server.lua`) — file-based command/response via `%TEMP%\reaper_mcp` or `/tmp/reaper_mcp` - Dynamic tool registration — drop a module into `reaper_mcp/tools/` with a `register(mcp)` function and it's auto-discovered - Cross-platform support (Windows, macOS, Linux)