@mthines/reaper-mcp 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-agents/{mix-analyzer.md → critique.md} +3 -3
- package/claude-agents/editor.md +184 -0
- package/claude-agents/{gain-stage.md → levels.md} +1 -1
- package/claude-agents/{master.md → mastering.md} +1 -1
- package/claude-agents/{mix-engineer.md → mixer.md} +1 -1
- package/claude-agents/preflight.md +149 -0
- package/claude-agents/producer.md +159 -0
- package/claude-agents/setup.md +150 -0
- package/claude-agents/stems.md +135 -0
- package/knowledge/workflows/delivery.md +202 -0
- package/knowledge/workflows/editing.md +251 -0
- package/knowledge/workflows/session-prep.md +205 -0
- package/knowledge/workflows/stem-prep.md +175 -0
- package/main.js +50 -6
- package/package.json +1 -1
- package/reaper/mcp_bridge.lua +100 -4
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Session Preparation
|
|
3
|
+
id: session-prep
|
|
4
|
+
description: Organize and prepare a REAPER session for mixing — naming, coloring, routing, markers, bus structure
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Session Preparation
|
|
8
|
+
|
|
9
|
+
## When to Use
|
|
10
|
+
|
|
11
|
+
Before any mixing begins. Session preparation is the organizational groundwork that makes everything downstream faster and less error-prone. A well-prepared session prevents routing mistakes, makes navigation instant, and ensures no tracks are overlooked.
|
|
12
|
+
|
|
13
|
+
Use this workflow when:
|
|
14
|
+
- Opening a new session for the first time before mixing
|
|
15
|
+
- A session has grown organically and needs reorganization
|
|
16
|
+
- Tracks have generic names like "Audio_001" or "Track 14"
|
|
17
|
+
- There is no bus/routing structure in place
|
|
18
|
+
- The session has no markers for song sections
|
|
19
|
+
|
|
20
|
+
## Prerequisites
|
|
21
|
+
|
|
22
|
+
- REAPER session is open with all recorded tracks
|
|
23
|
+
- You have a general idea of the song structure (verse, chorus, bridge, etc.)
|
|
24
|
+
- No mixing has been done yet (or you are willing to reorganize before continuing)
|
|
25
|
+
|
|
26
|
+
## Step-by-Step
|
|
27
|
+
|
|
28
|
+
### Step 1: Save a safety snapshot
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
tool: snapshot_save
|
|
32
|
+
params:
|
|
33
|
+
name: "pre-session-prep"
|
|
34
|
+
description: "State before session organization"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Step 2: Inventory all tracks
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
tool: get_project_info
|
|
41
|
+
tool: list_tracks
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Note: track count, existing names, any folder/bus structure already present, sample rate, tempo.
|
|
45
|
+
|
|
46
|
+
Identify each track's instrument/source by its name, audio content, or position. If names are unclear, read media items to check source filenames:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
tool: list_media_items
|
|
50
|
+
params:
|
|
51
|
+
trackIndex: [n]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Step 3: Rename tracks with descriptive names
|
|
55
|
+
|
|
56
|
+
Apply clear, consistent names. Standard naming conventions:
|
|
57
|
+
|
|
58
|
+
| Category | Examples |
|
|
59
|
+
|----------|---------|
|
|
60
|
+
| Drums | Kick In, Kick Out, Snare Top, Snare Bot, Hi-Hat, Tom 1, Tom 2, OH L, OH R, Room L, Room R |
|
|
61
|
+
| Bass | Bass DI, Bass Amp |
|
|
62
|
+
| Guitars | Gtr Rhythm L, Gtr Rhythm R, Gtr Lead, Gtr Clean, Gtr Acoustic |
|
|
63
|
+
| Keys | Piano, Organ, Synth Pad, Synth Lead |
|
|
64
|
+
| Vocals | Lead Vox, BV 1, BV 2, BV Harmony, Dbl Vox |
|
|
65
|
+
| Effects | FX Riser, FX Impact, FX Ambience |
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
tool: set_track_property
|
|
69
|
+
params:
|
|
70
|
+
trackIndex: [n]
|
|
71
|
+
property: "name"
|
|
72
|
+
value: "Kick In"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Step 4: Reorder and group tracks
|
|
76
|
+
|
|
77
|
+
Standard track ordering (top to bottom):
|
|
78
|
+
1. **Drums** — Kick, Snare, Toms, Hi-Hat, Overheads, Room
|
|
79
|
+
2. **Bass** — DI, Amp
|
|
80
|
+
3. **Guitars** — Rhythm (L/R pairs), Lead, Clean, Acoustic
|
|
81
|
+
4. **Keys/Synths** — Piano, Organ, Pads, Leads
|
|
82
|
+
5. **Vocals** — Lead, Doubles, Backing Vocals, Harmonies
|
|
83
|
+
6. **Effects/Samples** — Risers, Impacts, Pads
|
|
84
|
+
7. **Buses** — Drum Bus, Instrument Bus, Vocal Bus, Effects Bus
|
|
85
|
+
8. **Returns** — Reverb, Delay
|
|
86
|
+
9. **Master/Mix Bus** — Always last
|
|
87
|
+
|
|
88
|
+
### Step 5: Color code by group
|
|
89
|
+
|
|
90
|
+
Apply consistent colors across the session:
|
|
91
|
+
|
|
92
|
+
| Group | Suggested Color |
|
|
93
|
+
|-------|----------------|
|
|
94
|
+
| Drums | Blue |
|
|
95
|
+
| Bass | Dark Blue / Navy |
|
|
96
|
+
| Guitars | Green |
|
|
97
|
+
| Keys/Synths | Purple |
|
|
98
|
+
| Vocals | Orange / Yellow |
|
|
99
|
+
| Effects/Samples | Pink / Magenta |
|
|
100
|
+
| Buses | Gray |
|
|
101
|
+
| Returns | Teal |
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
tool: set_track_property
|
|
105
|
+
params:
|
|
106
|
+
trackIndex: [n]
|
|
107
|
+
property: "color"
|
|
108
|
+
value: "0,100,200"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Step 6: Set up bus/routing structure
|
|
112
|
+
|
|
113
|
+
Create submix buses for each instrument group. Typical bus structure:
|
|
114
|
+
|
|
115
|
+
| Bus | Routes From | Purpose |
|
|
116
|
+
|-----|------------|---------|
|
|
117
|
+
| Drum Bus | All drum tracks | Group processing, glue compression |
|
|
118
|
+
| Bass Bus | Bass DI + Amp | Blend and control |
|
|
119
|
+
| Guitar Bus | All guitar tracks | Group EQ, width control |
|
|
120
|
+
| Vocal Bus | Lead + BVs | Unified vocal processing |
|
|
121
|
+
| Instrument Bus | Guitar Bus + Keys | Non-rhythm instrument group |
|
|
122
|
+
| Effects Bus | FX tracks | Level control for effects |
|
|
123
|
+
| Reverb Return | Aux send destination | Shared reverb space |
|
|
124
|
+
| Delay Return | Aux send destination | Shared delay effects |
|
|
125
|
+
|
|
126
|
+
Check existing routing first:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
tool: get_track_routing
|
|
130
|
+
params:
|
|
131
|
+
trackIndex: [n]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Step 7: Add markers for song sections
|
|
135
|
+
|
|
136
|
+
Navigate through the session and identify song sections. Add markers at each section boundary:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
tool: add_marker
|
|
140
|
+
params:
|
|
141
|
+
position: [seconds]
|
|
142
|
+
name: "Intro"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Standard section markers:
|
|
146
|
+
- Intro
|
|
147
|
+
- Verse 1
|
|
148
|
+
- Pre-Chorus 1
|
|
149
|
+
- Chorus 1
|
|
150
|
+
- Verse 2
|
|
151
|
+
- Pre-Chorus 2
|
|
152
|
+
- Chorus 2
|
|
153
|
+
- Bridge
|
|
154
|
+
- Chorus 3 / Final Chorus
|
|
155
|
+
- Outro
|
|
156
|
+
|
|
157
|
+
Optionally add regions for each section:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
tool: add_region
|
|
161
|
+
params:
|
|
162
|
+
startPosition: [seconds]
|
|
163
|
+
endPosition: [seconds]
|
|
164
|
+
name: "Chorus 1"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Step 8: Verify session parameters
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
tool: get_project_info
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Confirm:
|
|
174
|
+
- Sample rate is consistent (44.1 kHz, 48 kHz, etc.)
|
|
175
|
+
- Tempo is correct
|
|
176
|
+
- Time signature is set
|
|
177
|
+
|
|
178
|
+
### Step 9: Save post-prep snapshot
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
tool: snapshot_save
|
|
182
|
+
params:
|
|
183
|
+
name: "post-session-prep"
|
|
184
|
+
description: "Session organized — tracks named, colored, routed, markers placed"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Verification
|
|
188
|
+
|
|
189
|
+
After completing session preparation:
|
|
190
|
+
|
|
191
|
+
1. Every track has a descriptive name (no "Audio_001" or "Track 14")
|
|
192
|
+
2. Tracks are ordered by instrument group
|
|
193
|
+
3. Each group has a consistent color
|
|
194
|
+
4. Bus structure is in place (at minimum: drum bus, vocal bus)
|
|
195
|
+
5. Song section markers are placed at correct positions
|
|
196
|
+
6. All tracks route to appropriate buses (no orphan tracks going directly to master)
|
|
197
|
+
7. Session parameters are verified
|
|
198
|
+
|
|
199
|
+
## Common Pitfalls
|
|
200
|
+
|
|
201
|
+
- **Renaming without checking content**: Listen or check media items before naming — a track labeled "Guitar" might actually be a synth
|
|
202
|
+
- **Over-complex routing**: Start simple. A drum bus, vocal bus, and instrument bus is sufficient for most sessions. Add complexity only when needed.
|
|
203
|
+
- **Forgetting returns**: Reverb and delay sends need return tracks routed to the master bus
|
|
204
|
+
- **Inconsistent naming**: Pick a convention and stick with it — "Lead Vox" or "Lead Vocal" but not both
|
|
205
|
+
- **Not saving a snapshot**: Session prep involves many changes. Save before starting so you can revert if needed.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Stem Preparation
|
|
3
|
+
id: stem-prep
|
|
4
|
+
description: Verify bus structure and routing for stem export readiness
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Stem Preparation
|
|
8
|
+
|
|
9
|
+
## When to Use
|
|
10
|
+
|
|
11
|
+
After mastering is complete and before final delivery. Stem preparation ensures the session's bus structure, routing, and naming conventions are correct so that exported stems will sum to match the full mix exactly.
|
|
12
|
+
|
|
13
|
+
Use this workflow when:
|
|
14
|
+
- The mix is complete and mastered, ready for stem export
|
|
15
|
+
- Stems are needed for sync licensing, remix, or immersive audio
|
|
16
|
+
- A mastering engineer has requested stems instead of a stereo mix
|
|
17
|
+
- The session needs routing verification before export
|
|
18
|
+
|
|
19
|
+
## Prerequisites
|
|
20
|
+
|
|
21
|
+
- Mix is complete (all mixing and mastering decisions are finalized)
|
|
22
|
+
- Bus structure exists (drum bus, vocal bus, instrument bus, etc.)
|
|
23
|
+
- Session is playing back correctly (no muted tracks that should be active)
|
|
24
|
+
- Genre and delivery specs are known
|
|
25
|
+
|
|
26
|
+
## Step-by-Step
|
|
27
|
+
|
|
28
|
+
### Step 1: Save a safety snapshot
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
tool: snapshot_save
|
|
32
|
+
params:
|
|
33
|
+
name: "pre-stem-prep"
|
|
34
|
+
description: "State before stem preparation"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Step 2: Document the current bus structure
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
tool: list_tracks
|
|
41
|
+
tool: get_project_info
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Map out the full routing hierarchy. For each track:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
tool: get_track_routing
|
|
48
|
+
params:
|
|
49
|
+
trackIndex: [n]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Identify:
|
|
53
|
+
- Which tracks route to which buses
|
|
54
|
+
- Which buses route to the master
|
|
55
|
+
- Any orphan tracks routing directly to master (should go through a bus)
|
|
56
|
+
- Any tracks with sends to multiple destinations
|
|
57
|
+
|
|
58
|
+
### Step 3: Verify standard stem groups
|
|
59
|
+
|
|
60
|
+
Typical stem groups for export:
|
|
61
|
+
|
|
62
|
+
| Stem | Contains | Bus |
|
|
63
|
+
|------|----------|-----|
|
|
64
|
+
| Drums | Kick, Snare, Toms, OH, Room | Drum Bus |
|
|
65
|
+
| Bass | Bass DI, Bass Amp | Bass Bus |
|
|
66
|
+
| Guitars | All guitar tracks | Guitar Bus |
|
|
67
|
+
| Keys/Synths | Piano, Organ, Pads, Leads | Keys Bus |
|
|
68
|
+
| Vocals | Lead, BVs, Harmonies | Vocal Bus |
|
|
69
|
+
| Effects | Reverbs, Delays (printed) | Effects Bus |
|
|
70
|
+
|
|
71
|
+
Verify each group routes correctly:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
tool: get_track_properties
|
|
75
|
+
params:
|
|
76
|
+
trackIndex: [bus track index]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Step 4: Check for routing problems
|
|
80
|
+
|
|
81
|
+
Common issues to flag:
|
|
82
|
+
|
|
83
|
+
1. **Orphan tracks**: Source tracks routing directly to master instead of through a bus
|
|
84
|
+
2. **Double-routing**: A track routing to both a bus AND the master (will be in the stem AND the full mix twice)
|
|
85
|
+
3. **Missing tracks**: Tracks that are muted or have no output
|
|
86
|
+
4. **Send-only tracks**: Reverb returns that should be captured in a stem but are going to master instead of a bus
|
|
87
|
+
5. **Master bus processing**: Note which FX are on the master bus — stems are typically exported pre-master-bus-processing
|
|
88
|
+
|
|
89
|
+
For each bus, check that all expected source tracks are present:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
tool: get_track_routing
|
|
93
|
+
params:
|
|
94
|
+
trackIndex: [bus index]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Step 5: Verify naming conventions
|
|
98
|
+
|
|
99
|
+
Stems need clear, consistent names for delivery. Verify bus names follow a convention:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
[Song Title]_[Stem Name]_[Bit Depth]-[Sample Rate]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Examples:
|
|
106
|
+
- `MySong_Drums_24-48.wav`
|
|
107
|
+
- `MySong_Vocals_24-48.wav`
|
|
108
|
+
- `MySong_Bass_24-48.wav`
|
|
109
|
+
|
|
110
|
+
Check that bus track names are clean and descriptive:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
tool: get_track_properties
|
|
114
|
+
params:
|
|
115
|
+
trackIndex: [bus index]
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Rename if needed:
|
|
119
|
+
```
|
|
120
|
+
tool: set_track_property
|
|
121
|
+
params:
|
|
122
|
+
trackIndex: [bus index]
|
|
123
|
+
property: "name"
|
|
124
|
+
value: "Drum Bus"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Step 6: Verify technical specs
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
tool: get_project_info
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Confirm:
|
|
134
|
+
- **Sample rate**: Matches delivery requirements (typically 44.1 kHz or 48 kHz)
|
|
135
|
+
- **Bit depth**: 24-bit or 32-bit float for stems
|
|
136
|
+
- **All stems have same duration**: Check that all bus tracks span the full song (from before first note to after last decay)
|
|
137
|
+
|
|
138
|
+
### Step 7: Document the stem map
|
|
139
|
+
|
|
140
|
+
Create a clear report of:
|
|
141
|
+
- Each stem name and what tracks it contains
|
|
142
|
+
- Routing chain for each stem
|
|
143
|
+
- Any processing on stem buses (baked into the stem)
|
|
144
|
+
- Master bus processing that will NOT be in individual stems
|
|
145
|
+
- Total stem count
|
|
146
|
+
- Technical specs (sample rate, bit depth)
|
|
147
|
+
|
|
148
|
+
### Step 8: Save post-prep snapshot
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
tool: snapshot_save
|
|
152
|
+
params:
|
|
153
|
+
name: "post-stem-prep"
|
|
154
|
+
description: "Stems verified — routing correct, naming consistent, ready for export"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Verification
|
|
158
|
+
|
|
159
|
+
After completing stem preparation:
|
|
160
|
+
|
|
161
|
+
1. Every source track routes to exactly one bus (no orphans, no double-routing)
|
|
162
|
+
2. All buses route to the master
|
|
163
|
+
3. Bus names are clear and follow a consistent convention
|
|
164
|
+
4. No tracks are accidentally muted or disabled
|
|
165
|
+
5. Stem groups make musical sense (all drums together, all vocals together, etc.)
|
|
166
|
+
6. Technical specs are documented (sample rate, bit depth, duration)
|
|
167
|
+
7. The user knows which master bus processing will NOT be included in individual stems
|
|
168
|
+
|
|
169
|
+
## Common Pitfalls
|
|
170
|
+
|
|
171
|
+
- **Forgetting reverb/delay returns**: Shared effects (reverb bus, delay bus) need to be assigned to a stem or exported as their own stem. If they route to master only, they'll be in the full mix but not in any stem.
|
|
172
|
+
- **Double-counting sends**: If a track sends to both a bus and a reverb return, the dry signal is in one stem and the wet signal might be in another. This is usually correct, but document it.
|
|
173
|
+
- **Master bus processing**: If the master bus has EQ/compression/limiting, individual stems will sound different than the full mix. This is normal — stems are typically pre-master-bus.
|
|
174
|
+
- **Mismatched levels**: All stems should sum to equal the full mix at the master bus. If they don't, there's a routing or level issue.
|
|
175
|
+
- **Not checking mute states**: A muted track won't export. Verify all intended tracks are unmuted and active.
|
package/main.js
CHANGED
|
@@ -134,12 +134,14 @@ function getTimeoutCounter() {
|
|
|
134
134
|
|
|
135
135
|
// apps/reaper-mcp-server/src/bridge.ts
|
|
136
136
|
import { randomUUID } from "node:crypto";
|
|
137
|
-
import { readFile, writeFile, readdir, unlink, mkdir, stat } from "node:fs/promises";
|
|
137
|
+
import { appendFile, readFile, writeFile, readdir, unlink, mkdir, stat } from "node:fs/promises";
|
|
138
138
|
import { join as join2 } from "node:path";
|
|
139
139
|
import { homedir, platform } from "node:os";
|
|
140
140
|
import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
|
|
141
|
-
var POLL_INTERVAL_MS =
|
|
141
|
+
var POLL_INTERVAL_MS = 10;
|
|
142
142
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
143
|
+
var PROFILE_BRIDGE = process.env["BRIDGE_PROFILE"] === "1";
|
|
144
|
+
var lastTimings = null;
|
|
143
145
|
function getReaperResourcePath() {
|
|
144
146
|
const env = process.env["REAPER_RESOURCE_PATH"];
|
|
145
147
|
if (env) return env;
|
|
@@ -164,6 +166,10 @@ async function ensureBridgeDir() {
|
|
|
164
166
|
async function sendCommand(type, params = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
165
167
|
const tracer = getTracer();
|
|
166
168
|
const startMs = Date.now();
|
|
169
|
+
const t = {};
|
|
170
|
+
const profiling = PROFILE_BRIDGE;
|
|
171
|
+
const now = profiling ? () => performance.now() : () => 0;
|
|
172
|
+
const t0 = now();
|
|
167
173
|
return tracer.startActiveSpan(
|
|
168
174
|
`mcp.bridge ${type}`,
|
|
169
175
|
{
|
|
@@ -174,7 +180,10 @@ async function sendCommand(type, params = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
174
180
|
}
|
|
175
181
|
},
|
|
176
182
|
async (span) => {
|
|
183
|
+
if (profiling) t["spanSetup"] = now() - t0;
|
|
184
|
+
const tDir = now();
|
|
177
185
|
const dir = await ensureBridgeDir();
|
|
186
|
+
if (profiling) t["ensureDir"] = now() - tDir;
|
|
178
187
|
const id = randomUUID();
|
|
179
188
|
span.setAttribute("mcp.command.id", id);
|
|
180
189
|
const command2 = {
|
|
@@ -183,8 +192,13 @@ async function sendCommand(type, params = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
183
192
|
params,
|
|
184
193
|
timestamp: Date.now()
|
|
185
194
|
};
|
|
195
|
+
const tWrite = now();
|
|
186
196
|
const commandPath = join2(dir, `command_${id}.json`);
|
|
187
197
|
await writeFile(commandPath, JSON.stringify(command2, null, 2), "utf-8");
|
|
198
|
+
const notifyPath = join2(dir, "_notify");
|
|
199
|
+
await appendFile(notifyPath, id + "\n");
|
|
200
|
+
if (profiling) t["write"] = now() - tWrite;
|
|
201
|
+
const tLog = now();
|
|
188
202
|
const traceCtx = getTraceContext();
|
|
189
203
|
console.error(
|
|
190
204
|
JSON.stringify({
|
|
@@ -194,13 +208,22 @@ async function sendCommand(type, params = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
194
208
|
...traceCtx
|
|
195
209
|
})
|
|
196
210
|
);
|
|
211
|
+
if (profiling) t["log"] = now() - tLog;
|
|
197
212
|
const responsePath = join2(dir, `response_${id}.json`);
|
|
198
213
|
const deadline = Date.now() + timeoutMs;
|
|
214
|
+
const tPoll = now();
|
|
215
|
+
let pollAttempts = 0;
|
|
199
216
|
while (Date.now() < deadline) {
|
|
200
217
|
try {
|
|
218
|
+
pollAttempts++;
|
|
219
|
+
const tRead = now();
|
|
201
220
|
const data = await readFile(responsePath, "utf-8");
|
|
202
221
|
const response = JSON.parse(data);
|
|
222
|
+
if (profiling) t["readParse"] = now() - tRead;
|
|
223
|
+
if (profiling) t["pollWait"] = now() - tPoll - (t["readParse"] ?? 0);
|
|
224
|
+
const tCleanup = now();
|
|
203
225
|
await Promise.allSettled([unlink(commandPath), unlink(responsePath)]);
|
|
226
|
+
if (profiling) t["cleanup"] = now() - tCleanup;
|
|
204
227
|
const durationMs2 = Date.now() - startMs;
|
|
205
228
|
const succeeded = response.success;
|
|
206
229
|
span.setAttribute("mcp.response.success", succeeded);
|
|
@@ -219,8 +242,24 @@ async function sendCommand(type, params = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
219
242
|
);
|
|
220
243
|
}
|
|
221
244
|
span.end();
|
|
245
|
+
const tMetrics = now();
|
|
222
246
|
getCommandDurationHistogram().record(durationMs2, { command_type: type });
|
|
223
247
|
getCommandCounter().add(1, { command_type: type, success: String(succeeded) });
|
|
248
|
+
if (profiling) t["metrics"] = now() - tMetrics;
|
|
249
|
+
if (profiling) {
|
|
250
|
+
lastTimings = {
|
|
251
|
+
spanSetupMs: Math.round(t["spanSetup"] ?? 0),
|
|
252
|
+
ensureDirMs: Math.round(t["ensureDir"] ?? 0),
|
|
253
|
+
writeMs: Math.round(t["write"] ?? 0),
|
|
254
|
+
logMs: Math.round(t["log"] ?? 0),
|
|
255
|
+
pollWaitMs: Math.round(t["pollWait"] ?? 0),
|
|
256
|
+
pollAttempts,
|
|
257
|
+
readParseMs: Math.round(t["readParse"] ?? 0),
|
|
258
|
+
cleanupMs: Math.round(t["cleanup"] ?? 0),
|
|
259
|
+
metricsMs: Math.round(t["metrics"] ?? 0),
|
|
260
|
+
totalMs: Math.round(now() - t0)
|
|
261
|
+
};
|
|
262
|
+
}
|
|
224
263
|
return response;
|
|
225
264
|
} catch {
|
|
226
265
|
await sleep(POLL_INTERVAL_MS);
|
|
@@ -1797,6 +1836,11 @@ function ensureClaudeSettings(settingsPath) {
|
|
|
1797
1836
|
writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
1798
1837
|
return "updated";
|
|
1799
1838
|
}
|
|
1839
|
+
function resolveAssetDirWithFallback(baseDir, buildName, sourceName) {
|
|
1840
|
+
const resolved = resolveAssetDir(baseDir, buildName);
|
|
1841
|
+
if (existsSync(resolved)) return resolved;
|
|
1842
|
+
return resolveAssetDir(baseDir, sourceName);
|
|
1843
|
+
}
|
|
1800
1844
|
|
|
1801
1845
|
// apps/reaper-mcp-server/src/main.ts
|
|
1802
1846
|
var __dirname = dirname2(fileURLToPath2(import.meta.url));
|
|
@@ -1855,7 +1899,7 @@ async function installSkills(scope) {
|
|
|
1855
1899
|
} else {
|
|
1856
1900
|
console.log("Knowledge base not found in package. Skipping.");
|
|
1857
1901
|
}
|
|
1858
|
-
const rulesSrc =
|
|
1902
|
+
const rulesSrc = resolveAssetDirWithFallback(__dirname, "claude-rules", join4(".claude", "rules"));
|
|
1859
1903
|
if (existsSync2(rulesSrc)) {
|
|
1860
1904
|
const dest = join4(claudeDir, "rules");
|
|
1861
1905
|
const count = copyDirSync(rulesSrc, dest);
|
|
@@ -1863,7 +1907,7 @@ async function installSkills(scope) {
|
|
|
1863
1907
|
} else {
|
|
1864
1908
|
console.log("Claude rules not found in package. Skipping.");
|
|
1865
1909
|
}
|
|
1866
|
-
const skillsSrc =
|
|
1910
|
+
const skillsSrc = resolveAssetDirWithFallback(__dirname, "claude-skills", join4(".claude", "skills"));
|
|
1867
1911
|
if (existsSync2(skillsSrc)) {
|
|
1868
1912
|
const dest = join4(claudeDir, "skills");
|
|
1869
1913
|
const count = copyDirSync(skillsSrc, dest);
|
|
@@ -1871,7 +1915,7 @@ async function installSkills(scope) {
|
|
|
1871
1915
|
} else {
|
|
1872
1916
|
console.log("Claude skills not found in package. Skipping.");
|
|
1873
1917
|
}
|
|
1874
|
-
const agentsSrc =
|
|
1918
|
+
const agentsSrc = resolveAssetDirWithFallback(__dirname, "claude-agents", join4(".claude", "agents"));
|
|
1875
1919
|
if (existsSync2(agentsSrc)) {
|
|
1876
1920
|
const dest = join4(claudeDir, "agents");
|
|
1877
1921
|
const count = copyDirSync(agentsSrc, dest);
|
|
@@ -1899,7 +1943,7 @@ Created: ${mcpJsonPath}`);
|
|
|
1899
1943
|
}
|
|
1900
1944
|
}
|
|
1901
1945
|
console.log("\nDone! Claude Code now has mix engineer agents, knowledge, and REAPER MCP tools.");
|
|
1902
|
-
console.log(
|
|
1946
|
+
console.log(`All ${MCP_TOOL_NAMES.length} REAPER tools are pre-approved \u2014 agents work autonomously.`);
|
|
1903
1947
|
console.log('\nTry: @mix-engineer "Please gain stage my tracks"');
|
|
1904
1948
|
console.log('Or: @mix-analyzer "Roast my mix"');
|
|
1905
1949
|
}
|
package/package.json
CHANGED
package/reaper/mcp_bridge.lua
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
-- The script will keep running in the background via defer().
|
|
10
10
|
-- =============================================================================
|
|
11
11
|
|
|
12
|
-
local POLL_INTERVAL = 0
|
|
12
|
+
local POLL_INTERVAL = 0 -- scan every defer cycle; the notify file makes this cheap
|
|
13
13
|
local HEARTBEAT_INTERVAL = 1.0 -- write heartbeat every 1s
|
|
14
14
|
local MCP_ANALYZER_FX_NAME = "reaper-mcp/mcp_analyzer" -- JSFX analyzer name
|
|
15
15
|
|
|
@@ -21,6 +21,14 @@ reaper.RecursiveCreateDirectory(bridge_dir, 0)
|
|
|
21
21
|
|
|
22
22
|
local last_poll = 0
|
|
23
23
|
local last_heartbeat = 0
|
|
24
|
+
local defer_count = 0
|
|
25
|
+
local defer_intervals = {} -- circular buffer of last 100 defer intervals
|
|
26
|
+
local defer_idx = 0
|
|
27
|
+
local defer_buf_size = 100
|
|
28
|
+
local last_defer_time = 0
|
|
29
|
+
local scan_count = 0
|
|
30
|
+
local scan_durations = {} -- last 100 scan durations
|
|
31
|
+
local scan_idx = 0
|
|
24
32
|
|
|
25
33
|
-- =============================================================================
|
|
26
34
|
-- JSON Parser (minimal, sufficient for our command format)
|
|
@@ -243,6 +251,23 @@ local function list_files(dir, prefix)
|
|
|
243
251
|
return files
|
|
244
252
|
end
|
|
245
253
|
|
|
254
|
+
-- Read pending command IDs from the notify file (avoids directory listing entirely).
|
|
255
|
+
-- Returns a list of command filenames, or falls back to list_files if no notify file.
|
|
256
|
+
local notify_path = bridge_dir .. "_notify"
|
|
257
|
+
local function read_notify()
|
|
258
|
+
local f = io.open(notify_path, "r")
|
|
259
|
+
if not f then return nil end
|
|
260
|
+
local content = f:read("*a")
|
|
261
|
+
f:close()
|
|
262
|
+
os.remove(notify_path)
|
|
263
|
+
if not content or content == "" then return nil end
|
|
264
|
+
local files = {}
|
|
265
|
+
for id in content:gmatch("[^\n]+") do
|
|
266
|
+
files[#files + 1] = "command_" .. id .. ".json"
|
|
267
|
+
end
|
|
268
|
+
return files
|
|
269
|
+
end
|
|
270
|
+
|
|
246
271
|
-- =============================================================================
|
|
247
272
|
-- dB conversion helpers
|
|
248
273
|
-- =============================================================================
|
|
@@ -2442,7 +2467,12 @@ function handlers.create_track_envelope(params)
|
|
|
2442
2467
|
if not chunk:find(chunk_key) then
|
|
2443
2468
|
-- Insert a minimal envelope chunk before the closing >
|
|
2444
2469
|
local env_chunk = "\n<" .. chunk_key .. "\nACT 1 -1\nVIS 1 1 1\nLANEHEIGHT 0 0\nARM 0\nDEFSHAPE 0 -1 -1\n>\n"
|
|
2445
|
-
|
|
2470
|
+
-- Use position capture to find the last ">" (closing the <TRACK block).
|
|
2471
|
+
-- We cannot use "\n>$" because GetTrackStateChunk may include a trailing newline.
|
|
2472
|
+
local last_gt = chunk:match(".*()>")
|
|
2473
|
+
if last_gt then
|
|
2474
|
+
chunk = chunk:sub(1, last_gt - 1) .. env_chunk .. chunk:sub(last_gt)
|
|
2475
|
+
end
|
|
2446
2476
|
reaper.SetTrackStateChunk(track, chunk, false)
|
|
2447
2477
|
else
|
|
2448
2478
|
-- Envelope exists in chunk but may be hidden; make it visible
|
|
@@ -2624,11 +2654,57 @@ function handlers.insert_envelope_points(params)
|
|
|
2624
2654
|
}
|
|
2625
2655
|
end
|
|
2626
2656
|
|
|
2657
|
+
-- =============================================================================
|
|
2658
|
+
-- Bridge diagnostics handler
|
|
2659
|
+
-- =============================================================================
|
|
2660
|
+
|
|
2661
|
+
handlers["_bridge_diagnostics"] = function(params)
|
|
2662
|
+
-- Compute defer interval stats from circular buffer
|
|
2663
|
+
local intervals = {}
|
|
2664
|
+
for i = 1, math.min(defer_count, defer_buf_size) do
|
|
2665
|
+
intervals[#intervals + 1] = defer_intervals[i]
|
|
2666
|
+
end
|
|
2667
|
+
table.sort(intervals)
|
|
2668
|
+
local n = #intervals
|
|
2669
|
+
local sum = 0
|
|
2670
|
+
for _, v in ipairs(intervals) do sum = sum + v end
|
|
2671
|
+
|
|
2672
|
+
local scan_times = {}
|
|
2673
|
+
for i = 1, math.min(scan_count, defer_buf_size) do
|
|
2674
|
+
scan_times[#scan_times + 1] = scan_durations[i]
|
|
2675
|
+
end
|
|
2676
|
+
table.sort(scan_times)
|
|
2677
|
+
local sn = #scan_times
|
|
2678
|
+
local ssum = 0
|
|
2679
|
+
for _, v in ipairs(scan_times) do ssum = ssum + v end
|
|
2680
|
+
|
|
2681
|
+
return {
|
|
2682
|
+
pollInterval = POLL_INTERVAL * 1000,
|
|
2683
|
+
deferCount = defer_count,
|
|
2684
|
+
scanCount = scan_count,
|
|
2685
|
+
deferIntervals = n > 0 and {
|
|
2686
|
+
count = n,
|
|
2687
|
+
avgMs = math.floor(sum / n * 1000 + 0.5) / 1000,
|
|
2688
|
+
minMs = math.floor(intervals[1] * 1000 * 1000 + 0.5) / 1000,
|
|
2689
|
+
maxMs = math.floor(intervals[n] * 1000 * 1000 + 0.5) / 1000,
|
|
2690
|
+
p50Ms = math.floor(intervals[math.floor(n * 0.5) + 1] * 1000 * 1000 + 0.5) / 1000,
|
|
2691
|
+
p95Ms = math.floor(intervals[math.floor(n * 0.95) + 1] * 1000 * 1000 + 0.5) / 1000,
|
|
2692
|
+
} or nil,
|
|
2693
|
+
scanDurations = sn > 0 and {
|
|
2694
|
+
count = sn,
|
|
2695
|
+
avgMs = math.floor(ssum / sn * 1000 + 0.5) / 1000,
|
|
2696
|
+
minMs = math.floor(scan_times[1] * 1000 * 1000 + 0.5) / 1000,
|
|
2697
|
+
maxMs = math.floor(scan_times[sn] * 1000 * 1000 + 0.5) / 1000,
|
|
2698
|
+
} or nil,
|
|
2699
|
+
}
|
|
2700
|
+
end
|
|
2701
|
+
|
|
2627
2702
|
-- =============================================================================
|
|
2628
2703
|
-- Command dispatcher
|
|
2629
2704
|
-- =============================================================================
|
|
2630
2705
|
|
|
2631
2706
|
local function process_command(filename)
|
|
2707
|
+
local pickup_time = reaper.time_precise()
|
|
2632
2708
|
local path = bridge_dir .. filename
|
|
2633
2709
|
local content = read_file(path)
|
|
2634
2710
|
if not content then return end
|
|
@@ -2660,6 +2736,13 @@ local function process_command(filename)
|
|
|
2660
2736
|
}
|
|
2661
2737
|
end
|
|
2662
2738
|
|
|
2739
|
+
-- Add pickup timing to response for profiling
|
|
2740
|
+
if cmd.timestamp then
|
|
2741
|
+
response._pickupMs = math.floor((pickup_time - (cmd.timestamp / 1000)) * 1000 + 0.5)
|
|
2742
|
+
response._deferCycle = defer_count
|
|
2743
|
+
response._scanCycle = scan_count
|
|
2744
|
+
end
|
|
2745
|
+
|
|
2663
2746
|
-- Write response
|
|
2664
2747
|
local response_path = bridge_dir .. "response_" .. cmd.id .. ".json"
|
|
2665
2748
|
write_file(response_path, json_encode(response))
|
|
@@ -2688,13 +2771,26 @@ end
|
|
|
2688
2771
|
local function main_loop()
|
|
2689
2772
|
local now = reaper.time_precise()
|
|
2690
2773
|
|
|
2691
|
-
--
|
|
2774
|
+
-- Track defer cadence
|
|
2775
|
+
if last_defer_time > 0 then
|
|
2776
|
+
defer_count = defer_count + 1
|
|
2777
|
+
defer_idx = (defer_idx % defer_buf_size) + 1
|
|
2778
|
+
defer_intervals[defer_idx] = now - last_defer_time
|
|
2779
|
+
end
|
|
2780
|
+
last_defer_time = now
|
|
2781
|
+
|
|
2782
|
+
-- Poll for commands: prefer notify file (instant), fall back to dir listing
|
|
2692
2783
|
if now - last_poll >= POLL_INTERVAL then
|
|
2693
2784
|
last_poll = now
|
|
2694
|
-
local
|
|
2785
|
+
local scan_start = reaper.time_precise()
|
|
2786
|
+
local files = read_notify() or list_files(bridge_dir, "command_")
|
|
2695
2787
|
for _, filename in ipairs(files) do
|
|
2696
2788
|
process_command(filename)
|
|
2697
2789
|
end
|
|
2790
|
+
local scan_dur = reaper.time_precise() - scan_start
|
|
2791
|
+
scan_count = scan_count + 1
|
|
2792
|
+
scan_idx = (scan_idx % defer_buf_size) + 1
|
|
2793
|
+
scan_durations[scan_idx] = scan_dur
|
|
2698
2794
|
end
|
|
2699
2795
|
|
|
2700
2796
|
-- Write heartbeat at interval
|