Add get_notes_from_clip and delete_notes_from_clip tools#83
Add get_notes_from_clip and delete_notes_from_clip tools#83meleyal wants to merge 1 commit intoahujasid:mainfrom
Conversation
Adds two new MCP tools: - get_notes_from_clip: reads all MIDI notes from a clip, returning pitch, start_time, duration, velocity, and mute for each note - delete_notes_from_clip: removes all MIDI notes from a clip without deleting the clip itself, enabling true overwrite when combined with add_notes_to_clip
📝 WalkthroughWalkthroughThis PR introduces two new MCP tools for Ableton clip note management: Changes
Sequence DiagramsequenceDiagram
actor Client
participant MCP as MCP Server
participant RS as Remote Script
participant Live as Ableton Live
rect rgba(100, 200, 150, 0.5)
Note over Client,Live: Get Notes from Clip Flow
Client->>MCP: get_notes_from_clip(track_idx, clip_idx)
MCP->>RS: Send "get_notes_from_clip" command
RS->>RS: Validate track/clip indices
RS->>Live: clip.get_notes(loop_start to loop_end)
Live-->>RS: Raw MIDI note tuples
RS->>RS: Map to dicts & sort by (start_time, pitch)
RS->>MCP: Return notes + clip metadata (JSON)
MCP-->>Client: JSON formatted notes
end
rect rgba(150, 150, 200, 0.5)
Note over Client,Live: Delete Notes from Clip Flow
Client->>MCP: delete_notes_from_clip(track_idx, clip_idx)
MCP->>RS: Send "delete_notes_from_clip" command
RS->>RS: Validate MIDI clip presence/type
RS->>Live: clip.remove_notes(loop_start to loop_end)
Live-->>RS: Deletion complete
RS->>MCP: Return status payload (JSON)
MCP-->>Client: Confirmation response
end
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Review Summary by QodoAdd get_notes_from_clip and delete_notes_from_clip MCP tools
WalkthroughsDescription• Adds get_notes_from_clip tool to read MIDI notes from clips • Adds delete_notes_from_clip tool to remove all notes from clips • Enables full clip overwrite workflow with note deletion capability • Routes delete_notes_from_clip on main thread for safe state mutation Diagramflowchart LR
A["MCP Server"] -->|get_notes_from_clip| B["Remote Script Handler"]
A -->|delete_notes_from_clip| B
B -->|Main Thread| C["Ableton Live API"]
C -->|Read Notes| D["MIDI Clip"]
C -->|Remove Notes| D
D -->|Return Notes List| B
B -->|JSON Response| A
File Changes1. AbletonMCP_Remote_Script/__init__.py
|
Code Review by Qodo
|
| notes_raw = clip.get_notes(clip.loop_start, 0, clip.loop_end, 128) | ||
| notes = [] | ||
| for n in notes_raw: | ||
| notes.append({ | ||
| "pitch": int(n[0]), | ||
| "start_time": float(n[1]), | ||
| "duration": float(n[2]), | ||
| "velocity": int(n[3]), | ||
| "mute": bool(n[4]), | ||
| }) | ||
| notes.sort(key=lambda n: (n["start_time"], n["pitch"])) | ||
|
|
||
| return { | ||
| "track_index": track_index, | ||
| "clip_index": clip_index, | ||
| "clip_length": float(clip.length), | ||
| "note_count": len(notes), | ||
| "notes": notes, | ||
| } | ||
| except Exception as e: | ||
| self.log_message("Error getting notes from clip: " + str(e)) | ||
| raise | ||
|
|
||
| def _delete_notes_from_clip(self, track_index, clip_index): | ||
| """Remove every MIDI note from a clip, leaving the clip intact.""" | ||
| try: | ||
| if track_index < 0 or track_index >= len(self._song.tracks): | ||
| raise IndexError("Track index out of range") | ||
| track = self._song.tracks[track_index] | ||
| if clip_index < 0 or clip_index >= len(track.clip_slots): | ||
| raise IndexError("Clip index out of range") | ||
| clip_slot = track.clip_slots[clip_index] | ||
| if not clip_slot.has_clip: | ||
| raise Exception("No clip at track {0}, slot {1}".format(track_index, clip_index)) | ||
| clip = clip_slot.clip | ||
| if not clip.is_midi_clip: | ||
| raise Exception("Clip is not a MIDI clip") | ||
|
|
||
| clip.remove_notes(clip.loop_start, 0, clip.loop_end, 128) |
There was a problem hiding this comment.
1. Loop-bounded note operations 🐞 Bug ≡ Correctness
_get_notes_from_clip / _delete_notes_from_clip only read/delete notes within clip.loop_start..clip.loop_end, so notes outside the loop region will not be returned or deleted despite the docstrings claiming “all/every” notes.
Agent Prompt
### Issue description
`_get_notes_from_clip` and `_delete_notes_from_clip` claim to operate on **all** notes in a MIDI clip, but they call `get_notes/remove_notes` using `clip.loop_start` and `clip.loop_end`, which limits the operation to the loop region only.
### Issue Context
Both tools are intended to enable full overwrite workflows (`delete_notes_from_clip` + `add_notes_to_clip`). If notes exist outside the loop region, they will remain and the overwrite won’t be true.
### Fix Focus Areas
- AbletonMCP_Remote_Script/__init__.py[1073-1132]
### Suggested change
Update both calls to cover the full clip range (e.g., start at `0.0` for `clip.length`), or if loop-only behavior is intended, update the docstrings/return fields to explicitly describe loop-only semantics.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
MCP_Server/server.py (1)
678-694: Classifydelete_notes_from_clipwith the other mutating commands.This new tool introduces the delete-then-add overwrite path, but
AbletonConnection.send_command()still treatsdelete_notes_from_cliplike a read. Adding it tois_modifying_commandwould keep it on the same write-pacing path as the other clip mutations and make back-to-back overwrite calls less brittle.# in AbletonConnection.send_command() is_modifying_command = command_type in [ "create_midi_track", "create_audio_track", "set_track_name", "create_clip", "add_notes_to_clip", "set_clip_name", "set_tempo", "fire_clip", "stop_clip", "set_device_parameter", "start_playback", "stop_playback", "load_instrument_or_effect", "delete_notes_from_clip", ]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCP_Server/server.py` around lines 678 - 694, The new delete_notes_from_clip tool is currently treated as a read by AbletonConnection.send_command, causing it to bypass write pacing; update AbletonConnection.send_command to include "delete_notes_from_clip" in the is_modifying_command list (the list checked against command_type) so delete_notes_from_clip is classified with other mutating commands and follows the same write-pacing/path as create_clip, add_notes_to_clip, etc.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@AbletonMCP_Remote_Script/__init__.py`:
- Around line 1088-1098: The code calls clip.get_notes(clip.loop_start, 0,
clip.loop_end, 128) which misuses the third argument (time_span) and thus
queries the wrong range; change calls to use from_time=0.0 and
time_span=clip.length so that clip.get_notes(...) and the matching
clip.remove_notes(...) cover the entire clip range (use clip.get_notes(0.0, 0,
clip.length, 128) and the corresponding remove_notes(0.0, 0, clip.length)
invocation), and apply the same fix to the second occurrence around the code
that was flagged (the block near line 1126) so both note-read and note-remove
operations operate over the full clip.
---
Nitpick comments:
In `@MCP_Server/server.py`:
- Around line 678-694: The new delete_notes_from_clip tool is currently treated
as a read by AbletonConnection.send_command, causing it to bypass write pacing;
update AbletonConnection.send_command to include "delete_notes_from_clip" in the
is_modifying_command list (the list checked against command_type) so
delete_notes_from_clip is classified with other mutating commands and follows
the same write-pacing/path as create_clip, add_notes_to_clip, etc.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3734f300-b354-4f49-a7f1-3703cb69b83e
📒 Files selected for processing (2)
AbletonMCP_Remote_Script/__init__.pyMCP_Server/server.py
| notes_raw = clip.get_notes(clip.loop_start, 0, clip.loop_end, 128) | ||
| notes = [] | ||
| for n in notes_raw: | ||
| notes.append({ | ||
| "pitch": int(n[0]), | ||
| "start_time": float(n[1]), | ||
| "duration": float(n[2]), | ||
| "velocity": int(n[3]), | ||
| "mute": bool(n[4]), | ||
| }) | ||
| notes.sort(key=lambda n: (n["start_time"], n["pitch"])) |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Ableton Live Clip API: for the Python Clip.get_notes(from_time, from_pitch, time_span, pitch_span)andremove_notes(from_time, from_pitch, time_span, pitch_span) calls, does the third argument represent a span, and what range should be used to cover every note in a clip?
💡 Result:
Yes, the third argument (time_span) represents a span: it defines the end of the time range as from_time + time_span (in beats of absolute clip time). The fourth argument (pitch_span) works similarly: end pitch range is from_pitch + pitch_span. To cover every note in a clip, use from_time=0, time_span=clip.length (or a value >= clip.length to be safe), from_pitch=0, pitch_span=128. This covers time [0, clip.length] and pitches [0, 128) which includes all possible MIDI pitches (0-127). Note that get_notes(o) returns notes whose start times fall within the specified rectangular range [from_time, from_time + time_span) x [from_pitch, from_pitch + pitch_span). Notes may extend outside this range but are included if their start is inside. The functions do not limit to loop brace or markers; notes outside can be queried/removed with sufficient span. These are the extended versions (get_notes_extended, remove_notes_extended) recommended since Live 11 for MPE support; the legacy versions are deprecated.
Citations:
- 1: https://adammurray.link/max-for-live/js-in-live/generating-midi-clips/
- 2: https://docs.cycling74.com/apiref/lom/clip
- 3: https://cycling74.com/forums/getnotesextended-assistance
- 4: https://www.youtube.com/watch?v=i3FkxaV9vu4
🏁 Script executed:
cd AbletonMCP_Remote_Script && head -1135 __init__.py | tail -+1085 | cat -nRepository: ahujasid/ableton-mcp
Length of output: 131
🏁 Script executed:
head -1135 AbletonMCP_Remote_Script/__init__.py | tail -n +1085 | cat -nRepository: ahujasid/ableton-mcp
Length of output: 2506
Use the full clip range for these note operations.
Clip.get_notes() and remove_notes() take time_span as their third argument—the time range covered is [from_time, from_time + time_span). Passing clip.loop_end as time_span causes two issues: it extends beyond the loop boundary (e.g., if loop_start=2 and loop_end=8, you query [2, 10) instead of [2, 8)), and it misses all notes outside the loop (times 0–loop_start). Since both functions' docstrings claim to operate on "every MIDI note" / "all notes in a clip," use 0.0 as from_time and clip.length as time_span to cover the entire clip range.
💡 Suggested fix
- notes_raw = clip.get_notes(clip.loop_start, 0, clip.loop_end, 128)
+ notes_raw = clip.get_notes(0.0, 0, clip.length, 128)
- clip.remove_notes(clip.loop_start, 0, clip.loop_end, 128)
+ clip.remove_notes(0.0, 0, clip.length, 128)Also applies to: 1126
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@AbletonMCP_Remote_Script/__init__.py` around lines 1088 - 1098, The code
calls clip.get_notes(clip.loop_start, 0, clip.loop_end, 128) which misuses the
third argument (time_span) and thus queries the wrong range; change calls to use
from_time=0.0 and time_span=clip.length so that clip.get_notes(...) and the
matching clip.remove_notes(...) cover the entire clip range (use
clip.get_notes(0.0, 0, clip.length, 128) and the corresponding remove_notes(0.0,
0, clip.length) invocation), and apply the same fix to the second occurrence
around the code that was flagged (the block near line 1126) so both note-read
and note-remove operations operate over the full clip.
Summary
get_notes_from_clip: reads all MIDI notes from a clip, returning each note's pitch, start_time, duration, velocity, and mute — sorted by start timedelete_notes_from_clip: removes all MIDI notes from a clip without deleting the clip itself, enabling true overwrite when combined withadd_notes_to_clipdelete_notes_from_clipis correctly dispatched on Ableton's main thread (alongsideadd_notes_to_clip) sinceclip.remove_notes()mutates Live's stateTest plan
get_notes_from_clipand verify notes are returned correctlydelete_notes_from_clipand verify the clip is empty but still existsadd_notes_to_clipafterdelete_notes_from_clipto confirm full overwrite worksSummary by CodeRabbit
New Features