Skip to content

Feature/device parameter control#41

Open
12Matt3r wants to merge 15 commits intoahujasid:mainfrom
12Matt3r:feature/device-parameter-control
Open

Feature/device parameter control#41
12Matt3r wants to merge 15 commits intoahujasid:mainfrom
12Matt3r:feature/device-parameter-control

Conversation

@12Matt3r
Copy link
Copy Markdown

@12Matt3r 12Matt3r commented Aug 16, 2025

PR Type

Enhancement


Description

  • Add comprehensive device parameter control with get/set by name or index

  • Implement full scene management (list, fire, create, rename)

  • Add automation writing capabilities for device parameters

  • Introduce Max for Live device modification utilities

  • Expand session control with locators, return tracks, and send levels


Diagram Walkthrough

flowchart LR
  A["Device Control"] --> B["Parameter Management"]
  A --> C["Device Deletion"]
  D["Scene Management"] --> E["Fire/Create/Rename"]
  F["Automation"] --> G["Write Parameter Curves"]
  H["M4L Utils"] --> I["Modify Device Defaults"]
  J["Session Control"] --> K["Locators & Sends"]
Loading

File Walkthrough

Relevant files
Enhancement
__init__.py
Core Ableton Live API integration expansion                           

AbletonMCP_Remote_Script/init.py

  • Add device parameter control methods (get_device_parameters,
    set_device_parameter, delete_device)
  • Implement scene management functions (list_scenes, fire_scene,
    create_scene, rename_scene)
  • Add automation writing capability (write_automation)
  • Include audio track creation and various session control features
  • Enhance browser tree functionality with recursive exploration
+521/-81
m4l_utils.py
Max for Live device modification utilities                             

MCP_Server/m4l_utils.py

  • Create utility for modifying Max for Live device files
  • Implement parameter default value modification in .amxd files
  • Handle gzip compression/decompression and JSON parsing
+68/-0   
server.py
MCP server tool expansion and integration                               

MCP_Server/server.py

  • Add MCP tools for device parameter control and management
  • Implement scene management tools (list, fire, create, rename)
  • Add automation writing and Max for Live device modification tools
  • Include session control tools for locators, return tracks, and sends
  • Enhance browser functionality with depth control
+407/-8 
Documentation
README.md
Documentation update for expanded features                             

README.md

  • Update documentation with comprehensive command list
  • Add examples for all new features and capabilities
  • Reorganize content to reflect expanded functionality
  • Include project history and enhanced feature descriptions
+112/-55

Summary by CodeRabbit

  • New Features

    • Added scene control: list, fire, create, rename.
    • Added locator and arrangement tools: list locators, create locator, set song position.
    • Added track control: create MIDI and audio tracks; list return tracks; set send level.
    • Expanded device and clip control: get device details/parameters, find by name, set parameter, delete device, get clip info, write automation.
    • Enhanced browser exploration with depth-limited tree.
    • Added user feedback message display and M4L device default modification.
  • Documentation

    • Rewrote README with a structured command list, new installation/setup guides, and project history.

I've introduced the ability to get and set parameters for your devices in Ableton Live.

New Features:
- Added `get_device_parameters` to list all controllable parameters for a device.
- Added `set_device_parameter` to modify the value of a specific parameter.

To implement this, I updated the following files:
- `AbletonMCP_Remote_Script/__init__.py`: Added new handlers to interact with the Live API.
- `MCP_Server/server.py`: Updated the server to handle the new commands.
- `README.md`: Documented the new capabilities.

This functionality works for all Ableton devices, including Max for Live devices that expose their parameters to the Live API.
I've enhanced the device control capabilities with this commit. Here's a summary of my changes:

1.  You can now use the `set_device_parameter` function to identify parameters by name, not just by index, making it easier to use.
2.  I've introduced a new `delete_device` command so you can remove a device from a track.

To implement this, I updated the Ableton Remote Script with logic to find parameters by name and to delete devices via the Live API. I also updated the MCP Server to expose the modified `set_device_parameter` function and the new `delete_device` command. Finally, I documented these new features and added examples to the `README.md`.
I've introduced the ability to create and manage audio tracks, which complements the existing MIDI track functionality. You can now create audio tracks in your Ableton Live session and load audio files into their clip slots.

I also updated the documentation to reflect these new capabilities, including examples for creating audio tracks and loading audio clips.
I've introduced a comprehensive set of features for managing scenes in Ableton Live, allowing you to have more structured song control.

New Features:
- `list_scenes`: Get a list of all scenes in the session.
- `fire_scene`: Fire a specific scene by its index.
- `create_scene`: Create a new scene at a specific index.
- `rename_scene`: Rename a scene.

I've also updated the `README.md` to document these new capabilities and provide examples for you.
This commit introduces a powerful new feature: the ability to write automation for device parameters within a clip.

New Features:
- A `write_automation` tool that can create automation curves for any device parameter. It takes a list of time/value points to create the automation.

Implementation Details:
- The Ableton Remote Script has been updated with a `_write_automation` command handler. This function handles finding the correct clip, device, and parameter, and then uses the Live API to write the automation points to the parameter's envelope.
- The MCP Server now exposes the `write_automation` tool, with a detailed docstring explaining how to use it.
- The `README.md` has been updated to document this new capability and provide an example.
This commit introduces a new tool, `get_device_details`, to provide detailed information about a specific device on a track.

