Skip to content

Add get_notes_from_clip and delete_notes_from_clip tools#83

Open
meleyal wants to merge 1 commit intoahujasid:mainfrom
meleyal:meleyal.get-delete-notes-from-clip
Open

Add get_notes_from_clip and delete_notes_from_clip tools#83
meleyal wants to merge 1 commit intoahujasid:mainfrom
meleyal:meleyal.get-delete-notes-from-clip

Conversation

@meleyal
Copy link
Copy Markdown

@meleyal meleyal commented Mar 25, 2026

Summary

  • Adds 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 time
  • Adds 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
  • delete_notes_from_clip is correctly dispatched on Ableton's main thread (alongside add_notes_to_clip) since clip.remove_notes() mutates Live's state

Test plan

  • Create a MIDI clip with some notes
  • Call get_notes_from_clip and verify notes are returned correctly
  • Call delete_notes_from_clip and verify the clip is empty but still exists
  • Call add_notes_to_clip after delete_notes_from_clip to confirm full overwrite works

Summary by CodeRabbit

New Features

  • Extract MIDI note information from clips, including pitch, timing, duration, velocity, and mute status
  • Delete MIDI notes from clips in your Ableton session to easily modify your compositions

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
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 25, 2026

📝 Walkthrough

Walkthrough

This PR introduces two new MCP tools for Ableton clip note management: get_notes_from_clip retrieves and formats MIDI notes with metadata, while delete_notes_from_clip removes all notes from a clip's loop range. Implementation spans the MCP server interface layer and remote script command handlers.

Changes

Cohort / File(s) Summary
Remote Script Command Handlers
AbletonMCP_Remote_Script/__init__.py
Added two private methods: _get_notes_from_clip() reads MIDI notes and returns them as sorted dicts with pitch, start_time, duration, velocity, and mute properties; _delete_notes_from_clip() removes all notes from a clip's loop range. Extended command routing to dispatch these handlers on the main thread (delete) and non-main thread (get).
MCP Server Tool Endpoints
MCP_Server/server.py
Added two public tools: get_notes_from_clip() and delete_notes_from_clip(), both accepting track and clip indices. Each establishes an Ableton connection, sends the corresponding remote script command, and returns JSON-formatted results or error messages.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 With whiskers twitching, the rabbit discovered delight,
In fetching and deleting MIDI notes, oh what a sight!
From Ableton's chambers to servers so bright,
The notes dance and vanish—all handled just right! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main changes in the pull request: adding two new tools (get_notes_from_clip and delete_notes_from_clip) across both the MCP server and the Ableton Remote Script.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@meleyal meleyal marked this pull request as ready for review April 13, 2026 09:40
@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Add get_notes_from_clip and delete_notes_from_clip MCP tools

✨ Enhancement

Grey Divider

Walkthroughs

Description
• 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
Diagram
flowchart 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
Loading

Grey Divider

File Changes

1. AbletonMCP_Remote_Script/__init__.py ✨ Enhancement +77/-4

Implement note read and delete operations

• Adds _get_notes_from_clip() method to extract all MIDI notes from a clip with pitch, start_time,
 duration, velocity, and mute properties
• Adds _delete_notes_from_clip() method to remove all notes from a clip while preserving the clip
 itself
• Routes delete_notes_from_clip command through main thread dispatcher for safe state mutation
• Adds command routing for both new tools in the _process_command() method

AbletonMCP_Remote_Script/init.py


2. MCP_Server/server.py ✨ Enhancement +45/-0

Expose note management tools to MCP interface

• Adds get_notes_from_clip() MCP tool that queries clip notes and returns formatted JSON response
• Adds delete_notes_from_clip() MCP tool that removes all notes from a specified clip
• Both tools accept track_index and clip_index parameters for clip targeting
• Includes comprehensive docstrings explaining parameters and use cases

MCP_Server/server.py


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Apr 13, 2026

Code Review by Qodo

🐞 Bugs (2)   📘 Rule violations (0)   📎 Requirement gaps (0)   🖥 UI issues (0)   🎨 UX Issues (0)
🐞\ ≡ Correctness (1) ☼ Reliability (1)

Grey Divider


Action required

1. Loop-bounded note operations 🐞
Description
_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.
Code

AbletonMCP_Remote_Script/init.py[R1088-1126]

+            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)
Evidence
The implementation queries/removes notes using loop boundaries, not the clip’s full time range,
while the docstrings state it returns/removes all notes in the clip. This makes the tool silently
incomplete whenever the loop region doesn’t span the full clip.

AbletonMCP_Remote_Script/init.py[1073-1106]
AbletonMCP_Remote_Script/init.py[1111-1132]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### 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



Remediation recommended

2. Delete command not flagged mutating 🐞
Description
delete_notes_from_clip sends a state-mutating command, but AbletonConnection.send_command
doesn’t include it in is_modifying_command, so it won’t get the longer timeout and pre/post delays
used for mutating operations.
Code

MCP_Server/server.py[R689-694]

+        ableton = get_ableton_connection()
+        result = ableton.send_command("delete_notes_from_clip", {
+            "track_index": track_index,
+            "clip_index": clip_index,
+        })
+        return json.dumps(result, indent=2)
Evidence
The new MCP tool calls send_command("delete_notes_from_clip", ...), but send_command’s
mutating-command allowlist omits delete_notes_from_clip, which directly affects timeout selection
and whether sleeps are applied around the request/response.

MCP_Server/server.py[93-145]
MCP_Server/server.py[677-697]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`AbletonConnection.send_command()` uses an allowlist (`is_modifying_command`) to decide whether to apply longer timeouts and pre/post delays. The new `delete_notes_from_clip` command mutates Live state but is not in that list.

### Issue Context
The PR adds the `delete_notes_from_clip` tool in `MCP_Server/server.py`, and the command is handled on Ableton’s main thread on the remote-script side. The MCP server should still treat it as state-modifying for consistent socket timing behavior.

### Fix Focus Areas
- MCP_Server/server.py[103-109]

### Suggested change
Include `"delete_notes_from_clip"` in the `is_modifying_command` list so it uses the mutating-command timeout and delays.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines +1088 to +1126
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
MCP_Server/server.py (1)

678-694: Classify delete_notes_from_clip with the other mutating commands.

This new tool introduces the delete-then-add overwrite path, but AbletonConnection.send_command() still treats delete_notes_from_clip like a read. Adding it to is_modifying_command would 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

📥 Commits

Reviewing files that changed from the base of the PR and between e008328 and ad149a6.

📒 Files selected for processing (2)
  • AbletonMCP_Remote_Script/__init__.py
  • MCP_Server/server.py

Comment on lines +1088 to +1098
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"]))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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:


🏁 Script executed:

cd AbletonMCP_Remote_Script && head -1135 __init__.py | tail -+1085 | cat -n

Repository: ahujasid/ableton-mcp

Length of output: 131


🏁 Script executed:

head -1135 AbletonMCP_Remote_Script/__init__.py | tail -n +1085 | cat -n

Repository: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant