Skip to content

feat: Add get_notes_from_clip to read MIDI notes from clips#54

Open
billy-and-the-oceans wants to merge 2 commits intoahujasid:mainfrom
billy-and-the-oceans:main
Open

feat: Add get_notes_from_clip to read MIDI notes from clips#54
billy-and-the-oceans wants to merge 2 commits intoahujasid:mainfrom
billy-and-the-oceans:main

Conversation

@billy-and-the-oceans
Copy link
Copy Markdown

@billy-and-the-oceans billy-and-the-oceans commented Jan 8, 2026

User description

Summary

This PR adds a new get_notes_from_clip function that allows reading existing MIDI notes from clips in Ableton Live. This was previously missing from the API - we could write notes but not read them.

New capability:

  • get_notes_from_clip(track_index, clip_index) returns JSON with:
    • clip_name: Name of the clip
    • length: Clip length in beats
    • note_count: Number of notes
    • notes: Array of note objects (pitch, start_time, duration, velocity, mute)

Use Cases

  • Analyze existing clips and patterns
  • Visualize MIDI data
  • Transpose or transform notes programmatically
  • Build on existing musical content
  • AI-assisted music production workflows

Changes

  • AbletonMCP_Remote_Script/__init__.py: Added _get_notes_from_clip() method using clip.get_notes_extended() API
  • MCP_Server/server.py: Added @mcp.tool() decorated get_notes_from_clip() function
  • MCP_Server/server.py: Updated FastMCP init to use instructions parameter (newer mcp library compatibility)
  • README.md: Updated capabilities list and example commands

Test Plan

  • Tested reading drum clips with 100+ notes
  • Tested with different clip lengths (4, 8, 32 beats)
  • Verified all note properties are returned correctly
  • Works with both Live 11 and Live 12

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 noreply@anthropic.com


PR Type

Enhancement


Description

  • Add get_notes_from_clip() function to read MIDI notes from clips

  • Implement note retrieval using Ableton's get_notes_extended() API

  • Update FastMCP initialization parameter from description to instructions

  • Expand README with new capability and usage examples


Diagram Walkthrough

flowchart LR
  A["User Request"] -->|track_index, clip_index| B["get_notes_from_clip"]
  B -->|sends command| C["Remote Script Handler"]
  C -->|_get_notes_from_clip| D["Ableton API"]
  D -->|get_notes_extended| E["Extract Note Data"]
  E -->|format notes| F["JSON Response"]
  F -->|clip_name, length, notes| A
Loading

File Walkthrough

Relevant files
Enhancement
__init__.py
Implement note reading from MIDI clips                                     

AbletonMCP_Remote_Script/init.py

  • Added command routing for get_notes_from_clip in _process_command()
  • Implemented _get_notes_from_clip() method using Ableton's
    get_notes_extended() API
  • Extracts pitch, start_time, duration, velocity, and mute properties
    from notes
  • Returns clip metadata (name, length, note_count) along with notes
    array
  • Fixed whitespace formatting in _add_notes_to_clip() method
+58/-9   
server.py
Add MCP tool for reading clip notes                                           

MCP_Server/server.py

  • Added @mcp.tool() decorated get_notes_from_clip() function
  • Updated FastMCP initialization to use instructions parameter instead
    of deprecated description
  • Function sends command to remote script and returns JSON-formatted
    note data
  • Fixed whitespace formatting in add_notes_to_clip() docstring
+28/-5   
Documentation
README.md
Document new note reading capability                                         

README.md

  • Added "Read notes from existing MIDI clips" to capabilities list with
    NEW marker
  • Added two example commands demonstrating note reading and
    transposition workflows
+3/-0     

Summary by CodeRabbit

  • New Features

    • Retrieve MIDI notes from existing clips with detailed note information including pitch, timing, duration, velocity, and mute status. Metadata includes clip name and total note count.
  • Documentation

    • Updated with new command examples and capability descriptions for reading MIDI note data from clips.

✏️ Tip: You can customize this high-level summary in your review settings.

billy-and-the-oceans and others added 2 commits January 8, 2026 23:39
This adds the ability to read existing MIDI notes from clips, which was
previously missing from the API. The new function returns:
- clip_name: Name of the clip
- length: Clip length in beats
- note_count: Number of notes
- notes: Array of note objects with pitch, start_time, duration, velocity, mute