This new feature allows for the retrieval of key properties of a device, such as its `class_name`, which can be used to identify whether a device is a Max for Live device (`MaxDevice`), a VST plugin, or a native Ableton device.

Implementation Details:
- The Ableton Remote Script has been updated with a `_get_device_details` command handler.
- The MCP Server now exposes the `get_device_details` tool.
- The `README.md` has been updated to document this new capability.
This commit introduces a new advanced feature: the ability to programmatically modify Max for Live device files (.amxd).

New Features:
- A `modify_m4l_device_default` tool that can take an input .amxd file, change the default value of a specified parameter, and save the result as a new .amxd file.

Implementation Details:
- A new `m4l_utils.py` file has been added to encapsulate the logic for reading, decompressing, parsing, modifying, and writing gzipped JSON .amxd files.
- The MCP Server now exposes the `modify_m4l_device_default` tool.
- The `README.md` has been updated to document this new advanced capability and provide a clear example.
This commit introduces a new helper tool, `find_device_by_name`, to improve the usability of device-related functions.

This tool allows finding the index of a device on a specific track by its name, which can then be used in other tools like `get_device_parameters`, `delete_device`, etc.

Implementation Details:
- The Ableton Remote Script has been updated with a `_find_device_by_name` command handler.
- The MCP Server now exposes the `find_device_by_name` tool.
- The `README.md` has been updated to document this new tool.
This commit introduces a new tool, `get_clip_info`, to get detailed information about a specific clip in the Ableton Live session.

This tool provides more detailed information than what was previously available, including the clip's color, looping status, start/end markers, and time signature.

Implementation Details:
- The Ableton Remote Script has been updated with a `_get_clip_info` command handler.
- The MCP Server now exposes the `get_clip_info` tool.
- The `README.md` has been updated to document this new tool.
This commit introduces a new tool, `show_message`, to display a custom message in Ableton Live's status bar.

This feature provides a simple way for the AI to give direct feedback to the user within the Ableton Live application.

Implementation Details:
- The Ableton Remote Script has been updated with a `_show_message` command handler that utilizes the built-in `show_message` method of the Control Surface.
- The MCP Server now exposes the `show_message` tool.
- The `README.md` has been updated to document this new tool.
This commit introduces a new set of tools for interacting with Ableton Live's Arrangement View.

New Features:
- `list_locators`: Get a list of all locators (cue points) in the arrangement.
- `create_locator`: Create a new locator at a specific time (in beats).
- `set_song_position`: Move the playhead to a specific time in the arrangement.

Implementation Details:
- The Ableton Remote Script has been updated with new command handlers for each of these actions.
- The MCP Server now exposes these commands as tools for the AI client.
- The `README.md` has been updated to document these new capabilities.
This commit introduces new tools for managing mixer sends in Ableton Live, allowing for more detailed mix control.

New Features:
- `list_return_tracks`: Get a list of all return tracks in the session.
- `set_send_level`: Set the send level for a specific track and send index.

Implementation Details:
- The Ableton Remote Script has been updated with new command handlers for these mixer-related actions.
- The MCP Server now exposes these commands as tools for the AI client.
- The `README.md` has been updated to document these new capabilities.
This commit introduces a wide range of new features to the AbletonMCP project, significantly expanding its capabilities.

- **Enhanced Browser Navigation**: The `get_browser_tree` tool is now recursive, allowing for deeper exploration of the Ableton Live browser.
- **Scene Management**: New tools have been added to list, fire, create, and rename scenes.
- **Advanced Device Control**: It is now possible to get and set device parameters by name, delete devices, and find devices by name.
- **Automation**: A new `write_automation` tool allows for the creation of automation curves for device parameters.
- **Max for Live Integration**: A new `modify_m4l_device_default` tool allows for the modification of `.amxd` files.
- **Arrangement View Control**: New tools have been added to list and create locators, and to set the song position.
- **Mixer Control**: New tools have been added to list return tracks and set send levels.
- **Audio Track Creation**: A new `create_audio_track` tool has been added.
- **User Feedback**: A new `show_message` tool allows for displaying messages in the Ableton Live status bar.
- **Documentation**: The `README.md` has been updated to reflect all of the new features.
This commit replaces the existing README.md file with a new, more comprehensive version. The new README.md includes:

- A full history of the project.
- Detailed installation instructions.
- A complete command list with explanations.
- A breakdown of all the features we have implemented.
This commit replaces the existing README.md file with a new, more comprehensive version. The new README.md includes:

- A full history of the project.
- Detailed installation instructions.
- A complete command list with explanations and examples.
- A breakdown of all the features we have implemented.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Aug 16, 2025

Walkthrough

Adds extensive Ableton Live command routing and thread-safe execution in the Remote Script, introduces many new MCP server endpoints for scenes/locators/tracks/devices/clips/browser/automation, adds a Max for Live utility to modify default parameter values in .amxd files, updates README with restructured command list and onboarding, and changes get_browser_tree signature.

Changes

Cohort / File(s) Summary
Remote Script: routing, thread safety, new commands
AbletonMCP_Remote_Script/__init__.py
Adds handlers for scenes, locators, devices, clips, returns, sends; implements device/parameter/clip/automation utilities; extends browser traversal with max_depth; dispatches all state-changing ops to main thread via response_queue and schedule_message; updates get_browser_tree(category_type, max_depth).
MCP Server: endpoints and API expansion
MCP_Server/server.py
Adds endpoints for scenes (list/fire/create/rename), locators (list/create/set position), tracks (create MIDI/audio), mixer (set_send_level, list_return_tracks), devices (list params/details/find/set/delete), clips (get_clip_info), automation (write_automation), browser (get_browser_tree with max_depth), messaging (show_message), M4L modification; structured JSON responses and error handling.
M4L utility: parameter default editor
MCP_Server/m4l_utils.py
Adds set_parameter_default_value to read .amxd (gzipped JSON), find parameter by varname/long name, update initial default, and write modified device; raises on missing file or parameter.
Docs: restructuring and onboarding
README.md
Replaces capabilities narrative with categorized command list; updates installation (uv, platform steps), adds IDE integration (Claude Desktop, Cursor), includes project history, disclaimer; removes Smithery block; revises troubleshooting context.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant MCP_Server
  participant AbletonConn
  participant RemoteScript
  participant LiveApp

  Client->>MCP_Server: HTTP/Tool call (e.g., create_scene, set_device_parameter)
  MCP_Server->>AbletonConn: get_ableton_connection()
  MCP_Server->>RemoteScript: send_command(cmd, params)
  alt State-changing command
    RemoteScript->>RemoteScript: schedule_message(main-thread)
    RemoteScript->>LiveApp: Perform operation
    LiveApp-->>RemoteScript: Result
    RemoteScript-->>MCP_Server: Result via response_queue
  else Read-only command
    RemoteScript->>LiveApp: Query
    LiveApp-->>RemoteScript: Data
    RemoteScript-->>MCP_Server: Data
  end
  MCP_Server-->>Client: JSON/string response
Loading
sequenceDiagram
  participant User
  participant MCP_Server
  participant M4LUtils

  User->>MCP_Server: modify_m4l_device_default(input, output, name, value)
  MCP_Server->>M4LUtils: set_parameter_default_value(...)
  M4LUtils->>M4LUtils: Read .amxd (gzip) -> JSON -> modify -> write (gzip)
  M4LUtils-->>MCP_Server: True / Error
  MCP_Server-->>User: Success message or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Poem

In tracks and scenes I twitch my ears,
New buttons bloom—no bugs or fears.
I hop through trees with depth set two,
Nudge a knob—automation too.
A zipped M4L gets sweeter tone,
With gentle paws I tweak its cone.
Carrot logs: all changes shown. 🥕🎛️

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@qodo-code-review
Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

API Robustness

Newly added parameter setters and automation writing accept raw values without clamping or quantization handling; ensure values are validated against parameter ranges and quantized items to avoid exceptions or inconsistent state.

def _set_device_parameter(self, track_index, device_index, value, parameter_index=None, parameter_name=None):
    """Set a parameter for a device by index or name"""
    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 device_index < 0 or device_index >= len(track.devices):
            raise IndexError("Device index out of range")

        device = track.devices[device_index]

        parameter_to_set = None

        if parameter_index is not None:
            if parameter_index < 0 or parameter_index >= len(device.parameters):
                raise IndexError("Parameter index out of range")
            parameter_to_set = device.parameters[parameter_index]
        elif parameter_name is not None:
            for param in device.parameters:
                if param.name.lower() == parameter_name.lower():
                    parameter_to_set = param
                    break
            if parameter_to_set is None:
                raise ValueError("Parameter with name '{0}' not found".format(parameter_name))
        else:
            raise ValueError("Either parameter_index or parameter_name must be provided")

        if not parameter_to_set.is_enabled:
            raise Exception("Parameter is not enabled")

        parameter_to_set.value = value

        return {
            "track_index": track_index,
            "device_index": device_index,
            "parameter_name": parameter_to_set.name,
            "new_value": parameter_to_set.value
        }
    except Exception as e:
        self.log_message("Error setting device parameter: " + str(e))
        raise
Scene Creation Index

_create_scene uses the provided index directly; verify Ableton's API behavior for negative or out-of-range indices and consider bounds checks or documented behavior to prevent IndexError or unintended insertion points.

def _create_scene(self, scene_index):
    """Create a new scene"""
    try:
        # Create the scene at the specified index
        self._song.create_scene(scene_index)

        # Get the new scene's index
        new_scene_index = len(self._song.scenes) - 1 if scene_index == -1 else scene_index

        return {
            "created": True,
            "scene_index": new_scene_index
        }
    except Exception as e:
        self.log_message("Error creating scene: " + str(e))
        raise
M4L JSON Assumptions

The AMXD modification assumes a specific JSON schema (boxes->box.initial[0]); add schema checks and support for alternate structures to avoid corrupting devices or silently failing on different M4L versions.