Changes:
- AbletonMCP_Remote_Script/__init__.py: Added _get_notes_from_clip method
  using clip.get_notes_extended() API, and command handler
- MCP_Server/server.py: Added @mcp.tool() get_notes_from_clip function
- README.md: Updated capabilities and example commands

This enables AI assistants to analyze existing clips, visualize patterns,
transpose notes, and build on existing musical content.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The newer version of the mcp library changed the parameter name from
'description' to 'instructions' in FastMCP.__init__().

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 8, 2026

📝 Walkthrough

Walkthrough

A new feature enables retrieval of MIDI notes from existing clips. Changes include a backend handler in the Ableton remote script that extracts notes and converts them to a serializable format, a public MCP server tool that exposes this functionality, and corresponding documentation updates.

Changes

Cohort / File(s) Summary
Feature Implementation
AbletonMCP_Remote_Script/__init__.py, MCP_Server/server.py
Added _get_notes_from_clip method to extract MIDI notes (pitch, start_time, duration, velocity, mute) from clips with error handling. Exposed via new public get_notes_from_clip tool in MCP server. Updated FastMCP initialization to use instructions= parameter instead of description=.
Documentation
README.md
Extended Features and Capabilities sections with descriptions of new get_notes_from_clip command and example usage.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant MCP Server
    participant Ableton Script
    participant Ableton App

    Client->>MCP Server: Call get_notes_from_clip(track_index, clip_index)
    MCP Server->>Ableton Script: _process_command(get_notes_from_clip, params)
    Ableton Script->>Ableton App: Access clip from track
    Ableton App-->>Ableton Script: Clip object with notes
    Ableton Script->>Ableton Script: Extract & serialize notes (pitch, timing, velocity, mute)
    Ableton Script-->>MCP Server: Return JSON with notes + metadata
    MCP Server-->>Client: Indented JSON response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Notes from clips, oh what a delight!
The rabbit hops through MIDI's light,
Pitch and timing, velocity true,
A melody extracted, fresh and new! 🎵✨

🚥 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 PR title accurately describes the main change: adding a get_notes_from_clip function to read MIDI notes from clips, which is the primary feature across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.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 docstrings

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.

@qodo-code-review
Copy link
Copy Markdown

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Resource exhaustion

Description: The new get_notes_from_clip path fetches and serializes all notes across the full clip
(clip.get_notes_extended(0, 128, 0.0, clip.length)) without any upper bounds, enabling a
potentially untrusted caller to trigger excessive CPU/memory usage (and large downstream
JSON responses) by targeting very long/dense clips.
init.py [528-571]