modified = False
if "patcher" in data and "boxes" in data["patcher"]:
    for item in data["patcher"]["boxes"]:
        box = item.get("box")
        if not box:
            continue

        # Check for parameter name in varname or parameter_longname
        varname = box.get("varname")
        longname = None
        if "saved_attribute_attributes" in box and "valueof" in box["saved_attribute_attributes"]:
            longname = box["saved_attribute_attributes"]["valueof"].get("parameter_longname")

        if varname == parameter_name or longname == parameter_name:
            # Found the parameter. Now find its initial value to modify.
            # Based on research, the key is likely 'initial'.
            if "initial" in box and isinstance(box["initial"], list) and len(box["initial"]) > 0:
                box["initial"][0] = new_default_value
                modified = True
                break # Stop after finding the first match

if not modified:
    raise ValueError(f"Parameter '{parameter_name}' not found or could not be modified.")

# Convert the modified data back to a JSON string
modified_json_data = json.dumps(data, indent=2)

# Compress and write the new file
with gzip.open(output_filepath, 'wb') as f:
    f.write(modified_json_data.encode('utf-8'))

@qodo-code-review
Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Main-thread safety and API contracts

Many new Live mutations (scene ops, device deletes/sets, automation writes,
locators, send levels) are executed via your main-thread queue, but several
getters (e.g., list_scenes, list_locators, device/clip detail) still touch the
Live API off-thread inside _process_command. Live's Python API is not
thread-safe; move all Live access (read and write) onto the main thread queue or
a task schedule to avoid intermittent crashes and undefined behavior. Also,
several methods assume attributes like parameter.is_enabled/min/max/value_items
exist uniformly; standardize capability checks and normalize response schemas to
prevent client/server contract drift across Live versions and plugin devices.

Examples:

AbletonMCP_Remote_Script/__init__.py [225-226]
elif command_type == "list_scenes":
    response["result"] = self._list_scenes()
AbletonMCP_Remote_Script/__init__.py [1048-1060]
for param_index, param in enumerate(device.parameters):
    if param.is_enabled:
        param_info = {
            "index": param_index,
            "name": param.name,
            "value": param.value,
            "min": param.min,
            "max": param.max,
            "is_quantized": param.is_quantized,
        }

 ... (clipped 3 lines)

Solution Walkthrough:

Before:

# In _process_command (runs off-main-thread)
if command_type == "list_scenes":
    response["result"] = self._list_scenes() # Direct API access
...

# In _get_device_parameters
for param in device.parameters:
    param_info = {
        "name": param.name,
        "value": param.value, # Assumes these attributes exist
        "min": param.min,
        "max": param.max
    }
    parameters.append(param_info)

After:

# In _process_command
# Add read-only commands to the main-thread list
elif command_type in ["list_scenes", "get_device_parameters", ...]:
    # Schedule for main thread execution
    ...

# In _get_device_parameters
for param in device.parameters:
    param_info = {
        "name": param.name,
        "value": param.value,
        "min": getattr(param, 'min', None), # Safely get attributes
        "max": getattr(param, 'max', None)
    }
    parameters.append(param_info)
Suggestion importance[1-10]: 9

__

Why: This suggestion correctly identifies a critical thread-safety violation by performing read operations on the Live API off the main thread, which can cause instability. It also points out a significant robustness issue in the API design due to unchecked assumptions about device parameter attributes.

High
Possible issue
Validate and sort automation points

Guard against cases where the envelope is None and validate that each point has
numeric time/value within the clip length. Also coerce times to float and ensure
they are sorted before setting automation to avoid Live errors.

AbletonMCP_Remote_Script/init.py [551-606]

 def _write_automation(self, track_index, clip_index, device_index, points, parameter_index=None, parameter_name=None):
     """Write automation points for a device parameter within a clip."""
     try:
-        ...
+        # 1. Get the clip
+        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 ValueError("No clip in the specified slot.")
+        clip = clip_slot.clip
+
+        # 2. Get the device
+        if device_index < 0 or device_index >= len(track.devices):
+            raise IndexError("Device index out of range")
+        device = track.devices[device_index]
+
+        # 3. Get the parameter
+        parameter = None
+        if parameter_index is not None:
+            if parameter_index < 0 or parameter_index >= len(device.parameters):
+                raise IndexError("Parameter index out of range")
+            parameter = device.parameters[parameter_index]
+        elif parameter_name is not None:
+            for p in device.parameters:
+                if p.name.lower() == parameter_name.lower():
+                    parameter = p
+                    break
+            if parameter is None:
+                raise ValueError("Parameter with name '{0}' not found".format(parameter_name))
+        else:
+            raise ValueError("Either parameter_index or parameter_name must be provided")
+
+        # 4. Get or create the automation envelope
         envelope = clip.get_automation_envelope(parameter)
+        if envelope is None:
+            raise RuntimeError("Automation envelope is not available for parameter '{0}'".format(parameter.name))
 
-        # 5. Set the automation points
+        # 5. Validate and set the automation points
         automation_points = []
+        clip_len = clip.loop_end - clip.loop_start if clip.looping else max(clip.end_marker - clip.start_marker, 0.0)
         for point in points:
-            automation_points.append((point.get("time"), point.get("value")))
+            t = float(point.get("time"))
+            v = float(point.get("value"))
+            if t is None or v is None:
+                raise ValueError("Each automation point must include 'time' and 'value'")
+            if clip_len > 0 and (t < 0 or t > clip_len + 1e-9):
+                # allow slight epsilon, but clamp to clip length
+                t = max(0.0, min(t, clip_len))
+            automation_points.append((t, v))
+
+        # Sort by time to avoid out-of-order insertion issues
+        automation_points.sort(key=lambda tv: tv[0])
 
         envelope.set_automation(tuple(automation_points))
 
         return {
             "wrote_automation": True,
             "point_count": len(automation_points),
             "parameter_name": parameter.name
         }
     except Exception as e:
         self.log_message("Error writing automation: " + str(e))
         raise

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: The suggestion adds crucial validation for the automation envelope and points, preventing potential runtime errors and ensuring data integrity before calling the Ableton API.

Medium
Decode and validate JSON input

Decode gzipped bytes explicitly as UTF-8 before json.loads to avoid type errors
on Python versions expecting str. Also validate the top-level JSON is a dict
before traversing.

MCP_Server/m4l_utils.py [24-28]

 with gzip.open(input_filepath, 'rb') as f:
-    json_data = f.read()
+    json_bytes = f.read()
 
 # Parse the JSON data
-data = json.loads(json_data)
+try:
+    json_text = json_bytes.decode('utf-8')
+except Exception:
+    # Fallback to latin-1 to avoid hard crash on odd encodings
+    json_text = json_bytes.decode('latin-1')
 
+data = json.loads(json_text)
+if not isinstance(data, dict):
+    raise ValueError("Unexpected .amxd structure: top-level JSON is not an object")
+
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly adds explicit UTF-8 decoding and validation, which improves the robustness of the file parsing logic against potential encoding issues and malformed files.

Medium
General
Verify scene fire result

Use the returned result to confirm success and handle failure cases instead of
always reporting success. If the response status indicates error or 'fired' is
False, surface that to the caller.

MCP_Server/server.py [300-314]

 @mcp.tool()
 def fire_scene(ctx: Context, scene_index: int) -> str:
     """
     Fire a scene in the Ableton session.
 
     Parameters:
     - scene_index: The index of the scene to fire.
     """
     try:
         ableton = get_ableton_connection()
         result = ableton.send_command("fire_scene", {"scene_index": scene_index})
-        return f"Fired scene {scene_index}."
+        if isinstance(result, dict) and result.get("fired"):
+            return f"Fired scene {result.get('scene_index', scene_index)}."
+        return f"Failed to fire scene {scene_index}: {json.dumps(result)}"
     except Exception as e:
         logger.error(f"Error firing scene: {str(e)}")
         return f"Error firing scene: {str(e)}"
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the function assumes success without checking the return value, improving the accuracy of the tool's feedback to the user.

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

🔭 Outside diff range comments (3)
AbletonMCP_Remote_Script/__init__.py (2)

884-886: Typo breaks 'instruments' path resolution in browser navigation

The string literal is missing the leading 'i' (“nstruments”), preventing correct resolution for “instruments/...”. This will cause valid paths to fall back or fail.

Apply this diff:

-                if path_parts[0].lower() == "nstruments":
+                if path_parts[0].lower() == "instruments":
                     current_item = app.browser.instruments

326-334: Undefined _load_instrument_or_effect: add alias or implementation

The branch for command_type == "load_instrument_or_effect" currently calls a non-existent method. Since the server and README expect this command, you should either implement _load_instrument_or_effect or alias it to the existing _load_browser_item.

Locations to update:

  • AbletonMCP_Remote_Script/init.py (in the same class as _load_browser_item)

Suggested patch—add this immediately after the _load_browser_item definition:

 class RemoteScript:
     def _load_browser_item(self, track_index, item_uri):
         """Load a browser item onto a track by its URI"""
         try:
             …
         except Exception as e:
             …

+    def _load_instrument_or_effect(self, track_index, uri):
+        """
+        Legacy alias for load_instrument_or_effect:
+        load an instrument, effect, or audio file from the browser.
+        """
+        return self._load_browser_item(track_index, uri)

This ensures the existing branch continues working without duplicating logic.

MCP_Server/server.py (1)

112-145: Serialize send/receive over the shared socket

Guard the entire request/response with the connection lock to prevent interleaving payloads.

Apply this diff:

-        try:
-            logger.info(f"Sending command: {command_type} with params: {params}")
-            
-            # Send the command
-            self.sock.sendall(json.dumps(command).encode('utf-8'))
-            logger.info(f"Command sent, waiting for response...")
-            
-            # For state-modifying commands, add a small delay to give Ableton time to process
-            if is_modifying_command:
-                import time
-                time.sleep(0.1)  # 100ms delay
-            
-            # Set timeout based on command type
-            timeout = 15.0 if is_modifying_command else 10.0
-            self.sock.settimeout(timeout)
-            
-            # Receive the response
-            response_data = self.receive_full_response(self.sock)
+        try:
+            logger.info(f"Sending command: {command_type} with params: {params}")
+            with self.lock:
+                # Send the command
+                self.sock.sendall(json.dumps(command).encode('utf-8'))
+                logger.info(f"Command sent, waiting for response...")
+                
+                # For state-modifying commands, add a small delay to give Ableton time to process
+                if is_modifying_command:
+                    import time
+                    time.sleep(0.1)  # 100ms delay
+                
+                # Set timeout based on command type (receive_full_response will set its own timeout too)
+                timeout = 15.0 if is_modifying_command else 10.0
+                self.sock.settimeout(timeout)
+                
+                # Receive the response
+                response_data = self.receive_full_response(self.sock)
🧹 Nitpick comments (11)
README.md (2)

52-52: Fix Markdown link syntax for uv website

The link uses reference-style brackets but doesn’t define a reference. Use standard inline link syntax.

Apply this diff:

-Otherwise, install from [uv's official website][https://docs.astral.sh/uv/getting-started/installation/]
+Otherwise, install from [uv's official website](https://docs.astral.sh/uv/getting-started/installation/)

108-116: Step numbering/order is jumbled in “Installing the Ableton Remote Script”

The steps jump from 2 → 4 → 3 → 4. Reorder to be strictly increasing to avoid confusion.

Apply this diff to renumber consistently:

-4. Create a folder called 'AbletonMCP' in the Remote Scripts directory and paste the downloaded '__init__.py' file
-
-3. Launch Ableton Live
-
-4. Go to Settings/Preferences → Link, Tempo & MIDI
+3. Create a folder called 'AbletonMCP' in the Remote Scripts directory and paste the downloaded '__init__.py' file
+
+4. Launch Ableton Live
+
+5. Go to Settings/Preferences → Link, Tempo & MIDI

And update the following two lines accordingly:

-5. In the Control Surface dropdown, select "AbletonMCP"
-6. Set Input and Output to "None"
+6. In the Control Surface dropdown, select "AbletonMCP"
+7. Set Input and Output to "None"
MCP_Server/m4l_utils.py (3)

41-43: Flatten nested dict checks when reading parameter_longname

Slight simplification improves readability and avoids a second key lookup.

Apply this diff:

-                longname = None
-                if "saved_attribute_attributes" in box and "valueof" in box["saved_attribute_attributes"]:
-                    longname = box["saved_attribute_attributes"]["valueof"].get("parameter_longname")
+                saa = box.get("saved_attribute_attributes", {})
+                valueof = saa.get("valueof", {})
+                longname = valueof.get("parameter_longname")

19-21: Validate input types early (e.g., new_default_value must be numeric)

Guardrails here make error messages clearer than failing later during JSON writes.

Apply this diff:

     if not os.path.exists(input_filepath):
         raise FileNotFoundError(f"Input file not found: {input_filepath}")
 
+    if not isinstance(new_default_value, (int, float)):
+        raise TypeError(f"new_default_value must be numeric, got {type(new_default_value).__name__}")

66-68: Preserve original tracebacks when re-raising

Use exception chaining to avoid losing the original stack and cause.

Apply this diff:

-    except Exception as e:
-        # Re-raise exceptions with a more informative message
-        raise type(e)(f"Failed to modify M4L device: {e}")
+    except Exception as e:
+        # Re-raise with context for easier debugging
+        raise RuntimeError(f"Failed to modify M4L device: {e}") from e
AbletonMCP_Remote_Script/__init__.py (2)

593-598: Validate and normalize automation points before writing

Currently, points are passed through without validation. Non-numeric values or missing keys will cause runtime errors deeper in Live’s API. Recommend validation plus optional sorting by time.

Apply this diff:

-            automation_points = []
-            for point in points:
-                automation_points.append((point.get("time"), point.get("value")))
+            automation_points = []
+            for idx, point in enumerate(points):
+                t = point.get("time")
+                v = point.get("value")
+                if t is None or v is None:
+                    raise ValueError("Each automation point must include 'time' and 'value'")
+                if not isinstance(t, (int, float)) or not isinstance(v, (int, float)):
+                    raise TypeError("Automation point 'time' and 'value' must be numeric")
+                automation_points.append((float(t), float(v)))
+            # Optional: ensure chronological order
+            automation_points.sort(key=lambda tv: tv[0])

1309-1376: Consider enriching get_browser_tree result with totals and path for better UX

The server formatter looks for 'total_folders', 'path', and 'has_more' (with defaults). Adding a folder count and optional path strings would improve the output fidelity.

If desired, compute and include a total count:

             result = {
                 "type": category_type,
                 "categories": [],
-                "available_categories": browser_attrs
+                "available_categories": browser_attrs,
+                "total_folders": 0
             }
@@
-            def process_item_recursive(item, current_depth=0):
+            def process_item_recursive(item, current_depth=0, current_path=None):
                 if not item or current_depth >= max_depth:
                     return None
                 
                 item_info = {
                     "name": item.name if hasattr(item, 'name') else "Unknown",
                     "is_folder": hasattr(item, 'children') and bool(item.children),
                     "is_device": hasattr(item, 'is_device') and item.is_device,
                     "is_loadable": hasattr(item, 'is_loadable') and item.is_loadable,
                     "uri": item.uri if hasattr(item, 'uri') else None,
-                    "children": []
+                    "children": [],
+                    "path": (current_path + "/" + item.name) if current_path and hasattr(item, 'name') else (item.name if hasattr(item, 'name') else None)
                 }
@@
-                    for child in item.children:
-                        child_info = process_item_recursive(child, current_depth + 1)
+                    for child in item.children:
+                        child_info = process_item_recursive(child, current_depth + 1, item_info["path"])
                         if child_info:
                             item_info["children"].append(child_info)
                 
                 return item_info
@@
-                        category_tree = process_item_recursive(category_root)
+                        category_tree = process_item_recursive(category_root, 0, category_name)
                         if category_tree:
                             result["categories"].append(category_tree)
+                            # Update total_folders heuristic
+                            def count_folders(node):
+                                n = 1
+                                for c in node.get("children", []):
+                                    n += count_folders(c)
+                                return n
+                            result["total_folders"] += count_folders(category_tree)
MCP_Server/server.py (4)

105-111: Update modifying-commands list to reflect new endpoints (or remove the special-casing)

This list controls only small sleeps, but keeping it current helps avoid flakiness on heavier ops like scene/device changes.

Apply this diff to include new modifiers:

         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"
+            "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",
+            "fire_scene", "create_scene", "rename_scene", "write_automation",
+            "show_message", "create_locator", "set_song_position", "set_send_level",
+            "delete_device", "load_browser_item"
         ]

Alternatively, consider removing the special-casing entirely and relying on the Remote Script’s main-thread scheduling.


309-314: Avoid assigning unused variables in fire_scene

result isn’t used. Either remove the assignment or use the returned info to confirm success.

Apply this diff:

-        result = ableton.send_command("fire_scene", {"scene_index": scene_index})
-        return f"Fired scene {scene_index}."
+        ableton.send_command("fire_scene", {"scene_index": scene_index})
+        return f"Fired scene {scene_index}."

648-656: Align load_instrument_or_effect message with Remote Script response shape

The Remote Script returns keys: loaded, item_name, track_name, uri. It does not return new_devices or devices_after. Simplify messaging to avoid empty joins.

Apply this diff:

-        # Check if the instrument was loaded successfully
-        if result.get("loaded", False):
-            new_devices = result.get("new_devices", [])
-            if new_devices:
-                return f"Loaded instrument with URI '{uri}' on track {track_index}. New devices: {', '.join(new_devices)}"
-            else:
-                devices = result.get("devices_after", [])
-                return f"Loaded instrument with URI '{uri}' on track {track_index}. Devices on track: {', '.join(devices)}"
+        # Check if the item was loaded successfully
+        if result.get("loaded", False):
+            item_name = result.get("item_name", uri)
+            return f"Loaded '{item_name}' (URI '{uri}') on track {track_index}."

197-216: Connection liveness check via sendall(b'') is unreliable

A zero-length send is often a no-op and won’t detect a dead peer. Consider a lightweight ping (e.g., get_session_info) or a socket recv with MSG_PEEK, though the latter isn’t cross-platform-friendly in Python.

I can draft a safe “ping” helper that issues a short, non-modifying command with its own timeout if you’d like.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

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

📒 Files selected for processing (4)
  • AbletonMCP_Remote_Script/__init__.py (10 hunks)
  • MCP_Server/m4l_utils.py (1 hunks)
  • MCP_Server/server.py (7 hunks)
  • README.md (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
AbletonMCP_Remote_Script/__init__.py (1)
MCP_Server/server.py (5)
  • get_browser_tree (851-913)
  • create_scene (317-331)
  • create_audio_track (491-504)
  • delete_device (830-848)
  • show_message (1039-1052)
MCP_Server/server.py (2)
MCP_Server/m4l_utils.py (1)
  • set_parameter_default_value (5-68)
AbletonMCP_Remote_Script/__init__.py (1)
  • get_browser_tree (1310-1377)
🪛 Ruff (0.12.2)
MCP_Server/m4l_utils.py

44-47: Use a single if statement instead of nested if statements

(SIM102)


68-68: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

MCP_Server/server.py

310-310: Local variable result is assigned to but never used

Remove assignment to unused variable result

(F841)

🪛 LanguageTool
README.md

[grammar] ~18-~18: There might be a mistake here.
Context: ...elete devices, and find devices by name. - Automation: Write automation curves fo...

(QB_NEW_EN)


[grammar] ~32-~32: There might be a mistake here.
Context: ...ed device control. - Automation writing. - Max for Live integration. - Arrangement ...

(QB_NEW_EN)


[grammar] ~133-~133: There might be a mistake here.
Context: ...ation about the current Ableton session. - Example: "Get the session info." - `se...

(QB_NEW_EN)


[grammar] ~137-~137: There might be a mistake here.
Context: ...k(): Start playing the Ableton session. - **Example**: "Start playback." - stop_pla...

(QB_NEW_EN)


[grammar] ~139-~139: There might be a mistake here.
Context: ...ck()`: Stop playing the Ableton session. - Example: "Stop playback." ### Track C...

(QB_NEW_EN)


[grammar] ~143-~143: There might be a mistake here.
Context: ...iled information about a specific track. - Example: "Get info for track 1." - `cr...

(QB_NEW_EN)


[grammar] ~145-~145: There might be a mistake here.
Context: ...ex: int = -1)`: Create a new MIDI track. - Example: "Create a new MIDI track." - ...

(QB_NEW_EN)


[grammar] ~147-~147: There might be a mistake here.
Context: ...x: int = -1)`: Create a new audio track. - Example: "Create a new audio track." -...

(QB_NEW_EN)


[grammar] ~153-~153: There might be a mistake here.
Context: ...: float = 4.0)`: Create a new MIDI clip. - Example: "Create a 4-bar clip in track...

(QB_NEW_EN)


[grammar] ~161-~161: There might be a mistake here.
Context: ...clip_index: int)`: Start playing a clip. - Example: "Play the clip in track 1, sl...

(QB_NEW_EN)


[grammar] ~163-~163: There might be a mistake here.
Context: ... clip_index: int)`: Stop playing a clip. - Example: "Stop the clip in track 1, sl...

(QB_NEW_EN)


[grammar] ~167-~167: There might be a mistake here.
Context: ...udio file from the browser onto a track. - Example: "Load the 'Operator' synth on...

(QB_NEW_EN)


[grammar] ~183-~183: There might be a mistake here.
Context: ...st of all scenes in the Ableton session. - Example: "List all scenes." - `fire_sc...

(QB_NEW_EN)


[grammar] ~187-~187: There might be a mistake here.
Context: ...eate a new scene in the Ableton session. - Example: "Create a new scene." - `rena...

(QB_NEW_EN)


[grammar] ~193-~193: There might be a mistake here.
Context: ...tree of browser categories from Ableton. - Example: "Get the browser tree for ins...

(QB_NEW_EN)


[grammar] ~197-~197: There might be a mistake here.
Context: ...d then load a specific drum kit into it. - Example: "Load the '808 Core Kit' on t...

(QB_NEW_EN)


[grammar] ~201-~201: There might be a mistake here.
Context: ... modified default value for a parameter. - Example: "Take the device 'MyReverb.am...

(QB_NEW_EN)


[grammar] ~205-~205: There might be a mistake here.
Context: ...ors (cue points) in the Ableton session. - Example: "List all locators." - `creat...

(QB_NEW_EN)


[grammar] ~207-~207: There might be a mistake here.
Context: ...) at a specific time in the arrangement. - Example: "Create a locator at beat 32....

(QB_NEW_EN)


[grammar] ~213-~213: There might be a mistake here.
Context: ...ll return tracks in the Ableton session. - Example: "List all return tracks." - `...

(QB_NEW_EN)


[grammar] ~219-~219: There might be a mistake here.
Context: ...splay a message in Ableton's status bar. - Example: "Show the message 'Hello from...

(QB_NEW_EN)

🔇 Additional comments (2)
README.md (2)

17-24: Command list expansion reads well and matches new API surface

The new “Features” bullets align with the new MCP endpoints and Remote Script routing. Clear and consistent phrasing.


130-221: Command List structure is a big improvement

The categorized, signature-style listing with examples is easy to scan and maps to the new MCP endpoints.

Comment on lines 6 to +9
from dataclasses import dataclass
from contextlib import asynccontextmanager
from typing import AsyncIterator, Dict, Any, List, Union
from .m4l_utils import set_parameter_default_value
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make AbletonConnection thread-safe

Multiple tools can be invoked concurrently; without a lock, writes/reads on a shared socket can interleave and corrupt JSON framing. Add a connection-level lock.

Apply this diff:

-from dataclasses import dataclass
+from dataclasses import dataclass, field
+from threading import Lock

And extend the dataclass:

 class AbletonConnection:
@@
-    sock: socket.socket = None
+    sock: socket.socket = None
+    lock: Lock = field(default_factory=Lock)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from dataclasses import dataclass
from contextlib import asynccontextmanager
from typing import AsyncIterator, Dict, Any, List, Union
from .m4l_utils import set_parameter_default_value
# Imports at top of MCP_Server/server.py
from dataclasses import dataclass, field
from threading import Lock
from contextlib import asynccontextmanager
from typing import AsyncIterator, Dict, Any, List, Union
from .m4l_utils import set_parameter_default_value
@dataclass
class AbletonConnection:
sock: socket.socket = None
lock: Lock = field(default_factory=Lock)
# ... rest of class ...
🤖 Prompt for AI Agents
In MCP_Server/server.py around lines 6 to 9, the AbletonConnection must be made
thread-safe by adding a connection-level asyncio lock and using it to serialize
socket reads/writes; import asyncio and dataclasses.field, add a field like
connection_lock: asyncio.Lock = field(default_factory=asyncio.Lock, init=False,
repr=False) to the dataclass, and then wrap all socket send/receive or JSON
framing code paths with "async with self.connection_lock:" so writes/reads
cannot interleave and corrupt framing.

Copy link
Copy Markdown
Author

@12Matt3r 12Matt3r left a comment

Choose a reason for hiding this comment

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

LOOKS GOOD?

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.

1 participant