Referred Code
def _get_notes_from_clip(self, track_index, clip_index):
    """Get all MIDI notes from a clip"""
    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 in slot")

        clip = clip_slot.clip

        # Get all notes from the clip
        # get_notes_extended(from_pitch, pitch_span, from_time, time_span)
        # Get all pitches (0-128) for the full clip length


 ... (clipped 23 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Raw error returned: The new tool catches exceptions but returns the raw exception message to the caller
instead of a structured error response with safe, actionable context.

Referred Code
try:
    ableton = get_ableton_connection()
    result = ableton.send_command("get_notes_from_clip", {
        "track_index": track_index,
        "clip_index": clip_index
    })
    return json.dumps(result, indent=2)
except Exception as e:
    logger.error(f"Error getting notes from clip: {str(e)}")
    return f"Error getting notes from clip: {str(e)}"

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
Internal details exposed: The new get_notes_from_clip tool returns str(e) directly to the end user, which can leak
internal implementation details and environment information.

Referred Code
try:
    ableton = get_ableton_connection()
    result = ableton.send_command("get_notes_from_clip", {
        "track_index": track_index,
        "clip_index": clip_index
    })
    return json.dumps(result, indent=2)
except Exception as e:
    logger.error(f"Error getting notes from clip: {str(e)}")
    return f"Error getting notes from clip: {str(e)}"

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing audit context: The new get_notes_from_clip action is not audit-logged with user identity and outcome
context, making it hard to reconstruct who accessed clip data.

Referred Code
@mcp.tool()
def get_notes_from_clip(ctx: Context, track_index: int, clip_index: int) -> str:
    """
    Get all MIDI notes from a clip.

    Parameters:
    - track_index: The index of the track containing the clip
    - clip_index: The index of the clip slot containing the clip

    Returns: JSON with clip_name, length, note_count, and notes array.
             Each note has pitch, start_time, duration, velocity, and mute.
    """
    try:
        ableton = get_ableton_connection()
        result = ableton.send_command("get_notes_from_clip", {
            "track_index": track_index,
            "clip_index": clip_index
        })
        return json.dumps(result, indent=2)
    except Exception as e:
        logger.error(f"Error getting notes from clip: {str(e)}")


 ... (clipped 1 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Missing input validation: The new get_notes_from_clip tool forwards track_index and clip_index without local
validation or bounds checks, relying on the remote script for rejection of invalid inputs.

Referred Code
@mcp.tool()
def get_notes_from_clip(ctx: Context, track_index: int, clip_index: int) -> str:
    """
    Get all MIDI notes from a clip.

    Parameters:
    - track_index: The index of the track containing the clip
    - clip_index: The index of the clip slot containing the clip

    Returns: JSON with clip_name, length, note_count, and notes array.
             Each note has pitch, start_time, duration, velocity, and mute.
    """
    try:
        ableton = get_ableton_connection()
        result = ableton.send_command("get_notes_from_clip", {
            "track_index": track_index,
            "clip_index": clip_index
        })
        return json.dumps(result, indent=2)

Learn more about managing compliance generic rules or creating your own custom rules

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Expose filtering options for note retrieval

Enhance get_notes_from_clip to accept optional time and pitch range parameters.
This allows fetching a specific subset of notes, improving efficiency by passing
these filters to the clip.get_notes_extended() API.

Examples:

AbletonMCP_Remote_Script/__init__.py [528-571]
    def _get_notes_from_clip(self, track_index, clip_index):
        """Get all MIDI notes from a clip"""
        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")

 ... (clipped 34 lines)
MCP_Server/server.py [372-392]
def get_notes_from_clip(ctx: Context, track_index: int, clip_index: int) -> str:
    """
    Get all MIDI notes from a clip.

    Parameters:
    - track_index: The index of the track containing the clip
    - clip_index: The index of the clip slot containing the clip

    Returns: JSON with clip_name, length, note_count, and notes array.
             Each note has pitch, start_time, duration, velocity, and mute.

 ... (clipped 11 lines)

Solution Walkthrough:

Before:

# In MCP_Server/server.py
@mcp.tool()
def get_notes_from_clip(ctx: Context, track_index: int, clip_index: int) -> str:
    try:
        ableton = get_ableton_connection()
        result = ableton.send_command("get_notes_from_clip", {
            "track_index": track_index,
            "clip_index": clip_index
        })
        return json.dumps(result, indent=2)
    ...

# In AbletonMCP_Remote_Script/__init__.py
def _get_notes_from_clip(self, track_index, clip_index):
    ...
    clip = clip_slot.clip
    # Always get all notes for the full clip length
    notes_tuple = clip.get_notes_extended(0, 128, 0.0, clip.length)
    ...
    return result

After:

# In MCP_Server/server.py
@mcp.tool()
def get_notes_from_clip(ctx: Context, track_index: int, clip_index: int, from_pitch: int = None, pitch_span: int = None, from_time: float = None, time_span: float = None) -> str:
    try:
        ableton = get_ableton_connection()
        params = {"track_index": track_index, "clip_index": clip_index}
        if from_pitch is not None: params["from_pitch"] = from_pitch
        # ... add other optional params
        result = ableton.send_command("get_notes_from_clip", params)
        return json.dumps(result, indent=2)
    ...

# In AbletonMCP_Remote_Script/__init__.py
def _get_notes_from_clip(self, track_index, clip_index, **kwargs):
    ...
    clip = clip_slot.clip
    from_pitch = kwargs.get("from_pitch", 0)
    pitch_span = kwargs.get("pitch_span", 128)
    from_time = kwargs.get("from_time", 0.0)
    time_span = kwargs.get("time_span", clip.length)
    notes_tuple = clip.get_notes_extended(from_pitch, pitch_span, from_time, time_span)
    ...
    return result
Suggestion importance[1-10]: 8

__

Why: This is a valuable suggestion that correctly identifies a limitation in the new feature and proposes a significant enhancement by leveraging the full capabilities of the underlying get_notes_extended API, improving both performance and flexibility.

Medium
Possible issue
Verify clip is a MIDI clip

Before getting notes from a clip, verify it is a MIDI clip by checking the
is_midi_clip property. If not, raise a TypeError to prevent crashes and provide
a clearer error.

AbletonMCP_Remote_Script/init.py [544-549]

 clip = clip_slot.clip
+
+if not clip.is_midi_clip:
+    raise TypeError("Clip is not a MIDI clip")
 
 # Get all notes from the clip
 # get_notes_extended(from_pitch, pitch_span, from_time, time_span)
 # Get all pitches (0-128) for the full clip length
 notes_tuple = clip.get_notes_extended(0, 128, 0.0, clip.length)
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: This suggestion correctly identifies a potential AttributeError if the clip is not a MIDI clip and provides a robust solution by checking clip.is_midi_clip, which significantly improves error handling and prevents crashes.

Medium
  • More

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

🧹 Nitpick comments (1)
README.md (1)

132-132: Clear documentation of the new capability.

The capability description accurately reflects the new functionality. The "(NEW)" marker is helpful for highlighting recent additions.

Minor suggestion: Consider removing the "(NEW)" tag in a future PR once this feature has been in production for a release or two.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bea865e and bc2c663.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • AbletonMCP_Remote_Script/__init__.py
  • MCP_Server/server.py
  • README.md
🧰 Additional context used
🪛 Ruff (0.14.10)
MCP_Server/server.py

346-346: Unused function argument: ctx

(ARG001)


372-372: Unused function argument: ctx

(ARG001)


390-390: Do not catch blind exception: Exception

(BLE001)


391-391: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


391-391: Use explicit conversion flag

Replace with conversion flag

(RUF010)


392-392: Use explicit conversion flag

Replace with conversion flag

(RUF010)

AbletonMCP_Remote_Script/__init__.py

497-497: Abstract raise to an inner function

(TRY301)


497-497: Avoid specifying long messages outside the exception class

(TRY003)


502-502: Abstract raise to an inner function

(TRY301)


502-502: Create your own exception

(TRY002)


502-502: Avoid specifying long messages outside the exception class

(TRY003)


523-523: Consider moving this statement to an else block

(TRY300)


532-532: Abstract raise to an inner function

(TRY301)


532-532: Avoid specifying long messages outside the exception class

(TRY003)


537-537: Abstract raise to an inner function

(TRY301)


537-537: Avoid specifying long messages outside the exception class

(TRY003)


542-542: Abstract raise to an inner function

(TRY301)


542-542: Create your own exception

(TRY002)


542-542: Avoid specifying long messages outside the exception class

(TRY003)


568-568: Consider moving this statement to an else block

(TRY300)

🔇 Additional comments (7)
MCP_Server/server.py (3)

371-393: LGTM! Clean implementation with consistent error handling.

The new get_notes_from_clip function is well-structured and follows the existing patterns in the codebase. The error handling is appropriate, and returning formatted JSON is correct for this use case.

Note on static analysis hints: The unused ctx parameter warning is a false positive—the MCP framework requires this parameter in tool signatures. The broad exception catch and logging patterns are consistent with all other tools in this file.


345-353: Minor formatting changes look good.

The parameter reordering and spacing adjustments improve readability without changing behavior.


189-189: No action needed — instructions parameter is valid in FastMCP 1.3.0+.

The instructions parameter is documented in the FastMCP SDK as a valid optional parameter (type str | None) that provides human-readable instructions for how MCP clients should interact with the server. The usage is correct and matches the library's API.

AbletonMCP_Remote_Script/__init__.py (3)

228-231: LGTM! Command routing correctly placed.

The new command handler is properly placed alongside other read-only commands like "get_track_info", which correctly avoids the main-thread scheduling used for state-modifying commands.


488-527: Whitespace changes improve readability.

The blank line additions in _add_notes_to_clip make the function more readable without changing behavior.


528-571: Well-implemented with proper error handling and validation.

The _get_notes_from_clip function correctly:

  • Validates track and clip indices
  • Checks for clip existence
  • Uses get_notes_extended to retrieve notes with the correct pitch range (pitch_span=128 covers all valid MIDI pitches 0-127)
  • Converts notes to a serializable format
  • Returns structured metadata
README.md (1)

147-148: Excellent example commands.

These examples effectively demonstrate both simple retrieval and practical workflows. The transpose example shows how users can combine reading notes with other operations.

@y-uv
Copy link
Copy Markdown

y-uv commented Feb 14, 2026

nice

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants