Skip to content

Add Return Track and Mixing Controls to Ableton MCP #26

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

itsuzef
Copy link

@itsuzef itsuzef commented May 1, 2025

User description

Overview

This PR adds essential mixing capabilities to the Ableton MCP system, focusing on return tracks and volume controls. These features enable more comprehensive mixing workflows directly through the MCP API.

Features Added

1. Return Track Creation

  • Implemented _create_return_track function in the AbletonMCP_Remote_Script
  • Exposed the functionality through the MCP Server API as create_return_track
  • Enhanced the _get_track_by_index function to handle both regular tracks and return tracks
  • Created a test script (return_track_test.py) to verify the functionality

2. Send Controls

  • Implemented _set_send_level function to control send levels from tracks to return tracks
  • Exposed the functionality through the MCP Server API as set_send_level
  • Created a test script (send_control_test.py) to verify the functionality
  • This completes the return track workflow by allowing tracks to send audio to return tracks

3. Volume Controls

  • Implemented _set_track_volume function to control track volume levels
  • Added _linear_to_db helper function to convert linear volume values (0.0-1.0) to dB
  • Exposed the functionality through the MCP Server API as set_track_volume
  • Created a test script (volume_control_test.py) to verify the functionality
  • Provides intelligent volume control with proper dB conversion:
    • 0.0 → -∞ dB (muted)
    • 0.85 → 0 dB (unity gain)
    • 1.0 → +6 dB (maximum)

Testing

All features have been thoroughly tested with dedicated test scripts that demonstrate the functionality and verify correct behavior. You can find tests folder with all the tests.


PR Type

Enhancement, Tests, Documentation


Description

  • Added comprehensive support for return tracks, send level, and track volume controls in Ableton MCP, including dB conversion and improved device/EQ parameter handling.

  • Exposed new mixing and device control features through the MCP Server API, with enhanced command routing and error handling.

  • Introduced detailed test scripts for return track creation/manipulation, send level control, volume control, precise EQ Eight parameter control, audio effects device parameter control, and browser/device management.

  • Added extensive documentation covering device parameter control, existing system capabilities, and an implementation/testing summary.

  • Improved error handling, logging, and documentation throughout the codebase.


Changes walkthrough 📝

Relevant files
Enhancement
2 files
__init__.py
Add return track, send, and volume controls; device/EQ parameter
enhancements

AbletonMCP_Remote_Script/init.py

  • Added support for return tracks, including creation and manipulation
    via _create_return_track and _get_track_by_index.
  • Introduced send level and track volume controls with _set_send_level
    and _set_track_volume, including dB conversion.
  • Enhanced device parameter handling: get/set device parameters, EQ
    band/global controls, and EQ preset application.
  • Refactored track and clip operations to support both regular and
    return tracks.
  • Improved error handling and logging for new features.
  • +775/-55
    server.py
    Expose return track, send, volume, and device/EQ controls in server
    API

    MCP_Server/server.py

  • Added API endpoints/tools for return track creation, send level, and
    track volume control.
  • Exposed device parameter get/set, EQ band/global, and EQ preset
    operations via new tools.
  • Updated command routing and modifying command detection for new
    features.
  • Improved documentation and error handling for new API tools.
  • +495/-5 
    Tests
    6 files
    precise_eq_test.py
    Add precise EQ Eight parameter control test script             

    tests/precise_eq_test.py

  • Added a comprehensive test script for precise EQ Eight parameter
    control.
  • Tests frequency, gain, Q, filter types, and scale parameter for EQ
    bands.
  • Includes conversion helpers and detailed result validation.
  • +516/-0 
    audio_effects_test.py
    Add audio effects device parameter control test script     

    tests/audio_effects_test.py

  • Introduced a test script for generic audio effect device parameter
    control.
  • Tests parameter get/set for Compressor, Reverb, Auto Filter, and Delay
    devices.
  • Validates both quantized and continuous parameters.
  • +338/-0 
    browser_device_test.py
    Add browser and device management test script                       

    tests/browser_device_test.py

  • Added a test script for browser and device management features.
  • Tests browser tree/item retrieval and device/drum kit loading.
  • Validates integration of browser API and device loading.
  • +283/-0 
    return_track_test.py
    Add return track creation and manipulation test script     

    tests/return_track_test.py

  • Added a test script for return track creation and manipulation.
  • Tests creating return tracks, loading effects, and renaming.
  • Verifies correct indexing and session info updates.
  • +239/-0 
    send_control_test.py
    Add send level control test script for tracks and returns

    tests/send_control_test.py

  • Added a test script for send level control from tracks to return
    tracks.
  • Tests creating tracks/return tracks, loading effects, and setting send
    levels.
  • Validates send level changes at multiple values.
  • +231/-0 
    volume_control_test.py
    Add test script for track volume control via MCP API         

    tests/volume_control_test.py

  • Introduced a new test script to demonstrate and verify track volume
    control via the Ableton MCP API.
  • The script creates a MIDI track, sets its name, and tests various
    volume levels, displaying both linear and dB values.
  • Handles connection, command sending, and response parsing with error
    handling.
  • Provides clear output for each tested volume setting, including dB
    conversion.
  • +150/-0 
    Documentation
    3 files
    device_parameter_control.md
    Add detailed documentation for device parameter control in Ableton MCP

    docs/device_parameter_control.md

  • Added a comprehensive documentation file detailing device parameter
    control in Ableton MCP.
  • Describes core and specialized functions for device and EQ Eight
    parameter manipulation.
  • Explains parameter naming conventions, value normalization, and usage
    examples.
  • Outlines testing strategies and possible future enhancements.
  • +259/-0 
    existing_capabilities.md
    Document existing features and architecture of Ableton MCP

    docs/existing_capabilities.md

  • Introduced a new documentation file summarizing the existing
    capabilities of Ableton MCP.
  • Lists supported commands for session, track, clip, browser, and device
    management.
  • Explains the communication protocol and implementation details for
    both Remote Script and MCP Server.
  • Identifies missing features and suggests next steps for system
    enhancement.
  • +143/-0 
    implementation_summary.md
    Add implementation and testing summary for device parameter control

    docs/implementation_summary.md

  • Added a summary document describing the implementation and testing
    process for device parameter control.
  • Details the development of core, EQ Eight, return track, and volume
    control features.
  • Summarizes the testing process, challenges encountered, and solutions
    applied.
  • Outlines results and suggests future work for further improvements.
  • +161/-0 

    Need help?
  • Type /help how to ... in the comments thread for any questions about Qodo Merge usage.
  • Check out the documentation for more information.
  • Summary by CodeRabbit

    • New Features

      • Added advanced device parameter control, including retrieving and setting parameters for any device and specialized controls for EQ Eight (band/global parameters, presets).
      • Enabled creation and management of return tracks, including setting send levels between tracks and return tracks.
      • Introduced track volume control with decibel conversion and improved track information retrieval.
      • Expanded MCP server commands and API to support all new device, EQ, and mixer controls.
      • Added a new command-line interface (CLI) for managing the MCP server, displaying info, version, and installing the Remote Script.
      • Updated server startup to use the CLI command with configurable host and port.
    • Documentation

      • Added comprehensive documentation covering device parameter control, existing capabilities, implementation details, and usage examples.
    • Tests

      • Introduced extensive automated tests for device parameter control, EQ Eight precision, browser/device management, return track handling, send level control, and track volume control.
    • Chores

      • Updated configuration to ignore documentation files in version control.
      • Modified Dockerfile to start the MCP server via the new CLI command.
      • Updated license copyright to include an additional author.
      • Enhanced project metadata and updated entry points in configuration files.
      • Added a shell script to activate a Python environment and start the server locally.

    Copy link

    coderabbitai bot commented May 1, 2025

    Walkthrough

    This update introduces extensive enhancements to the Ableton MCP system, focusing on advanced device parameter control, especially for audio effects and the EQ Eight device. The AbletonMCP Remote Script is expanded with unified track handling, new commands for creating return tracks, setting send levels, adjusting track volumes, and comprehensive device parameter manipulation. The MCP Server is updated to expose these capabilities via new API tools. Several detailed documentation files are added, outlining architecture, implementation, and usage. A suite of new test scripts is introduced to verify device control, EQ precision, send and volume control, browser/device management, and return track operations. The .gitignore is updated to exclude the docs directory.

    Changes

    File(s) Change Summary
    .gitignore Added docs directory to ignored files.
    AbletonMCP_Remote_Script/__init__.py Major expansion: unified track retrieval, new methods for return track creation, send level/volume/device parameter/EQ control, improved error handling, and refined device type logic.
    MCP_Server/server.py Added API tools for device parameter control, EQ Eight manipulation, return track creation, send/volume control; extended command handling logic and recognized commands list.
    docs/device_parameter_control.md
    docs/existing_capabilities.md
    docs/implementation_summary.md
    Added comprehensive documentation on device parameter control, system capabilities, and implementation/testing summary.
    tests/audio_effects_test.py
    tests/browser_device_test.py
    tests/precise_eq_test.py
    tests/return_track_test.py
    tests/send_control_test.py
    tests/volume_control_test.py
    Added new test scripts for audio effects, browser/device management, EQ Eight precision, return track, send control, and volume control functionalities.
    MCP_Server/cli.py Added new CLI module with commands for server start, info display, version, and remote script installation.
    Dockerfile Changed default container command to use new CLI ableton-mcp server command.
    README.md Completely rewritten and reorganized for clearer user-focused installation, configuration, usage, and troubleshooting.
    pyproject.toml Updated author info, entry point changed to CLI module, updated project URLs.
    smithery.yaml Added project metadata and changed MCP server startup command to use CLI module with server argument.
    start_local_server.sh Added shell script to activate Python venv and run MCP server module.
    LICENSE Added an additional copyright holder to the MIT License.

    Sequence Diagram(s)

    sequenceDiagram
        participant Client
        participant MCP_Server
        participant AbletonMCP_Remote_Script
        participant AbletonLive
    
        Client->>MCP_Server: send_command("set_device_parameter", params)
        MCP_Server->>AbletonMCP_Remote_Script: set_device_parameter (track_index, device_index, parameter_name/index, value)
        AbletonMCP_Remote_Script->>AbletonLive: Set parameter value on device
        AbletonLive-->>AbletonMCP_Remote_Script: Parameter set result
        AbletonMCP_Remote_Script-->>MCP_Server: Success/Error response
        MCP_Server-->>Client: JSON response
    
        Client->>MCP_Server: send_command("set_eq_band", params)
        MCP_Server->>AbletonMCP_Remote_Script: set_eq_band (track_index, device_index, band_index, freq/gain/q/type)
        AbletonMCP_Remote_Script->>AbletonLive: Set EQ band parameters
        AbletonLive-->>AbletonMCP_Remote_Script: Band parameters set
        AbletonMCP_Remote_Script-->>MCP_Server: Success/Error response
        MCP_Server-->>Client: JSON response
    
        Client->>MCP_Server: send_command("create_return_track")
        MCP_Server->>AbletonMCP_Remote_Script: create_return_track
        AbletonMCP_Remote_Script->>AbletonLive: Create new return track
        AbletonLive-->>AbletonMCP_Remote_Script: Return track created
        AbletonMCP_Remote_Script-->>MCP_Server: Return track info
        MCP_Server-->>Client: JSON response
    
    Loading

    Poem

    🐇
    A hop, a skip, a leap in code,
    Device control now in full mode!
    EQ bands bend, sends now send,
    Return tracks bloom—no need to pretend.
    With docs and tests, we’re feeling grand,
    The MCP rabbit hops across the land!
    🥕


    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.
      • Generate unit testing code for this file.
      • 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. Examples:
      • @coderabbitai generate unit testing code for this file.
      • @coderabbitai modularize this function.
    • 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 src/utils.ts and generate unit testing code.
      • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
      • @coderabbitai help me debug CodeRabbit configuration file.

    Support

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

    Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

    CodeRabbit Commands (Invoked using PR comments)

    • @coderabbitai pause to pause the reviews on a PR.
    • @coderabbitai resume to resume the paused reviews.
    • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
    • @coderabbitai full review to do a full review from scratch and review all the files again.
    • @coderabbitai summary to regenerate the summary of the PR.
    • @coderabbitai generate docstrings to generate docstrings for this PR.
    • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
    • @coderabbitai resolve resolve all the CodeRabbit review comments.
    • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
    • @coderabbitai help to get help.

    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

    Documentation and Community

    • 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.

    Copy link

    qodo-merge-pro bot commented May 1, 2025

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

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

    Duplicate Code

    The _get_device_type function is defined twice in the file, once at line 874 and again at line 1446. Both implementations are identical and should be consolidated to avoid maintenance issues.

    def _get_device_type(self, device):
        """Get the type of a device"""
        if device.class_name == "PluginDevice":
            return "plugin"
        elif device.class_name == "InstrumentGroupDevice":
            return "instrument_rack"
        elif device.class_name == "DrumGroupDevice":
            return "drum_rack"
        elif device.can_have_drum_pads:
            return "drum_device"
        elif device.can_have_chains:
            return "rack"
        else:
            return "device"
    Error Handling

    The _linear_to_db function doesn't handle edge cases properly. When value is exactly 0, it returns -infinity, but for very small values close to 0, it will attempt to calculate log10 which could lead to extreme negative values or potential numerical issues.

    def _linear_to_db(self, value):
        """Convert a linear volume value (0.0 to 1.0) to dB
    
        Args:
            value: Linear volume value (0.0 to 1.0)
    
        Returns:
            Volume in dB
        """
        if value <= 0:
            return float('-inf')  # -infinity dB for zero volume
    
        # Ableton's volume mapping is approximately:
        # 0.85 -> 0dB
        # 0.0 -> -inf dB
        # 1.0 -> +6dB
    
        if value < 0.85:
            # Below 0dB
            return 20 * math.log10(value / 0.85)
        else:
            # Above 0dB (0.85 to 1.0 maps to 0dB to +6dB)
            return 6 * (value - 0.85) / 0.15
    Inconsistent Command Handling

    The is_modifying_command list in send_command method includes "create_return_track" but not "set_send_level" or "set_track_volume", which are also modifying commands. This inconsistency could lead to unexpected behavior.

    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", "start_playback", "stop_playback",
        "load_browser_item", "load_drum_kit", "set_device_parameter",
        "set_eq_band", "set_eq_global", "apply_eq_preset", "create_return_track"
    ]

    Copy link

    qodo-merge-pro bot commented May 1, 2025

    PR Code Suggestions ✨

    Explore these optional code suggestions:

    CategorySuggestion                                                                                                                                    Impact
    Possible issue
    Fix volume to dB conversion
    Suggestion Impact:The suggestion pointed out a mathematical issue with the dB conversion for values approaching zero. The commit addressed this by adding an epsilon value (1e-7) to prevent extreme negative values when the volume is very close to zero, which was the core issue identified in the suggestion.

    code diff:

    -        if value <= 0:
    -            return float('-inf')  # -infinity dB for zero volume
    +        # Add a small epsilon to prevent extreme negative values
    +        # when value is very close to zero but not exactly zero
    +        epsilon = 1e-7
    +        
    +        if value <= epsilon:
    +            return float('-inf')  # -infinity dB for zero or near-zero volume

    The current implementation has a mathematical issue when calculating dB values
    below 0dB. The formula 20 * math.log10(value / 0.85) will produce incorrect
    results because it doesn't match Ableton's actual mapping. For values
    approaching zero, this will yield extremely negative values rather than smoothly
    approaching -infinity.

    AbletonMCP_Remote_Script/init.py [1760-1782]

     def _linear_to_db(self, value):
         """Convert a linear volume value (0.0 to 1.0) to dB
         
         Args:
             value: Linear volume value (0.0 to 1.0)
         
         Returns:
             Volume in dB
         """
         if value <= 0:
             return float('-inf')  # -infinity dB for zero volume
         
         # Ableton's volume mapping is approximately:
         # 0.85 -> 0dB
         # 0.0 -> -inf dB
         # 1.0 -> +6dB
         
         if value < 0.85:
    -        # Below 0dB
    +        # Below 0dB - using a better approximation of Ableton's curve
             return 20 * math.log10(value / 0.85)
         else:
             # Above 0dB (0.85 to 1.0 maps to 0dB to +6dB)
             return 6 * (value - 0.85) / 0.15

    [Suggestion has been applied]

    Suggestion importance[1-10]: 4

    __

    Why: The suggestion correctly points out that the formula used for converting linear volume to dB below 0dB is an approximation and might be inaccurate, especially near zero. However, the improved_code is identical to the existing_code and does not provide the suggested "better approximation". While the observation about the approximation is valid, the suggestion is incomplete as it doesn't offer a concrete fix.

    Low
    General
    Correct unity gain value

    The volume level for unity gain (0dB) is incorrectly set to 0.85. In Ableton
    Live, unity gain is typically represented by a value of 0.85 or 0.86 depending
    on the version, but it's safer to use a more precise value of 0.8409 which
    corresponds exactly to 0dB.

    tests/volume_control_test.py [105-111]

     # Test volume levels
     volume_levels = [
         {"value": 0.0, "description": "Muted (0%)"},
         {"value": 0.425, "description": "Half volume (-6dB)"},
    -    {"value": 0.85, "description": "Unity gain (0dB)"},
    +    {"value": 0.8409, "description": "Unity gain (0dB)"},
         {"value": 1.0, "description": "Maximum (+6dB)"}
     ]
    • Apply this suggestion
    Suggestion importance[1-10]: 4

    __

    Why: The suggestion correctly identifies that 0.8409 is generally considered a more precise linear value for 0dB (unity gain) in Ableton Live compared to 0.85. Using this value improves the accuracy of the test case description and potentially provides a slightly more precise test point, although the impact is minor as the test doesn't assert the resulting dB value.

    Low
    • Update

    Copy link

    @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: 4

    🔭 Outside diff range comments (1)
    MCP_Server/server.py (1)

    103-110: ⚠️ Potential issue

    Include all state-mutating commands in is_modifying_command

    set_send_level and set_track_volume alter Live’s state yet are missing from the list.
    If omitted, large automation bursts might overrun the 10 s timeout originally intended for read-only queries.

                 "load_browser_item", "load_drum_kit", "set_device_parameter",
    -            "set_eq_band", "set_eq_global", "apply_eq_preset", "create_return_track"
    +            "set_eq_band", "set_eq_global", "apply_eq_preset", "create_return_track",
    +            "set_send_level", "set_track_volume"
    🧹 Nitpick comments (18)
    tests/volume_control_test.py (1)

    1-13: Unused import on line 12: sys

    The sys module is imported but not used in this file. Consider removing it for cleaner code.

    -import socket
    -import json
    -import time
    -import sys
    +import socket
    +import json
    +import time
    🧰 Tools
    🪛 Ruff (0.8.2)

    12-12: sys imported but unused

    Remove unused import: sys

    (F401)

    tests/send_control_test.py (2)

    7-11: Unused import on line 9: time

    The time module is imported but not used in this file. Consider removing it for cleaner code.

    -import socket
    -import json
    -import time
    -import sys
    +import socket
    -import json
    +import sys
    🧰 Tools
    🪛 Ruff (0.8.2)

    9-9: time imported but unused

    Remove unused import: time

    (F401)


    113-140: Unused variable on line 118: return_track_count

    The variable return_track_count is assigned but never used in this function.

        track_count = track_info["track_count"]
    -   return_track_count = track_info["return_track_count"]
        
        # Calculate the track index for the return track
        # The index for return tracks is track_count + return_track_index
        return_track_index = track_info["return_track_index"]
        track_index = track_count + return_track_index
    🧰 Tools
    🪛 Ruff (0.8.2)

    118-118: Local variable return_track_count is assigned to but never used

    Remove assignment to unused variable return_track_count

    (F841)

    tests/return_track_test.py (2)

    7-11: Unused import on line 9: time

    The time module is imported but not used in this file. Consider removing it for cleaner code.

    -import socket
    -import json
    -import time
    -import sys
    +import socket
    +import json
    +import sys
    🧰 Tools
    🪛 Ruff (0.8.2)

    9-9: time imported but unused

    Remove unused import: time

    (F401)


    186-186: Unnecessary f-string on line 186.

    The string has no interpolated values, so the f prefix is not needed.

    -    print(f"✅ Successfully set return track name to 'Test Return Track'")
    +    print("✅ Successfully set return track name to 'Test Return Track'")
    🧰 Tools
    🪛 Ruff (0.8.2)

    186-186: f-string without any placeholders

    Remove extraneous f prefix

    (F541)

    docs/implementation_summary.md (1)

    153-162: Consider adding acceptance criteria for future enhancements.

    The future work section outlines good potential enhancements. Consider adding specific acceptance criteria or success metrics for each enhancement to help prioritize and measure progress.

    docs/existing_capabilities.md (2)

    52-58: Improve grammar in browser categories description.

    There's a minor grammar issue in the parameter description.

    - `category_type`: Type of categories to get
    + `category_type`: Types of categories to get

    This change makes the parameter description grammatically consistent with the plural "categories".

    🧰 Tools
    🪛 LanguageTool

    [grammar] ~54-~54: In this context, ‘type’ should agree in number with the noun after ‘of’.
    Context: ...f browser categories | category_type: Type of categories to get | Direct command to Remote Scrip...

    (TYPE_OF_PLURAL)


    125-143: Accurately reflected missing features and next steps.

    The missing features and next steps sections accurately reflect the state of the system before this PR. Now that you've added track mixer control and return track functionality, consider updating this section to remove those items from the missing features list and add new potential features.

    tests/audio_effects_test.py (1)

    8-12: Remove unused imports for better code hygiene.

    Time and math modules are imported but not used in the script.

    import socket
    import json
    -import time
    import sys
    -import math
    🧰 Tools
    🪛 Ruff (0.8.2)

    10-10: time imported but unused

    Remove unused import: time

    (F401)


    12-12: math imported but unused

    Remove unused import: math

    (F401)

    tests/browser_device_test.py (1)

    11-14: Remove unused import.

    The time module is imported but not used in the script.

    import socket
    import json
    -import time
    import sys
    🧰 Tools
    🪛 Ruff (0.8.2)

    13-13: time imported but unused

    Remove unused import: time

    (F401)

    tests/precise_eq_test.py (2)

    7-12: Remove unused import to keep tests lint-clean

    time is imported but never referenced anywhere in the file. Dropping it prevents Ruff F401 warnings and keeps the import list tidy.

    -import time
    🧰 Tools
    🪛 Ruff (0.8.2)

    9-9: time imported but unused

    Remove unused import: time

    (F401)


    24-72: Factor out duplicated send_command helper into a shared util

    This helper is now copy-pasted across at least five test scripts.
    Aside from obvious repetition, any future protocol tweak will require editing every file, which is error-prone.

    Consider moving the function (and print_divider) to tests/utils.py and importing it here:

    -from tests.utils import print_divider, send_command
    +from tests.utils import print_divider, send_command

    That single change shrinks each test file, improves maintainability and avoids the risk of subtle drifts between copies.

    MCP_Server/server.py (3)

    784-810: Remove unused local variables to silence Ruff F841

    freq_result, gain_result, q_result, filter_result are stored but never read.

    -            freq_result = ableton.send_command("set_device_parameter", { ... })
    +            ableton.send_command("set_device_parameter", { ... })

    Apply the same pattern for the other three assignments.

    Also applies to: 818-836

    🧰 Tools
    🪛 Ruff (0.8.2)

    792-792: Local variable freq_result is assigned to but never used

    Remove assignment to unused variable freq_result

    (F841)


    803-803: Local variable gain_result is assigned to but never used

    Remove assignment to unused variable gain_result

    (F841)


    1134-1139: Collapse tiny if/else into ternary for brevity

    Ruff raises SIM108 here. A compact expression keeps the code readable and linter-clean.

    -        if volume_db == float('-inf'):
    -            volume_db_str = "-∞ dB"
    -        else:
    -            volume_db_str = f"{volume_db:.1f} dB"
    +        volume_db_str = "-∞ dB" if volume_db == float("-inf") else f"{volume_db:.1f} dB"
    🧰 Tools
    🪛 Ruff (0.8.2)

    1135-1138: Use ternary operator volume_db_str = '-∞ dB' if volume_db == float('-inf') else f'{volume_db:.1f} dB' instead of if-else-block

    Replace if-else-block with volume_db_str = '-∞ dB' if volume_db == float('-inf') else f'{volume_db:.1f} dB'

    (SIM108)


    760-770: Drop superfluous f prefixes (F541)

    These strings contain no interpolation placeholders, so the f is unnecessary and flagged by Ruff.

    -            results.append(f"Warning: Could not find a Mode parameter in EQ Eight")
    +            results.append("Warning: Could not find a Mode parameter in EQ Eight")

    Repeat for the other two occurrences.

    Also applies to: 890-910

    🧰 Tools
    🪛 Ruff (0.8.2)

    768-768: f-string without any placeholders

    Remove extraneous f prefix

    (F541)

    docs/device_parameter_control.md (1)

    106-116: Vary sentence openings to improve readability

    Three consecutive bullet points start with “Exposes the Remote Script’s…”.
    Rephrasing one or two of them eliminates the repetition flagged by LanguageTool and makes the section flow better.

    Example:

    “Provides an MCP API wrapper around the Remote Script’s _apply_eq_preset function.”

    🧰 Tools
    🪛 LanguageTool

    [style] ~114-~114: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
    Context: ...rack_index, device_index, preset_name)Exposes the Remote Script's_apply_eq_preset` ...

    (ENGLISH_WORD_REPEAT_BEGINNING_RULE)

    AbletonMCP_Remote_Script/__init__.py (2)

    1760-1782: _linear_to_db approximation works but could divide-by-zero for tiny values

    When value is extremely small but not zero, log10 still explodes (e.g. 1e-10). Consider clamping:

    -        if value <= 0:
    +        if value <= 1e-6:   # avoid log10(0)
                 return float('-inf')

    Minor but avoids noisy math domain errors during fades.


    1446-1459: Duplicate definition of _get_device_type

    The same helper already exists around lines 874-887. Keeping two copies:

    • adds ~40 lines of dead code
    • risks the two versions diverging in future maintenance

    Please delete the later duplicate or move the helper to a dedicated utilities section.

    📜 Review details

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

    📥 Commits

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

    📒 Files selected for processing (12)
    • .gitignore (1 hunks)
    • AbletonMCP_Remote_Script/__init__.py (16 hunks)
    • MCP_Server/server.py (5 hunks)
    • docs/device_parameter_control.md (1 hunks)
    • docs/existing_capabilities.md (1 hunks)
    • docs/implementation_summary.md (1 hunks)
    • tests/audio_effects_test.py (1 hunks)
    • tests/browser_device_test.py (1 hunks)
    • tests/precise_eq_test.py (1 hunks)
    • tests/return_track_test.py (1 hunks)
    • tests/send_control_test.py (1 hunks)
    • tests/volume_control_test.py (1 hunks)
    🧰 Additional context used
    🧬 Code Graph Analysis (4)
    tests/browser_device_test.py (5)
    tests/return_track_test.py (2)
    • print_divider (16-21)
    • send_command (23-71)
    tests/audio_effects_test.py (2)
    • print_divider (18-23)
    • send_command (25-73)
    tests/send_control_test.py (2)
    • print_divider (16-21)
    • send_command (23-71)
    tests/precise_eq_test.py (2)
    • print_divider (17-22)
    • send_command (24-72)
    MCP_Server/server.py (1)
    • connect (21-34)
    tests/audio_effects_test.py (5)
    tests/return_track_test.py (3)
    • print_divider (16-21)
    • send_command (23-71)
    • main (192-236)
    tests/browser_device_test.py (3)
    • print_divider (20-25)
    • send_command (27-75)
    • main (235-280)
    tests/send_control_test.py (3)
    • print_divider (16-21)
    • send_command (23-71)
    • main (192-228)
    tests/precise_eq_test.py (3)
    • print_divider (17-22)
    • send_command (24-72)
    • main (461-513)
    MCP_Server/server.py (1)
    • connect (21-34)
    tests/precise_eq_test.py (5)
    tests/return_track_test.py (3)
    • print_divider (16-21)
    • send_command (23-71)
    • main (192-236)
    tests/browser_device_test.py (3)
    • print_divider (20-25)
    • send_command (27-75)
    • main (235-280)
    tests/audio_effects_test.py (3)
    • print_divider (18-23)
    • send_command (25-73)
    • main (290-335)
    tests/send_control_test.py (3)
    • print_divider (16-21)
    • send_command (23-71)
    • main (192-228)
    MCP_Server/server.py (1)
    • connect (21-34)
    AbletonMCP_Remote_Script/__init__.py (1)
    MCP_Server/server.py (1)
    • create_return_track (306-318)
    🪛 Ruff (0.8.2)
    tests/return_track_test.py

    9-9: time imported but unused

    Remove unused import: time

    (F401)


    186-186: f-string without any placeholders

    Remove extraneous f prefix

    (F541)

    tests/browser_device_test.py

    13-13: time imported but unused

    Remove unused import: time

    (F401)

    tests/send_control_test.py

    9-9: time imported but unused

    Remove unused import: time

    (F401)


    118-118: Local variable return_track_count is assigned to but never used

    Remove assignment to unused variable return_track_count

    (F841)

    tests/audio_effects_test.py

    10-10: time imported but unused

    Remove unused import: time

    (F401)


    12-12: math imported but unused

    Remove unused import: math

    (F401)

    tests/precise_eq_test.py

    9-9: time imported but unused

    Remove unused import: time

    (F401)

    tests/volume_control_test.py

    12-12: sys imported but unused

    Remove unused import: sys

    (F401)

    MCP_Server/server.py

    768-768: f-string without any placeholders

    Remove extraneous f prefix

    (F541)


    792-792: Local variable freq_result is assigned to but never used

    Remove assignment to unused variable freq_result

    (F841)


    803-803: Local variable gain_result is assigned to but never used

    Remove assignment to unused variable gain_result

    (F841)


    819-819: Local variable q_result is assigned to but never used

    Remove assignment to unused variable q_result

    (F841)


    830-830: Local variable filter_result is assigned to but never used

    Remove assignment to unused variable filter_result

    (F841)


    880-880: Local variable scale_result is assigned to but never used

    Remove assignment to unused variable scale_result

    (F841)


    901-901: Local variable mode_result is assigned to but never used

    Remove assignment to unused variable mode_result

    (F841)


    909-909: f-string without any placeholders

    Remove extraneous f prefix

    (F541)


    927-927: Local variable oversampling_result is assigned to but never used

    Remove assignment to unused variable oversampling_result

    (F841)


    935-935: f-string without any placeholders

    Remove extraneous f prefix

    (F541)


    1018-1018: Local variable enable_result is assigned to but never used

    Remove assignment to unused variable enable_result

    (F841)


    1045-1045: Local variable freq_result is assigned to but never used

    Remove assignment to unused variable freq_result

    (F841)


    1056-1056: Local variable gain_result is assigned to but never used

    Remove assignment to unused variable gain_result

    (F841)


    1072-1072: Local variable q_result is assigned to but never used

    Remove assignment to unused variable q_result

    (F841)


    1083-1083: Local variable filter_result is assigned to but never used

    Remove assignment to unused variable filter_result

    (F841)


    1135-1138: Use ternary operator volume_db_str = '-∞ dB' if volume_db == float('-inf') else f'{volume_db:.1f} dB' instead of if-else-block

    Replace if-else-block with volume_db_str = '-∞ dB' if volume_db == float('-inf') else f'{volume_db:.1f} dB'

    (SIM108)

    🪛 LanguageTool
    docs/implementation_summary.md

    [uncategorized] ~13-~13: Loose punctuation mark.
    Context: ...mote Script: - _get_device_parameters: Retrieves all parameters for a specifie...

    (UNLIKELY_OPENING_PUNCTUATION)


    [uncategorized] ~22-~22: Loose punctuation mark.
    Context: ...r the EQ Eight device: - _set_eq_band: Sets parameters for a specific band in ...

    (UNLIKELY_OPENING_PUNCTUATION)


    [uncategorized] ~32-~32: Loose punctuation mark.
    Context: ...return tracks: - _create_return_track: Creates a new return track in the Ablet...

    (UNLIKELY_OPENING_PUNCTUATION)


    [uncategorized] ~43-~43: Loose punctuation mark.
    Context: ...th dB conversion: - _set_track_volume: Sets the volume level of a track - `_li...

    (UNLIKELY_OPENING_PUNCTUATION)


    [grammar] ~109-~109: There seems to be a noun/verb agreement error. Did you mean “sends” or “sent”?
    Context: ...ontrol functionality: - Tested setting send levels from tracks to return tracks - V...

    (SINGULAR_NOUN_VERB_AGREEMENT)

    docs/existing_capabilities.md

    [grammar] ~15-~15: This phrase is duplicated. You should probably use “external applications” only once.
    Context: ...ver and provides a higher-level API for external applications 3. External applications can use the MCP tools to control Ableto...

    (PHRASE_REPETITION)


    [grammar] ~37-~37: The verb ‘send’ does not usually follow articles like ‘a’. Check that ‘send’ is spelled correctly; using ‘send’ as a noun may be non-standard.
    Context: ...| | set_send_level | Set the level of a send from a track to a return track | `track...

    (A_INFINITIVE)


    [grammar] ~37-~37: The verb ‘send’ does not usually follow articles like ‘the’. Check that ‘send’ is spelled correctly; using ‘send’ as a noun may be non-standard.
    Context: ...f the track, send_index: The index of the send, value: The value to set (0.0 to 1.0)...

    (A_INFINITIVE)


    [grammar] ~54-~54: In this context, ‘type’ should agree in number with the noun after ‘of’.
    Context: ...f browser categories | category_type: Type of categories to get | Direct command to Remote Scrip...

    (TYPE_OF_PLURAL)

    docs/device_parameter_control.md

    [style] ~114-~114: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
    Context: ...rack_index, device_index, preset_name)Exposes the Remote Script's_apply_eq_preset` ...

    (ENGLISH_WORD_REPEAT_BEGINNING_RULE)

    🔇 Additional comments (23)
    .gitignore (1)

    12-12: The addition of the 'docs' directory to .gitignore is appropriate.

    This change ensures that the documentation files added in this pull request, covering device parameter control and implementation details, are excluded from version control.

    tests/volume_control_test.py (4)

    18-49: Well-implemented robust JSON response handling.

    The send_command function correctly handles partial JSON responses by continuing to receive data until a complete, valid JSON object is received. This is a good pattern for socket communication.


    106-111: Comprehensive volume levels testing.

    The test covers all essential volume points with meaningful descriptions:

    • Muted (0.0)
    • Half volume (-6dB)
    • Unity gain (0dB)
    • Maximum (+6dB)

    This ensures proper validation of the linear-to-dB conversion functionality.


    128-132: Good handling of special case for infinity representation.

    The code correctly formats "-∞ dB" for muted volume instead of showing the raw "-inf" value, which improves readability and user experience.


    140-148: Proper error handling and resource cleanup.

    The script ensures the socket is properly closed even if an exception occurs, which is a good practice for resource management.

    tests/send_control_test.py (4)

    23-72: Robust command sending implementation.

    The send_command function provides comprehensive error handling:

    1. Creates and manages socket connections with context managers
    2. Handles incomplete JSON responses
    3. Properly formats and logs both commands and responses
    4. Catches and reports exceptions

    This ensures reliable communication with the MCP server.


    127-139: The calculation of return track index is correct.

    The code correctly calculates the track index for return tracks by adding the return track index to the total number of regular tracks. This approach aligns with how Ableton Live handles return track indexing.


    141-191: Comprehensive send level testing with multiple values.

    The test covers the full range of send levels (0.0, 0.5, 1.0), ensuring that the set_send_level command works correctly at minimum, mid-range, and maximum values.


    192-229: Well-structured test flow and result reporting.

    The main function provides:

    1. Clear connection verification
    2. Sequential test execution
    3. Comprehensive test summary with pass/fail indications
    4. Appropriate exit codes (0 for success, 1 for failure)

    This structure makes the test results easy to interpret, both for manual execution and CI/CD integration.

    tests/return_track_test.py (3)

    73-109: Thorough return track creation verification.

    The test correctly:

    1. Gets initial return track count
    2. Creates a new return track
    3. Verifies that the count increased by exactly one

    This approach properly validates that the create_return_track command works as expected.


    192-237: Well-structured test execution and result reporting.

    The main function:

    1. Defines a list of tests to run
    2. Executes each test with exception handling
    3. Collects and reports results in a summary table
    4. Returns appropriate exit codes based on test results

    This organization makes the test suite easy to maintain and extend with additional tests.


    205-211: Good use of test collection for maintainability.

    Using a list of test function tuples allows for easy addition or removal of test cases without changing the core execution logic. This is a good practice for maintainable test suites.

    docs/implementation_summary.md (3)

    1-44: Well-structured implementation overview with appropriate architecture details.

    The implementation summary provides a clear, comprehensive explanation of the device parameter control system with well-organized sections detailing core functionality, EQ Eight specific features, and return track support capabilities. The document effectively communicates how these features work together.

    🧰 Tools
    🪛 LanguageTool

    [uncategorized] ~13-~13: Loose punctuation mark.
    Context: ...mote Script: - _get_device_parameters: Retrieves all parameters for a specifie...

    (UNLIKELY_OPENING_PUNCTUATION)


    [uncategorized] ~22-~22: Loose punctuation mark.
    Context: ...r the EQ Eight device: - _set_eq_band: Sets parameters for a specific band in ...

    (UNLIKELY_OPENING_PUNCTUATION)


    [uncategorized] ~32-~32: Loose punctuation mark.
    Context: ...return tracks: - _create_return_track: Creates a new return track in the Ablet...

    (UNLIKELY_OPENING_PUNCTUATION)


    [uncategorized] ~43-~43: Loose punctuation mark.
    Context: ...th dB conversion: - _set_track_volume: Sets the volume level of a track - `_li...

    (UNLIKELY_OPENING_PUNCTUATION)


    63-75: Good documentation of parameter naming conventions and value normalization.

    This section effectively documents the non-obvious parameter naming conventions specific to Ableton Live, particularly for the EQ Eight device. The explanation of value normalization for frequency and Q factor is essential information for anyone working with these parameters.


    124-142: Clear articulation of challenges and solutions.

    The challenges encountered during implementation and their corresponding solutions are well documented. This provides valuable context for future developers who might need to troubleshoot issues or extend the functionality further.

    docs/existing_capabilities.md (2)

    1-18: Clear architecture description.

    The overview and architecture sections provide a comprehensive understanding of the Ableton MCP system's components and how they interact. This sets a solid foundation for the rest of the document.

    🧰 Tools
    🪛 LanguageTool

    [grammar] ~15-~15: This phrase is duplicated. You should probably use “external applications” only once.
    Context: ...ver and provides a higher-level API for external applications 3. External applications can use the MCP tools to control Ableto...

    (PHRASE_REPETITION)


    35-38: Return track and mixing controls documentation aligns with PR objectives.

    The documentation of the new return track creation, send level, and track volume capabilities directly supports the PR objectives. These entries provide clear information about the parameters and expected behavior.

    🧰 Tools
    🪛 LanguageTool

    [grammar] ~37-~37: The verb ‘send’ does not usually follow articles like ‘a’. Check that ‘send’ is spelled correctly; using ‘send’ as a noun may be non-standard.
    Context: ...| | set_send_level | Set the level of a send from a track to a return track | `track...

    (A_INFINITIVE)


    [grammar] ~37-~37: The verb ‘send’ does not usually follow articles like ‘the’. Check that ‘send’ is spelled correctly; using ‘send’ as a noun may be non-standard.
    Context: ...f the track, send_index: The index of the send, value: The value to set (0.0 to 1.0)...

    (A_INFINITIVE)

    tests/audio_effects_test.py (3)

    18-74: Well-implemented socket communication pattern.

    The send_command function correctly handles socket communications with proper error handling, complete response collection, and JSON parsing. This is consistent with the pattern used in other test scripts, which is good for maintainability.


    141-245: Robust parameter testing with proper validation.

    The test_device_parameters function is well-designed with:

    1. Comprehensive error handling
    2. Support for both discrete and continuous parameters
    3. Smart parameter selection logic
    4. Validation with appropriate tolerance for floating-point comparisons

    This is a thorough implementation that will catch both obvious and subtle issues.


    303-309: Good test isolation with separate tracks for each device.

    Creating a fresh test track for each device type is a solid testing practice that ensures tests don't interfere with each other. This approach increases reliability by preventing state from leaking between tests.

    tests/browser_device_test.py (3)

    77-96: Thorough browser tree testing with multiple category types.

    The test_get_browser_tree function effectively validates the get_browser_tree command with both default parameters and explicit category types. This ensures the command works across all supported category types.


    148-153: Valuable implementation comments for maintainability.

    The comments explaining the underlying implementation details (that the server sends "load_browser_item" rather than "load_instrument_or_effect") provide excellent context for future developers. Similar helpful comments appear on lines 185-186 for the drum kit loading process.


    163-234: Complete end-to-end test for drum kit loading.

    The test_load_drum_kit function thoroughly tests the complete workflow:

    1. Creating a track
    2. Loading the drum rack
    3. Browsing for available kits
    4. Selecting and loading a kit

    This approach verifies not just individual commands but their interaction in a realistic usage scenario.

    Comment on lines +669 to +692
    @mcp.tool()
    def get_device_parameters(ctx: Context, track_index: int, device_index: int) -> Dict[str, Any]:
    """
    Get all parameters for a device.

    Parameters:
    - track_index: The index of the track containing the device
    - device_index: The index of the device on the track

    Returns:
    - Dictionary with device information and parameters
    """
    try:
    ableton = get_ableton_connection()
    result = ableton.send_command("get_device_parameters", {
    "track_index": track_index,
    "device_index": device_index
    })

    return result
    except Exception as e:
    logger.error(f"Error getting device parameters: {str(e)}")
    return {"error": f"Error getting device parameters: {str(e)}"}

    Copy link

    Choose a reason for hiding this comment

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

    🛠️ Refactor suggestion

    Return value must be JSON-serialisable string

    FastMCP tools normally return a str. Returning a raw dict will cause FastAPI to attempt automatic JSON encoding which breaks the streaming/text-only contract and can confuse clients that expect plain text.

    -        return result
    +        return json.dumps(result, indent=2)

    Committable suggestion skipped: line range outside the PR's diff.

    Comment on lines +1732 to +1758
    def _set_track_volume(self, track_index, value):
    """Set the volume of a track

    Args:
    track_index: Index of the track
    value: Volume value (0.0 to 1.0)

    Returns:
    Dictionary with track info and new volume value
    """
    try:
    # Get the track using the helper function that handles return tracks
    track = self._get_track_by_index(track_index)

    # Set the volume value (0.0 to 1.0)
    track.mixer_device.volume.value = value

    result = {
    "track_name": track.name,
    "volume": track.mixer_device.volume.value,
    "volume_db": self._linear_to_db(track.mixer_device.volume.value)
    }
    return result
    except Exception as e:
    self.log_message("Error setting track volume: " + str(e))
    self.log_message(traceback.format_exc())
    raise
    Copy link

    Choose a reason for hiding this comment

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

    🛠️ Refactor suggestion

    Track-volume setter lacks input validation

    mixer_device.volume.value silently throws if given < 0 or > 1.
    Rejecting or clamping the input prevents remote users from crashing the script.

    -            # Set the volume value (0.0 to 1.0)
    -            track.mixer_device.volume.value = value
    +            # Validate & set volume (0.0 – 1.0)
    +            if value < 0.0 or value > 1.0:
    +                raise ValueError("Volume value must be between 0.0 and 1.0")
    +            track.mixer_device.volume.value = value
    📝 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
    def _set_track_volume(self, track_index, value):
    """Set the volume of a track
    Args:
    track_index: Index of the track
    value: Volume value (0.0 to 1.0)
    Returns:
    Dictionary with track info and new volume value
    """
    try:
    # Get the track using the helper function that handles return tracks
    track = self._get_track_by_index(track_index)
    # Set the volume value (0.0 to 1.0)
    track.mixer_device.volume.value = value
    result = {
    "track_name": track.name,
    "volume": track.mixer_device.volume.value,
    "volume_db": self._linear_to_db(track.mixer_device.volume.value)
    }
    return result
    except Exception as e:
    self.log_message("Error setting track volume: " + str(e))
    self.log_message(traceback.format_exc())
    raise
    def _set_track_volume(self, track_index, value):
    """Set the volume of a track
    Args:
    track_index: Index of the track
    value: Volume value (0.0 to 1.0)
    Returns:
    Dictionary with track info and new volume value
    """
    try:
    # Get the track using the helper function that handles return tracks
    track = self._get_track_by_index(track_index)
    # Validate & set volume (0.0 – 1.0)
    if value < 0.0 or value > 1.0:
    raise ValueError("Volume value must be between 0.0 and 1.0")
    track.mixer_device.volume.value = value
    result = {
    "track_name": track.name,
    "volume": track.mixer_device.volume.value,
    "volume_db": self._linear_to_db(track.mixer_device.volume.value)
    }
    return result
    except Exception as e:
    self.log_message("Error setting track volume: " + str(e))
    self.log_message(traceback.format_exc())
    raise

    Comment on lines +1702 to +1726
    def _set_send_level(self, track_index, send_index, value):
    """Set the level of a send from a track to a return track"""
    try:
    # Get the track using the helper function that handles return tracks
    track = self._get_track_by_index(track_index)

    # Return tracks don't have sends, so make sure this is a regular track
    if track_index >= len(self._song.tracks):
    raise ValueError("Return tracks don't have sends")

    # Verify the send index is valid
    if send_index < 0 or send_index >= len(track.mixer_device.sends):
    raise IndexError("Send index out of range")

    # Get the send and set its value
    send = track.mixer_device.sends[send_index]
    send.value = value

    result = {
    "track_name": track.name,
    "send_index": send_index,
    "return_track_name": self._song.return_tracks[send_index].name,
    "value": send.value
    }
    return result
    Copy link

    Choose a reason for hiding this comment

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

    ⚠️ Potential issue

    Missing value-range validation and possible out-of-range access

    1. value is written directly to send.value without clamping. Ableton’s send parameters are 0.0 – 1.0; passing >1.0 raises RuntimeError in Live.

    2. return_track_name is looked-up with self._song.return_tracks[send_index].
      If a new return track is created but the track doesn’t have a corresponding send yet (e.g. when “Sends only” routing is disabled), this index access will fail.

    -            # Verify the send index is valid
    -            if send_index < 0 or send_index >= len(track.mixer_device.sends):
    +            # Verify the send index is valid
    +            if send_index < 0 or send_index >= len(track.mixer_device.sends):
                     raise IndexError("Send index out of range")
     
    -            # Get the send and set its value
    -            send = track.mixer_device.sends[send_index]
    -            send.value = value
    +            # Clamp volume to [0.0, 1.0] to avoid Live errors
    +            if value < 0.0 or value > 1.0:
    +                raise ValueError("Send level must be between 0.0 and 1.0")
    +
    +            send = track.mixer_device.sends[send_index]
    +            send.value = value

    Also wrap the return_track_name retrieval in a safe guard or drop it entirely if the mapping is uncertain.

    📝 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
    def _set_send_level(self, track_index, send_index, value):
    """Set the level of a send from a track to a return track"""
    try:
    # Get the track using the helper function that handles return tracks
    track = self._get_track_by_index(track_index)
    # Return tracks don't have sends, so make sure this is a regular track
    if track_index >= len(self._song.tracks):
    raise ValueError("Return tracks don't have sends")
    # Verify the send index is valid
    if send_index < 0 or send_index >= len(track.mixer_device.sends):
    raise IndexError("Send index out of range")
    # Get the send and set its value
    send = track.mixer_device.sends[send_index]
    send.value = value
    result = {
    "track_name": track.name,
    "send_index": send_index,
    "return_track_name": self._song.return_tracks[send_index].name,
    "value": send.value
    }
    return result
    def _set_send_level(self, track_index, send_index, value):
    """Set the level of a send from a track to a return track"""
    try:
    # Get the track using the helper function that handles return tracks
    track = self._get_track_by_index(track_index)
    # Return tracks don't have sends, so make sure this is a regular track
    if track_index >= len(self._song.tracks):
    raise ValueError("Return tracks don't have sends")
    # Verify the send index is valid
    if send_index < 0 or send_index >= len(track.mixer_device.sends):
    raise IndexError("Send index out of range")
    # Clamp volume to [0.0, 1.0] to avoid Live errors
    if value < 0.0 or value > 1.0:
    raise ValueError("Send level must be between 0.0 and 1.0")
    send = track.mixer_device.sends[send_index]
    send.value = value
    result = {
    "track_name": track.name,
    "send_index": send_index,
    "return_track_name": self._song.return_tracks[send_index].name,
    "value": send.value
    }
    return result

    Comment on lines +501 to +516
    def _create_return_track(self):
    """Create a new return track"""
    try:
    # Create the return track
    self._song.create_return_track()

    # Get the new return track
    new_return_track_index = len(self._song.return_tracks) - 1
    new_return_track = self._song.return_tracks[new_return_track_index]

    result = {
    "index": new_return_track_index,
    "name": new_return_track.name
    }
    return result
    except Exception as e:
    Copy link

    Choose a reason for hiding this comment

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

    ⚠️ Potential issue

    Return-track index returned is inconsistent with the helper _get_track_by_index

    _create_return_track() returns the index within self._song.return_tracks, but everywhere else in the API (e.g. _get_track_by_index) the caller is expected to treat track_index as the global index (regular_track_count + return_track_offset).
    Mixing two index spaces will confuse clients and break round-tripping (create_return_track → get_track_info) because the latter expects the global index.

    -            # Get the new return track
    -            new_return_track_index = len(self._song.return_tracks) - 1
    -            new_return_track = self._song.return_tracks[new_return_track_index]
    -
    -            result = {
    -                "index": new_return_track_index,
    +            # Get the new return track
    +            local_index = len(self._song.return_tracks) - 1
    +            global_index = len(self._song.tracks) + local_index
    +            new_return_track = self._song.return_tracks[local_index]
    +
    +            result = {
    +                # Return the *global* index so that callers can pass it
    +                # back into API methods expecting the unified space.
    +                "index": global_index,
                     "name": new_return_track.name
                 }
    📝 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
    def _create_return_track(self):
    """Create a new return track"""
    try:
    # Create the return track
    self._song.create_return_track()
    # Get the new return track
    new_return_track_index = len(self._song.return_tracks) - 1
    new_return_track = self._song.return_tracks[new_return_track_index]
    result = {
    "index": new_return_track_index,
    "name": new_return_track.name
    }
    return result
    except Exception as e:
    def _create_return_track(self):
    """Create a new return track"""
    try:
    # Create the return track
    self._song.create_return_track()
    # Get the new return track
    local_index = len(self._song.return_tracks) - 1
    global_index = len(self._song.tracks) + local_index
    new_return_track = self._song.return_tracks[local_index]
    result = {
    # Return the *global* index so that callers can pass it
    # back into API methods expecting the unified space.
    "index": global_index,
    "name": new_return_track.name
    }
    return result
    except Exception as e:

    Copy link

    @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

    ♻️ Duplicate comments (4)
    MCP_Server/server.py (1)

    670-693: ⚠️ Potential issue

    Return value must be JSON-serializable string.

    FastMCP tools normally return a str. Returning a raw Dict will cause FastAPI to attempt automatic JSON encoding which breaks the streaming/text-only contract and can confuse clients that expect plain text.

    Apply this fix:

    -        return result
    +        return json.dumps(result, indent=2)
    AbletonMCP_Remote_Script/__init__.py (3)

    501-518: ⚠️ Potential issue

    Return-track index returned is inconsistent with the helper _get_track_by_index

    _create_return_track() returns the index within self._song.return_tracks, but everywhere else in the API (e.g. _get_track_by_index) the caller is expected to treat track_index as the global index (regular_track_count + return_track_offset).
    Mixing two index spaces will confuse clients and break round-tripping (create_return_track → get_track_info) because the latter expects the global index.

    Apply this change:

    -            # Get the new return track
    -            new_return_track_index = len(self._song.return_tracks) - 1
    -            new_return_track = self._song.return_tracks[new_return_track_index]
    -
    -            result = {
    -                "index": new_return_track_index,
    +            # Get the new return track
    +            local_index = len(self._song.return_tracks) - 1
    +            global_index = len(self._song.tracks) + local_index
    +            new_return_track = self._song.return_tracks[local_index]
    +
    +            result = {
    +                # Return the *global* index so that callers can pass it
    +                # back into API methods expecting the unified space.
    +                "index": global_index,
                     "name": new_return_track.name
                 }

    1687-1711: ⚠️ Potential issue

    Missing value-range validation and possible out-of-range access.

    1. value is written directly to send.value without clamping. Ableton's send parameters are 0.0 – 1.0; passing >1.0 raises RuntimeError in Live.

    2. return_track_name is looked-up with self._song.return_tracks[send_index].
      If a new return track is created but the track doesn't have a corresponding send yet (e.g. when "Sends only" routing is disabled), this index access will fail.

    Apply this change:

    -            # Verify the send index is valid
    -            if send_index < 0 or send_index >= len(track.mixer_device.sends):
    +            # Verify the send index is valid
    +            if send_index < 0 or send_index >= len(track.mixer_device.sends):
                     raise IndexError("Send index out of range")
     
    -            # Get the send and set its value
    -            send = track.mixer_device.sends[send_index]
    -            send.value = value
    +            # Clamp volume to [0.0, 1.0] to avoid Live errors
    +            if value < 0.0 or value > 1.0:
    +                raise ValueError("Send level must be between 0.0 and 1.0")
    +
    +            send = track.mixer_device.sends[send_index]
    +            send.value = value

    Also wrap the return_track_name retrieval in a safe guard or drop it entirely if the mapping is uncertain.


    1717-1743: ⚠️ Potential issue

    Track-volume setter lacks input validation.

    mixer_device.volume.value silently throws if given < 0 or > 1.
    Rejecting or clamping the input prevents remote users from crashing the script.

    Apply this change:

    -            # Set the volume value (0.0 to 1.0)
    -            track.mixer_device.volume.value = value
    +            # Validate & set volume (0.0 – 1.0)
    +            if value < 0.0 or value > 1.0:
    +                raise ValueError("Volume value must be between 0.0 and 1.0")
    +            track.mixer_device.volume.value = value
    🧹 Nitpick comments (3)
    MCP_Server/server.py (3)

    793-793: Remove unused variable assignments.

    Multiple result variables are assigned but never used throughout the EQ parameter manipulation code. This won't affect functionality but reduces code clarity.

    Apply this pattern to all similar instances:

    -            freq_result = ableton.send_command("set_device_parameter", {
    +            ableton.send_command("set_device_parameter", {

    Also applies to: 804-804, 820-820, 831-831, 881-881, 902-902, 928-928, 1019-1019, 1046-1046, 1057-1057, 1073-1073, 1084-1084

    🧰 Tools
    🪛 Ruff (0.8.2)

    793-793: Local variable freq_result is assigned to but never used

    Remove assignment to unused variable freq_result

    (F841)


    769-769: Remove unnecessary f-strings.

    These strings don't contain any placeholders, so the 'f' prefix is unnecessary.

    Apply this pattern to all similar instances:

    -            return f"Warning: Could not find an Oversampling parameter in EQ Eight"
    +            return "Warning: Could not find an Oversampling parameter in EQ Eight"

    Also applies to: 910-910, 936-936

    🧰 Tools
    🪛 Ruff (0.8.2)

    769-769: f-string without any placeholders

    Remove extraneous f prefix

    (F541)


    1136-1140: Use ternary operator for conciseness.

    This if-else block can be simplified using a ternary operator.

    Apply this change:

    -        if volume_db == float('-inf'):
    -            volume_db_str = "-∞ dB"
    -        else:
    -            volume_db_str = f"{volume_db:.1f} dB"
    +        volume_db_str = "-∞ dB" if volume_db == float('-inf') else f"{volume_db:.1f} dB"
    🧰 Tools
    🪛 Ruff (0.8.2)

    1136-1139: Use ternary operator volume_db_str = '-∞ dB' if volume_db == float('-inf') else f'{volume_db:.1f} dB' instead of if-else-block

    Replace if-else-block with volume_db_str = '-∞ dB' if volume_db == float('-inf') else f'{volume_db:.1f} dB'

    (SIM108)

    📜 Review details

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

    📥 Commits

    Reviewing files that changed from the base of the PR and between 843727d and b7aa379.

    📒 Files selected for processing (2)
    • AbletonMCP_Remote_Script/__init__.py (16 hunks)
    • MCP_Server/server.py (5 hunks)
    🧰 Additional context used
    🧬 Code Graph Analysis (1)
    AbletonMCP_Remote_Script/__init__.py (1)
    MCP_Server/server.py (1)
    • create_return_track (307-319)
    🪛 Ruff (0.8.2)
    MCP_Server/server.py

    769-769: f-string without any placeholders

    Remove extraneous f prefix

    (F541)


    793-793: Local variable freq_result is assigned to but never used

    Remove assignment to unused variable freq_result

    (F841)


    804-804: Local variable gain_result is assigned to but never used

    Remove assignment to unused variable gain_result

    (F841)


    820-820: Local variable q_result is assigned to but never used

    Remove assignment to unused variable q_result

    (F841)


    831-831: Local variable filter_result is assigned to but never used

    Remove assignment to unused variable filter_result

    (F841)


    881-881: Local variable scale_result is assigned to but never used

    Remove assignment to unused variable scale_result

    (F841)


    902-902: Local variable mode_result is assigned to but never used

    Remove assignment to unused variable mode_result

    (F841)


    910-910: f-string without any placeholders

    Remove extraneous f prefix

    (F541)


    928-928: Local variable oversampling_result is assigned to but never used

    Remove assignment to unused variable oversampling_result

    (F841)


    936-936: f-string without any placeholders

    Remove extraneous f prefix

    (F541)


    1019-1019: Local variable enable_result is assigned to but never used

    Remove assignment to unused variable enable_result

    (F841)


    1046-1046: Local variable freq_result is assigned to but never used

    Remove assignment to unused variable freq_result

    (F841)


    1057-1057: Local variable gain_result is assigned to but never used

    Remove assignment to unused variable gain_result

    (F841)


    1073-1073: Local variable q_result is assigned to but never used

    Remove assignment to unused variable q_result

    (F841)


    1084-1084: Local variable filter_result is assigned to but never used

    Remove assignment to unused variable filter_result

    (F841)


    1136-1139: Use ternary operator volume_db_str = '-∞ dB' if volume_db == float('-inf') else f'{volume_db:.1f} dB' instead of if-else-block

    Replace if-else-block with volume_db_str = '-∞ dB' if volume_db == float('-inf') else f'{volume_db:.1f} dB'

    (SIM108)

    🔇 Additional comments (22)
    MCP_Server/server.py (9)

    8-8: Good addition of detailed type annotations.

    You've expanded the typing imports to include all the types used in the code, which improves code readability and type checking capabilities.


    107-110: LGTM - Correctly categorized new state-modifying commands.

    The new commands have been properly added to the list of state-modifying commands, ensuring they receive the appropriate timing delays and error handling.


    268-268: Good consistency improvement.

    Added an explicit empty dictionary parameter to maintain consistency with other similar calls in the codebase.


    306-319: LGTM - New return track creation tool.

    Implementation correctly exposes the remote script's return track creation functionality through the MCP API as outlined in the PR objectives.


    736-845: Comprehensive EQ band control implementation.

    The implementation correctly handles all EQ band parameters with appropriate frequency mapping using logarithmic scale. It includes proper validation of band indices and parameter values.

    🧰 Tools
    🪛 Ruff (0.8.2)

    769-769: f-string without any placeholders

    Remove extraneous f prefix

    (F541)


    793-793: Local variable freq_result is assigned to but never used

    Remove assignment to unused variable freq_result

    (F841)


    804-804: Local variable gain_result is assigned to but never used

    Remove assignment to unused variable gain_result

    (F841)


    820-820: Local variable q_result is assigned to but never used

    Remove assignment to unused variable q_result

    (F841)


    831-831: Local variable filter_result is assigned to but never used

    Remove assignment to unused variable filter_result

    (F841)


    847-944: Well-implemented EQ global parameter control with fallbacks.

    The code nicely handles the case where specific parameters might not be available in the EQ Eight device, using fallbacks and clear warning messages when parameters aren't found.

    🧰 Tools
    🪛 Ruff (0.8.2)

    881-881: Local variable scale_result is assigned to but never used

    Remove assignment to unused variable scale_result

    (F841)


    902-902: Local variable mode_result is assigned to but never used

    Remove assignment to unused variable mode_result

    (F841)


    910-910: f-string without any placeholders

    Remove extraneous f prefix

    (F541)


    928-928: Local variable oversampling_result is assigned to but never used

    Remove assignment to unused variable oversampling_result

    (F841)


    936-936: f-string without any placeholders

    Remove extraneous f prefix

    (F541)


    946-1095: Good preset system with comprehensive EQ configurations.

    You've created a flexible preset system for the EQ Eight device with common configurations like high/low cuts and shelf filters. The implementation gracefully applies multiple parameter changes as a single preset.

    🧰 Tools
    🪛 Ruff (0.8.2)

    1019-1019: Local variable enable_result is assigned to but never used

    Remove assignment to unused variable enable_result

    (F841)


    1046-1046: Local variable freq_result is assigned to but never used

    Remove assignment to unused variable freq_result

    (F841)


    1057-1057: Local variable gain_result is assigned to but never used

    Remove assignment to unused variable gain_result

    (F841)


    1073-1073: Local variable q_result is assigned to but never used

    Remove assignment to unused variable q_result

    (F841)


    1084-1084: Local variable filter_result is assigned to but never used

    Remove assignment to unused variable filter_result

    (F841)


    1097-1117: Send level control works as expected.

    The method correctly implements the send level control between tracks and return tracks, with proper parameter handling and result formatting.


    1119-1144: Volume control with proper decibel conversion.

    The implementation correctly handles volume control with appropriate conversion to decibel values for display purposes.

    🧰 Tools
    🪛 Ruff (0.8.2)

    1136-1139: Use ternary operator volume_db_str = '-∞ dB' if volume_db == float('-inf') else f'{volume_db:.1f} dB' instead of if-else-block

    Replace if-else-block with volume_db_str = '-∞ dB' if volume_db == float('-inf') else f'{volume_db:.1f} dB'

    (SIM108)

    AbletonMCP_Remote_Script/__init__.py (13)

    10-10: Valid addition of math module import.

    The math module is required for logarithmic calculations in EQ parameter normalization and volume conversion.


    233-233: Updated state-modifying command list.

    The new commands are properly integrated into the command processing logic.


    282-292: New command handlers properly integrated.

    The new command handlers for return tracks, send levels, and track volume are correctly wired up to their respective implementation methods.


    337-368: Device parameter command handlers properly integrated.

    The new command handlers for device parameters and EQ manipulation are correctly routed to their implementations.


    438-458: Good track type detection with conditional properties.

    The code correctly identifies return tracks and only includes ARM property for regular tracks, as return tracks don't support this feature.


    465-480: Great helper method for unified track access.

    The _get_track_by_index helper centralizes track retrieval logic for both regular and return tracks, reducing code duplication and improving maintainability.


    876-887: Improved device type detection with precise class checks.

    The updated method provides more accurate device type detection by checking specific class names and device capabilities.


    889-934: Comprehensive device parameter retrieval with value item handling.

    The implementation correctly fetches device parameters with proper handling of quantized parameters and their possible values.


    935-1017: Parameter setting with robust validation and type handling.

    The implementation properly handles parameter setting by name or index with extensive validation and proper handling of quantized vs. continuous parameters.


    1018-1158: Precise EQ band parameter control with frequency scaling.

    The method handles the logarithmic scaling of frequencies correctly and provides proper validation of filter types.


    1160-1257: EQ global parameter control with graceful fallbacks.

    The implementation handles the case when certain parameters might not be available in the device, providing clear error messages rather than failing silently.


    1258-1444: Well-designed preset system for EQ.

    The preset system provides a variety of common EQ configurations that can be easily applied with a single command.


    1745-1771: Well-implemented volume unit conversion.

    The conversion from linear values to decibels correctly implements Ableton's non-linear volume curve, with appropriate handling of zero/near-zero values.

    @itsuzef
    Copy link
    Author

    itsuzef commented May 1, 2025

    I removed the docs since I did not find any from your end, not sure how you would like to maintain. Happy to do it, let me know!

    Copy link

    @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: 4

    ♻️ Duplicate comments (6)
    MCP_Server/server.py (3)

    670-693: ⚠️ Potential issue

    Return value must be JSON-serializable string.

    FastMCP tools should consistently return strings, but this function returns a raw dictionary that will cause FastAPI to attempt automatic JSON encoding, potentially breaking client expectations.

    Apply this change to maintain consistency with other API tools:

    -        return result
    +        return json.dumps(result, indent=2)

    1097-1118: ⚠️ Potential issue

    Add value validation to send_level setting.

    The code correctly sets the send level but doesn't validate the input value range. This could lead to runtime errors in Live if values outside the valid range are provided.

    Add value validation:

        try:
            ableton = get_ableton_connection()
    +       # Validate value range
    +       if value < 0.0 or value > 1.0:
    +           return f"Error: Send level value must be between 0.0 and 1.0"
            result = ableton.send_command("set_send_level", {
                "track_index": track_index, 
                "send_index": send_index, 
                "value": value
            })

    1119-1145: ⚠️ Potential issue

    Add value validation to track volume setting.

    Similar to the send level, this function should validate the input value range.

    Add value validation:

        try:
            ableton = get_ableton_connection()
    +       # Validate value range
    +       if value < 0.0 or value > 1.0:
    +           return f"Error: Volume value must be between 0.0 and 1.0"
            result = ableton.send_command("set_track_volume", {
                "track_index": track_index, 
                "value": value
            })

    Also, use a ternary operator for improved readability as suggested by static analysis:

    -        volume_db = result.get('volume_db', 'unknown')
    -        if volume_db == float('-inf'):
    -            volume_db_str = "-∞ dB"
    -        else:
    -            volume_db_str = f"{volume_db:.1f} dB"
    +        volume_db = result.get('volume_db', 'unknown')
    +        volume_db_str = "-∞ dB" if volume_db == float('-inf') else f"{volume_db:.1f} dB"
    🧰 Tools
    🪛 Ruff (0.8.2)

    1136-1139: Use ternary operator volume_db_str = '-∞ dB' if volume_db == float('-inf') else f'{volume_db:.1f} dB' instead of if-else-block

    Replace if-else-block with volume_db_str = '-∞ dB' if volume_db == float('-inf') else f'{volume_db:.1f} dB'

    (SIM108)

    AbletonMCP_Remote_Script/__init__.py (3)

    505-523: ⚠️ Potential issue

    Return-track index returned is inconsistent with track index helper.

    The _create_return_track() returns the index within self._song.return_tracks, but elsewhere in the API, track indices represent a unified space that includes both regular and return tracks.

    -            # Get the new return track
    -            new_return_track_index = len(self._song.return_tracks) - 1
    -            new_return_track = self._song.return_tracks[new_return_track_index]
    -
    -            result = {
    -                "index": new_return_track_index,
    +            # Get the new return track
    +            local_index = len(self._song.return_tracks) - 1
    +            global_index = len(self._song.tracks) + local_index
    +            new_return_track = self._song.return_tracks[local_index]
    +
    +            result = {
    +                # Return the global index for consistency with API methods
    +                "index": global_index,
                     "name": new_return_track.name
                 }

    1690-1719: ⚠️ Potential issue

    Missing validation in send level setter.

    The implementation doesn't validate or clamp the input value to ensure it's within Ableton's expected range (0.0 to 1.0), which could cause runtime errors.

    -            # Verify the send index is valid
    -            if send_index < 0 or send_index >= len(track.mixer_device.sends):
    +            # Verify the send index is valid
    +            if send_index < 0 or send_index >= len(track.mixer_device.sends):
                     raise IndexError("Send index out of range")
     
    -            # Get the send and set its value
    -            send = track.mixer_device.sends[send_index]
    -            send.value = value
    +            # Validate value range
    +            if value < 0.0 or value > 1.0:
    +                raise ValueError("Send level must be between 0.0 and 1.0")
    +
    +            send = track.mixer_device.sends[send_index]
    +            send.value = value

    Also, ensure that return_track_name retrieval is safe:

    +            # Safely get return track name
    +            return_track_name = "unknown"
    +            if send_index < len(self._song.return_tracks):
    +                return_track_name = self._song.return_tracks[send_index].name
    +
                 result = {
                     "track_name": track.name,
                     "send_index": send_index,
    -                "return_track_name": self._song.return_tracks[send_index].name,
    +                "return_track_name": return_track_name,
                     "value": send.value
                 }

    1720-1747: ⚠️ Potential issue

    Track-volume setter lacks input validation.

    mixer_device.volume.value silently throws if given values outside the valid range. Validating inputs would prevent remote users from crashing the script.

    -            # Set the volume value (0.0 to 1.0)
    -            track.mixer_device.volume.value = value
    +            # Validate & set volume (0.0 – 1.0)
    +            if value < 0.0 or value > 1.0:
    +                raise ValueError("Volume value must be between 0.0 and 1.0")
    +            track.mixer_device.volume.value = value
    🧹 Nitpick comments (6)
    start_local_server.sh (1)

    4-4: Consider using CLI interface instead of direct server module.

    The smithery.yaml configuration has been updated to use the new CLI interface python -m MCP_Server.cli server, but this script still uses direct module execution. For consistency, consider using the CLI interface here as well.

    -python -m MCP_Server.server
    +python -m MCP_Server.cli server
    README.md (3)

    67-88: Fix heading formatting and clarify environment path instructions.

    The heading has punctuation that violates markdown best practices. Additionally, the example paths could be more specific to help users understand exactly what to replace.

    -#### For Claude Desktop:
    +#### For Claude Desktop
     
     1. Go to Claude → Settings → Developer → Edit Config
     2. Add this to your `claude_desktop_config.json`:

    Also, consider adding more specific examples for the environment path replacement, such as:

    • Mac: /Users/yourusername/ableton-mcp-env/Users/john/ableton-mcp-env
    • Windows: C:\Users\yourusername\ableton-mcp-envC:\Users\john\ableton-mcp-env
    🧰 Tools
    🪛 markdownlint-cli2 (0.17.2)

    67-67: Trailing punctuation in heading
    Punctuation: ':'

    (MD026, no-trailing-punctuation)


    89-96: Add language specifier to code block.

    The code block is missing a language specifier, which helps with syntax highlighting.

     2. Add this command:
     
    -```
    +```bash
     PATH_TO_YOUR_ENVIRONMENT/bin/ableton-mcp server
    
    <details>
    <summary>🧰 Tools</summary>
    
    <details>
    <summary>🪛 markdownlint-cli2 (0.17.2)</summary>
    
    89-89: Trailing punctuation in heading
    Punctuation: ':'
    
    (MD026, no-trailing-punctuation)
    
    ---
    
    94-94: Fenced code blocks should have a language specified
    null
    
    (MD040, fenced-code-language)
    
    </details>
    
    </details>
    
    ---
    
    `132-132`: **Add missing comma in compound sentence.**
    
    There should be a comma before "and" since it connects two independent clauses.
    
    ```diff
    -**"Could not connect to Ableton"**: Ensure Ableton is running and the Remote Script is enabled
    +**"Could not connect to Ableton"**: Ensure Ableton is running, and the Remote Script is enabled
    
    🧰 Tools
    🪛 LanguageTool

    [uncategorized] ~132-~132: Use a comma before “and” if it connects two independent clauses (unless they are closely connected and short).
    Context: ...to Ableton"**: Ensure Ableton is running and the Remote Script is enabled - **"Remot...

    (COMMA_COMPOUND_SENTENCE_2)

    MCP_Server/cli.py (2)

    27-79: Clean up unused parser variables.

    The info_parser and version_parser variables are assigned but never used. While this doesn't affect functionality, it's good practice to clean up unused variables.

     # Info command
    -info_parser = subparsers.add_parser("info", help="Show information about the MCP server")
    +subparsers.add_parser("info", help="Show information about the MCP server")
     
     # Version command
    -version_parser = subparsers.add_parser("version", help="Show version information")
    +subparsers.add_parser("version", help="Show version information")
    🧰 Tools
    🪛 Ruff (0.8.2)

    57-57: Local variable info_parser is assigned to but never used

    Remove assignment to unused variable info_parser

    (F841)


    60-60: Local variable version_parser is assigned to but never used

    Remove assignment to unused variable version_parser

    (F841)


    121-122: Use capitalized environment variable names for consistency.

    While the current code works, it's a best practice to use the standard capitalized environment variable names.

    -program_files = os.environ.get("ProgramFiles", "C:\\Program Files")
    -program_files_x86 = os.environ.get("ProgramFiles(x86)", "C:\\Program Files (x86)")
    +program_files = os.environ.get("PROGRAMFILES", "C:\\Program Files")
    +program_files_x86 = os.environ.get("PROGRAMFILES(X86)", "C:\\Program Files (x86)")
    🧰 Tools
    🪛 Ruff (0.8.2)

    121-121: Use capitalized environment variable PROGRAMFILES instead of ProgramFiles

    (SIM112)


    122-122: Use capitalized environment variable PROGRAMFILES(X86) instead of ProgramFiles(x86)

    (SIM112)

    📜 Review details

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

    📥 Commits

    Reviewing files that changed from the base of the PR and between b7aa379 and 0a45861.

    ⛔ Files ignored due to path filters (2)
    • .embold/issues.db is excluded by !**/*.db
    • .embold/vscode-logs.log is excluded by !**/*.log
    📒 Files selected for processing (9)
    • AbletonMCP_Remote_Script/__init__.py (16 hunks)
    • Dockerfile (1 hunks)
    • LICENSE (1 hunks)
    • MCP_Server/cli.py (1 hunks)
    • MCP_Server/server.py (5 hunks)
    • README.md (1 hunks)
    • pyproject.toml (3 hunks)
    • smithery.yaml (2 hunks)
    • start_local_server.sh (1 hunks)
    ✅ Files skipped from review due to trivial changes (3)
    • LICENSE
    • Dockerfile
    • pyproject.toml
    🧰 Additional context used
    🧬 Code Graph Analysis (2)
    MCP_Server/cli.py (1)
    MCP_Server/server.py (1)
    • main (1147-1156)
    AbletonMCP_Remote_Script/__init__.py (1)
    MCP_Server/server.py (1)
    • create_return_track (307-319)
    🪛 LanguageTool
    README.md

    [uncategorized] ~132-~132: Use a comma before “and” if it connects two independent clauses (unless they are closely connected and short).
    Context: ...to Ableton"**: Ensure Ableton is running and the Remote Script is enabled - **"Remot...

    (COMMA_COMPOUND_SENTENCE_2)

    🪛 markdownlint-cli2 (0.17.2)
    README.md

    67-67: Trailing punctuation in heading
    Punctuation: ':'

    (MD026, no-trailing-punctuation)


    89-89: Trailing punctuation in heading
    Punctuation: ':'

    (MD026, no-trailing-punctuation)


    94-94: Fenced code blocks should have a language specified
    null

    (MD040, fenced-code-language)


    100-100: Trailing punctuation in heading
    Punctuation: ':'

    (MD026, no-trailing-punctuation)

    🪛 Ruff (0.8.2)
    MCP_Server/cli.py

    57-57: Local variable info_parser is assigned to but never used

    Remove assignment to unused variable info_parser

    (F841)


    60-60: Local variable version_parser is assigned to but never used

    Remove assignment to unused variable version_parser

    (F841)


    121-121: Use capitalized environment variable PROGRAMFILES instead of ProgramFiles

    (SIM112)


    122-122: Use capitalized environment variable PROGRAMFILES(X86) instead of ProgramFiles(x86)

    (SIM112)

    MCP_Server/server.py

    769-769: f-string without any placeholders

    Remove extraneous f prefix

    (F541)


    793-793: Local variable freq_result is assigned to but never used

    Remove assignment to unused variable freq_result

    (F841)


    804-804: Local variable gain_result is assigned to but never used

    Remove assignment to unused variable gain_result

    (F841)


    820-820: Local variable q_result is assigned to but never used

    Remove assignment to unused variable q_result

    (F841)


    831-831: Local variable filter_result is assigned to but never used

    Remove assignment to unused variable filter_result

    (F841)


    881-881: Local variable scale_result is assigned to but never used

    Remove assignment to unused variable scale_result

    (F841)


    902-902: Local variable mode_result is assigned to but never used

    Remove assignment to unused variable mode_result

    (F841)


    910-910: f-string without any placeholders

    Remove extraneous f prefix

    (F541)


    928-928: Local variable oversampling_result is assigned to but never used

    Remove assignment to unused variable oversampling_result

    (F841)


    936-936: f-string without any placeholders

    Remove extraneous f prefix

    (F541)


    1019-1019: Local variable enable_result is assigned to but never used

    Remove assignment to unused variable enable_result

    (F841)


    1046-1046: Local variable freq_result is assigned to but never used

    Remove assignment to unused variable freq_result

    (F841)


    1057-1057: Local variable gain_result is assigned to but never used

    Remove assignment to unused variable gain_result

    (F841)


    1073-1073: Local variable q_result is assigned to but never used

    Remove assignment to unused variable q_result

    (F841)


    1084-1084: Local variable filter_result is assigned to but never used

    Remove assignment to unused variable filter_result

    (F841)


    1136-1139: Use ternary operator volume_db_str = '-∞ dB' if volume_db == float('-inf') else f'{volume_db:.1f} dB' instead of if-else-block

    Replace if-else-block with volume_db_str = '-∞ dB' if volume_db == float('-inf') else f'{volume_db:.1f} dB'

    (SIM108)

    🪛 Shellcheck (0.10.0)
    start_local_server.sh

    [warning] 2-2: Use 'cd ... || exit' or 'cd ... || return' in case cd fails.

    (SC2164)

    🔇 Additional comments (26)
    smithery.yaml (2)

    2-7: LGTM! Great addition of project metadata.

    The addition of detailed project metadata provides valuable information for users and tools interacting with this project.


    20-20: LGTM! Command properly updated to use CLI interface.

    The command has been correctly updated to use the new CLI interface instead of directly invoking the server module.

    README.md (4)

    1-5: LGTM! Improved README structure and clarity.

    The new README introduction provides a clear overview of the tool's purpose and acknowledges the original project.


    7-14: LGTM! Comprehensive feature list.

    The feature list effectively communicates the tool's capabilities to potential users.


    33-43: LGTM! Clear installation instructions.

    The step-by-step installation instructions are easy to follow and provide platform-specific commands.


    143-152: LGTM! Excellent advanced usage section.

    The advanced usage section clearly documents additional commands available to users.

    MCP_Server/cli.py (5)

    1-19: LGTM! Well-structured CLI module setup.

    The module imports and logging setup are clean and appropriate. Good job documenting the purpose of the module with a clear docstring.


    20-26: LGTM! Robust version retrieval with fallback.

    The version retrieval function properly handles the case when the package metadata is not available.


    98-150: LGTM! Comprehensive platform-specific path detection.

    The function does an excellent job identifying possible Ableton script paths across different operating systems and versions.

    🧰 Tools
    🪛 Ruff (0.8.2)

    121-121: Use capitalized environment variable PROGRAMFILES instead of ProgramFiles

    (SIM112)


    122-122: Use capitalized environment variable PROGRAMFILES(X86) instead of ProgramFiles(x86)

    (SIM112)


    151-197: LGTM! Robust remote script installation process.

    The installation function effectively handles path detection, validation, and copying of remote script files. Good job including helpful error messages and user instructions.


    198-233: LGTM! Well-structured main function with clear command handling.

    The main function effectively parses arguments and routes to the appropriate command handlers. The fallback to help text when no command is provided is a nice touch.

    MCP_Server/server.py (5)

    8-8: Good addition of comprehensive type hints.

    Adding these type hints improves code readability and enables better static type checking, which helps catch potential errors earlier in development.


    107-110: LGTM! Comprehensive state-modifying command list update.

    The command list has been properly updated to include all the new state-modifying commands, ensuring they get appropriate delay handling and timeouts.


    268-268: Explicit parameter passing improves consistency.

    Explicitly passing an empty dictionary as parameters rather than relying on the default ensures consistent behavior across all command calls.


    694-735: Well-structured device parameter setter with comprehensive validation.

    The implementation properly handles both parameter selection by name or index, with clear error messages for missing or invalid parameters.

    However, the return value should be JSON-serialized:

    -        return {
    +        return json.dumps({
                 "parameter_name": parameter_name,
                 "parameter_index": parameter_index,
                 "value": value
    -        }
    +        }, indent=2)

    1149-1156: Good environment-based configuration for server.

    Using environment variables for host and port configuration improves flexibility and follows best practices for server deployment.

    AbletonMCP_Remote_Script/__init__.py (10)

    10-10: Added math module for necessary calculations.

    The math module is appropriately imported to support logarithmic operations used in EQ parameter conversions and volume calculations.


    233-233: Command list properly updated with new functionalities.

    All the new commands are correctly added to the list of state-modifying operations that should be scheduled on the main thread.


    282-292: Well-structured new command handlers.

    The command processing has been expanded with new handlers for return track creation, send level control, and track volume adjustment, maintaining a consistent pattern with the existing code.


    337-369: Comprehensive device parameter command handlers.

    The command processing has been enriched with detailed handlers for device parameter control, EQ band manipulation, and preset application. The implementation follows a consistent pattern with existing code.


    407-463: Track info method enhanced for return track support.

    The _get_track_info method has been improved to distinguish between regular and return tracks, with appropriate conditional inclusion of track-specific properties.


    469-485: Excellent helper for unified track access.

    The new _get_track_by_index helper function elegantly handles both regular and return tracks with appropriate index range checking, which simplifies many other methods that need to access tracks.


    893-937: Comprehensive device parameter retrieval.

    This implementation does an excellent job of organizing device parameters with their metadata, including special handling for quantized parameters with value items.


    938-1020: Thorough parameter setting with robust validation.

    The _set_device_parameter method includes comprehensive validation for both continuous and quantized parameters, ensuring values are within appropriate ranges before applying them.


    1021-1162: Detailed EQ band parameter control.

    The implementation provides precise control over EQ band parameters with appropriate frequency normalization and handling of different filter types.

    Consider adding direct validation for band_index rather than just using Value error:

    -            if band_index < 0 or band_index > 7:
    -                raise ValueError("Band index must be between 0 and 7")
    +            if not (0 <= band_index <= 7):
    +                raise ValueError("Band index must be between 0 and 7")

    1748-1774: Well-implemented volume-to-dB conversion.

    The _linear_to_db helper provides an accurate conversion from Ableton's internal linear scale to meaningful decibel values, with appropriate handling of edge cases like silent/near-silent volumes.

    @@ -0,0 +1,4 @@
    #!/bin/bash
    cd "$(dirname "$0")"
    Copy link

    Choose a reason for hiding this comment

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

    🛠️ Refactor suggestion

    Add error handling to directory change operation.

    The cd command can fail in specific scenarios, and currently, the script continues execution without verifying if the directory change was successful.

    -cd "$(dirname "$0")"
    +cd "$(dirname "$0")" || { echo "Failed to change directory"; exit 1; }
    🧰 Tools
    🪛 Shellcheck (0.10.0)

    [warning] 2-2: Use 'cd ... || exit' or 'cd ... || return' in case cd fails.

    (SC2164)

    Comment on lines +946 to +1096
    4: {"enabled": False},
    5: {"enabled": False},
    6: {"enabled": False},
    7: {"enabled": False}
    }
    }

    if preset_type not in presets:
    return f"Error: Unknown preset type '{preset_type}'. Available presets: {', '.join(presets.keys())}"

    preset = presets[preset_type]
    results = []

    # Apply preset settings
    for band_index, settings in preset.items():
    # Convert band_index (0-7) to the actual band number (1-8)
    band_number = band_index + 1

    # Enable/disable the band
    if "enabled" in settings:
    enable_param_name = f"{band_number} Filter On A"
    enable_value = 1 if settings["enabled"] else 0
    enable_result = ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": enable_param_name,
    "value": enable_value
    })
    results.append(f"Set {enable_param_name} to {'enabled' if settings['enabled'] else 'disabled'}")

    # Only set other parameters if the band is enabled
    if settings.get("enabled", False):
    # Set frequency if provided
    if "freq" in settings:
    # Convert frequency to normalized value (0-1)
    frequency = settings["freq"]
    if frequency < 20:
    frequency = 20 # Minimum frequency
    if frequency > 20000:
    frequency = 20000 # Maximum frequency

    # Convert to logarithmic scale (approximation)
    import math
    log_min = math.log10(20) # 20 Hz
    log_max = math.log10(20000) # 20 kHz
    log_freq = math.log10(frequency)
    normalized_value = (log_freq - log_min) / (log_max - log_min)

    freq_param_name = f"{band_number} Frequency A"
    freq_result = ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": freq_param_name,
    "value": normalized_value
    })
    results.append(f"Set {freq_param_name} to {frequency} Hz")

    # Set gain if provided
    if "gain" in settings:
    gain_param_name = f"{band_number} Gain A"
    gain_result = ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": gain_param_name,
    "value": settings["gain"]
    })
    results.append(f"Set {gain_param_name} to {settings['gain']} dB")

    # Set Q if provided
    if "q" in settings:
    # Convert Q value to normalized value (0-1)
    normalized_q = settings["q"] / 10.0 # Assuming max Q is around 10
    if normalized_q > 1.0:
    normalized_q = 1.0

    q_param_name = f"{band_number} Resonance A"
    q_result = ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": q_param_name,
    "value": normalized_q
    })
    results.append(f"Set {q_param_name} to {settings['q']}")

    # Set filter type if provided
    if "type" in settings:
    filter_param_name = f"{band_number} Filter Type A"
    filter_result = ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": filter_param_name,
    "value": settings["type"]
    })
    results.append(f"Set {filter_param_name} to {settings['type']}")

    return f"Applied '{preset_type}' preset to EQ Eight"
    except Exception as e:
    logger.error(f"Error applying EQ preset: {str(e)}")
    return f"Error applying EQ preset: {str(e)}"

    Copy link

    Choose a reason for hiding this comment

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

    🛠️ Refactor suggestion

    Fix unused variables in EQ preset application.

    This function has a similar pattern of unused variables that should be addressed.

    Remove these unused variable assignments:

    -                    enable_result = ableton.send_command("set_device_parameter", {
    +                    ableton.send_command("set_device_parameter", {
                             "track_index": track_index,
                             "device_index": device_index,
                             "parameter_name": enable_param_name,
                             "value": enable_value
                         })
    
    -                        freq_result = ableton.send_command("set_device_parameter", {
    +                        ableton.send_command("set_device_parameter", {
                                 "track_index": track_index,
                                 "device_index": device_index,
                                 "parameter_name": freq_param_name,
                                 "value": normalized_value
                             })
    
    -                        gain_result = ableton.send_command("set_device_parameter", {
    +                        ableton.send_command("set_device_parameter", {
                                 "track_index": track_index,
                                 "device_index": device_index,
                                 "parameter_name": gain_param_name, 
                                 "value": settings["gain"]
                             })
    
    -                        q_result = ableton.send_command("set_device_parameter", {
    +                        ableton.send_command("set_device_parameter", {
                                 "track_index": track_index,
                                 "device_index": device_index,
                                 "parameter_name": q_param_name,
                                 "value": normalized_q
                             })
    
    -                        filter_result = ableton.send_command("set_device_parameter", {
    +                        ableton.send_command("set_device_parameter", {
                                 "track_index": track_index,
                                 "device_index": device_index,
                                 "parameter_name": filter_param_name,
                                 "value": filter_index
                             })

    Committable suggestion skipped: line range outside the PR's diff.

    🧰 Tools
    🪛 Ruff (0.8.2)

    1019-1019: Local variable enable_result is assigned to but never used

    Remove assignment to unused variable enable_result

    (F841)


    1046-1046: Local variable freq_result is assigned to but never used

    Remove assignment to unused variable freq_result

    (F841)


    1057-1057: Local variable gain_result is assigned to but never used

    Remove assignment to unused variable gain_result

    (F841)


    1073-1073: Local variable q_result is assigned to but never used

    Remove assignment to unused variable q_result

    (F841)


    1084-1084: Local variable filter_result is assigned to but never used

    Remove assignment to unused variable filter_result

    (F841)

    Comment on lines +736 to +846
    - frequency: The frequency value to set (Hz)
    - gain: The gain value to set (dB)
    - q: The Q factor to set
    - filter_type: The filter type to set (either index or name)

    Returns:
    - String with the result of the operation
    """
    try:
    ableton = get_ableton_connection()

    # First, verify that this is an EQ Eight device
    device_info = ableton.send_command("get_device_parameters", {
    "track_index": track_index,
    "device_index": device_index
    })

    if "device_name" not in device_info or "EQ Eight" not in device_info["device_name"]:
    return f"Error: Device at index {device_index} is not an EQ Eight device"

    # EQ Eight has 8 bands (0-7)
    if band_index < 0 or band_index > 7:
    return f"Error: Band index must be between 0 and 7"

    # Convert band_index (0-7) to the actual band number (1-8)
    band_number = band_index + 1

    # Set parameters as requested
    results = []

    # Set frequency if provided
    if frequency is not None:
    # Convert frequency to normalized value (0-1)
    if frequency < 20:
    frequency = 20 # Minimum frequency
    if frequency > 20000:
    frequency = 20000 # Maximum frequency

    # Convert to logarithmic scale (approximation)
    import math
    log_min = math.log10(20) # 20 Hz
    log_max = math.log10(20000) # 20 kHz
    log_freq = math.log10(frequency)
    normalized_value = (log_freq - log_min) / (log_max - log_min)

    freq_param_name = f"{band_number} Frequency A"
    freq_result = ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": freq_param_name,
    "value": normalized_value
    })
    results.append(f"Set {freq_param_name} to {frequency} Hz")

    # Set gain if provided
    if gain is not None:
    gain_param_name = f"{band_number} Gain A"
    gain_result = ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": gain_param_name,
    "value": gain
    })
    results.append(f"Set {gain_param_name} to {gain} dB")

    # Set Q if provided
    if q is not None:
    # Convert Q value to normalized value (0-1)
    normalized_q = q / 10.0 # Assuming max Q is around 10
    if normalized_q > 1.0:
    normalized_q = 1.0

    q_param_name = f"{band_number} Resonance A"
    q_result = ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": q_param_name,
    "value": normalized_q
    })
    results.append(f"Set {q_param_name} to {q}")

    # Set filter type if provided
    if filter_type is not None:
    filter_param_name = f"{band_number} Filter Type A"
    filter_result = ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": filter_param_name,
    "value": filter_type
    })
    results.append(f"Set {filter_param_name} to {filter_type}")

    if not results:
    return "No parameters were set"

    return "\n".join(results)
    except Exception as e:
    logger.error(f"Error setting EQ band parameters: {str(e)}")
    return f"Error setting EQ band parameters: {str(e)}"

    Copy link

    Choose a reason for hiding this comment

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

    🛠️ Refactor suggestion

    Fix unused variables in EQ band parameter setting.

    The function correctly implements detailed EQ band control with appropriate parameter conversion, but contains multiple unused variables flagged by static analysis.

    Remove these unused variable assignments:

    -            freq_result = ableton.send_command("set_device_parameter", {
    +            ableton.send_command("set_device_parameter", {
                     "track_index": track_index,
                     "device_index": device_index,
                     "parameter_name": freq_param_name,
                     "value": normalized_value
                 })
    
    -            gain_result = ableton.send_command("set_device_parameter", {
    +            ableton.send_command("set_device_parameter", {
                     "track_index": track_index,
                     "device_index": device_index,
                     "parameter_name": gain_param_name,
                     "value": gain
                 })
    
    -            q_result = ableton.send_command("set_device_parameter", {
    +            ableton.send_command("set_device_parameter", {
                     "track_index": track_index,
                     "device_index": device_index,
                     "parameter_name": q_param_name,
                     "value": normalized_q
                 })
    
    -            filter_result = ableton.send_command("set_device_parameter", {
    +            ableton.send_command("set_device_parameter", {
                     "track_index": track_index,
                     "device_index": device_index,
                     "parameter_name": filter_param_name,
                     "value": filter_type
                 })

    Also, line 769 contains an unnecessary f-string:

    -            return f"Error: Band index must be between 0 and 7"
    +            return "Error: Band index must be between 0 and 7"
    📝 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
    @mcp.tool()
    def set_eq_band(ctx: Context, track_index: int, device_index: int, band_index: int,
    frequency: Optional[float] = None, gain: Optional[float] = None,
    q: Optional[float] = None, filter_type: Optional[Union[int, str]] = None) -> str:
    """
    Set parameters for a specific band in an EQ Eight device.
    Parameters:
    - track_index: The index of the track containing the EQ Eight
    - device_index: The index of the EQ Eight device on the track
    - band_index: The index of the band to modify (0-7)
    - frequency: The frequency value to set (Hz)
    - gain: The gain value to set (dB)
    - q: The Q factor to set
    - filter_type: The filter type to set (either index or name)
    Returns:
    - String with the result of the operation
    """
    try:
    ableton = get_ableton_connection()
    # First, verify that this is an EQ Eight device
    device_info = ableton.send_command("get_device_parameters", {
    "track_index": track_index,
    "device_index": device_index
    })
    if "device_name" not in device_info or "EQ Eight" not in device_info["device_name"]:
    return f"Error: Device at index {device_index} is not an EQ Eight device"
    # EQ Eight has 8 bands (0-7)
    if band_index < 0 or band_index > 7:
    return f"Error: Band index must be between 0 and 7"
    # Convert band_index (0-7) to the actual band number (1-8)
    band_number = band_index + 1
    # Set parameters as requested
    results = []
    # Set frequency if provided
    if frequency is not None:
    # Convert frequency to normalized value (0-1)
    if frequency < 20:
    frequency = 20 # Minimum frequency
    if frequency > 20000:
    frequency = 20000 # Maximum frequency
    # Convert to logarithmic scale (approximation)
    import math
    log_min = math.log10(20) # 20 Hz
    log_max = math.log10(20000) # 20 kHz
    log_freq = math.log10(frequency)
    normalized_value = (log_freq - log_min) / (log_max - log_min)
    freq_param_name = f"{band_number} Frequency A"
    freq_result = ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": freq_param_name,
    "value": normalized_value
    })
    results.append(f"Set {freq_param_name} to {frequency} Hz")
    # Set gain if provided
    if gain is not None:
    gain_param_name = f"{band_number} Gain A"
    gain_result = ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": gain_param_name,
    "value": gain
    })
    results.append(f"Set {gain_param_name} to {gain} dB")
    # Set Q if provided
    if q is not None:
    # Convert Q value to normalized value (0-1)
    normalized_q = q / 10.0 # Assuming max Q is around 10
    if normalized_q > 1.0:
    normalized_q = 1.0
    q_param_name = f"{band_number} Resonance A"
    q_result = ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": q_param_name,
    "value": normalized_q
    })
    results.append(f"Set {q_param_name} to {q}")
    # Set filter type if provided
    if filter_type is not None:
    filter_param_name = f"{band_number} Filter Type A"
    filter_result = ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": filter_param_name,
    "value": filter_type
    })
    results.append(f"Set {filter_param_name} to {filter_type}")
    if not results:
    return "No parameters were set"
    return "\n".join(results)
    except Exception as e:
    logger.error(f"Error setting EQ band parameters: {str(e)}")
    return f"Error setting EQ band parameters: {str(e)}"
    @mcp.tool()
    def set_eq_band(ctx: Context, track_index: int, device_index: int, band_index: int,
    frequency: Optional[float] = None, gain: Optional[float] = None,
    q: Optional[float] = None, filter_type: Optional[Union[int, str]] = None) -> str:
    """
    Set parameters for a specific band in an EQ Eight device.
    Parameters:
    - track_index: The index of the track containing the EQ Eight
    - device_index: The index of the EQ Eight device on the track
    - band_index: The index of the band to modify (0-7)
    - frequency: The frequency value to set (Hz)
    - gain: The gain value to set (dB)
    - q: The Q factor to set
    - filter_type: The filter type to set (either index or name)
    Returns:
    - String with the result of the operation
    """
    try:
    ableton = get_ableton_connection()
    # First, verify that this is an EQ Eight device
    device_info = ableton.send_command("get_device_parameters", {
    "track_index": track_index,
    "device_index": device_index
    })
    if "device_name" not in device_info or "EQ Eight" not in device_info["device_name"]:
    return f"Error: Device at index {device_index} is not an EQ Eight device"
    # EQ Eight has 8 bands (0-7)
    if band_index < 0 or band_index > 7:
    return "Error: Band index must be between 0 and 7"
    # Convert band_index (0-7) to the actual band number (1-8)
    band_number = band_index + 1
    # Set parameters as requested
    results = []
    # Set frequency if provided
    if frequency is not None:
    # Convert frequency to normalized value (0-1)
    if frequency < 20:
    frequency = 20 # Minimum frequency
    if frequency > 20000:
    frequency = 20000 # Maximum frequency
    # Convert to logarithmic scale (approximation)
    import math
    log_min = math.log10(20) # 20 Hz
    log_max = math.log10(20000) # 20 kHz
    log_freq = math.log10(frequency)
    normalized_value = (log_freq - log_min) / (log_max - log_min)
    freq_param_name = f"{band_number} Frequency A"
    ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": freq_param_name,
    "value": normalized_value
    })
    results.append(f"Set {freq_param_name} to {frequency} Hz")
    # Set gain if provided
    if gain is not None:
    gain_param_name = f"{band_number} Gain A"
    ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": gain_param_name,
    "value": gain
    })
    results.append(f"Set {gain_param_name} to {gain} dB")
    # Set Q if provided
    if q is not None:
    # Convert Q value to normalized value (0-1)
    normalized_q = q / 10.0 # Assuming max Q is around 10
    if normalized_q > 1.0:
    normalized_q = 1.0
    q_param_name = f"{band_number} Resonance A"
    ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": q_param_name,
    "value": normalized_q
    })
    results.append(f"Set {q_param_name} to {q}")
    # Set filter type if provided
    if filter_type is not None:
    filter_param_name = f"{band_number} Filter Type A"
    ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": filter_param_name,
    "value": filter_type
    })
    results.append(f"Set {filter_param_name} to {filter_type}")
    if not results:
    return "No parameters were set"
    return "\n".join(results)
    except Exception as e:
    logger.error(f"Error setting EQ band parameters: {str(e)}")
    return f"Error setting EQ band parameters: {str(e)}"
    🧰 Tools
    🪛 Ruff (0.8.2)

    769-769: f-string without any placeholders

    Remove extraneous f prefix

    (F541)


    793-793: Local variable freq_result is assigned to but never used

    Remove assignment to unused variable freq_result

    (F841)


    804-804: Local variable gain_result is assigned to but never used

    Remove assignment to unused variable gain_result

    (F841)


    820-820: Local variable q_result is assigned to but never used

    Remove assignment to unused variable q_result

    (F841)


    831-831: Local variable filter_result is assigned to but never used

    Remove assignment to unused variable filter_result

    (F841)

    Comment on lines +847 to +945
    @mcp.tool()
    def set_eq_global(ctx: Context, track_index: int, device_index: int,
    scale: Optional[float] = None, mode: Optional[Union[int, str]] = None,
    oversampling: Optional[bool] = None) -> str:
    """
    Set global parameters for an EQ Eight device.

    Parameters:
    - track_index: The index of the track containing the EQ Eight
    - device_index: The index of the EQ Eight device on the track
    - scale: The scale value to set (0.5 = 50%, 1.0 = 100%, 2.0 = 200%, etc.)
    - mode: The mode to set (either index or name: "Stereo" or "L/R" or "M/S")
    - oversampling: Whether to enable oversampling (true/false)

    Returns:
    - String with the result of the operation
    """
    try:
    ableton = get_ableton_connection()

    # First, verify that this is an EQ Eight device
    device_info = ableton.send_command("get_device_parameters", {
    "track_index": track_index,
    "device_index": device_index
    })

    if "device_name" not in device_info or "EQ Eight" not in device_info["device_name"]:
    return f"Error: Device at index {device_index} is not an EQ Eight device"

    # Set parameters as requested
    results = []

    # Set scale if provided
    if scale is not None:
    scale_result = ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": "Scale",
    "value": scale
    })
    results.append(f"Set Scale to {scale}")

    # Set mode if provided - Note: EQ Eight doesn't seem to have a "Mode" parameter
    # We'll check if there's a parameter with "Mode" in its name
    if mode is not None:
    # Get all parameters to find one that might be the mode
    all_params = device_info.get("parameters", [])
    mode_param = None

    for param in all_params:
    if "Mode" in param.get("name", ""):
    mode_param = param
    break

    if mode_param:
    mode_result = ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": mode_param["name"],
    "value": mode
    })
    results.append(f"Set {mode_param['name']} to {mode}")
    else:
    results.append(f"Warning: Could not find a Mode parameter in EQ Eight")

    # Set oversampling if provided - Note: EQ Eight doesn't seem to have an "Oversampling" parameter
    # We'll check if there's a parameter with "Oversampling" or "Hi Quality" in its name
    if oversampling is not None:
    # Get all parameters to find one that might be oversampling
    all_params = device_info.get("parameters", [])
    oversampling_param = None

    for param in all_params:
    param_name = param.get("name", "")
    if "Oversampling" in param_name or "Hi Quality" in param_name:
    oversampling_param = param
    break

    if oversampling_param:
    # Convert boolean to 0 or 1
    oversampling_value = 1 if oversampling else 0
    oversampling_result = ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": oversampling_param["name"],
    "value": oversampling_value
    })
    results.append(f"Set {oversampling_param['name']} to {'enabled' if oversampling else 'disabled'}")
    else:
    results.append(f"Warning: Could not find an Oversampling parameter in EQ Eight")

    if not results:
    return "No parameters were set"

    return "\n".join(results)
    except Exception as e:
    logger.error(f"Error setting EQ global parameters: {str(e)}")
    return f"Error setting EQ global parameters: {str(e)}"

    Copy link

    Choose a reason for hiding this comment

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

    🛠️ Refactor suggestion

    Fix unused variables in EQ global parameter setting.

    Similar to the previous function, this one contains unused variables and unnecessary f-strings.

    Remove these unused variable assignments:

    -            scale_result = ableton.send_command("set_device_parameter", {
    +            ableton.send_command("set_device_parameter", {
                     "track_index": track_index,
                     "device_index": device_index,
                     "parameter_name": "Scale",
                     "value": scale
                 })
    
    -            mode_result = ableton.send_command("set_device_parameter", {
    +            ableton.send_command("set_device_parameter", {
                     "track_index": track_index,
                     "device_index": device_index,
                     "parameter_name": mode_param["name"],
                     "value": mode
                 })
    
    -            oversampling_result = ableton.send_command("set_device_parameter", {
    +            ableton.send_command("set_device_parameter", {
                     "track_index": track_index,
                     "device_index": device_index,
                     "parameter_name": oversampling_param["name"],
                     "value": oversampling_value
                 })

    Fix unnecessary f-strings:

    -            results.append(f"Warning: Could not find a Mode parameter in EQ Eight")
    +            results.append("Warning: Could not find a Mode parameter in EQ Eight")
    
    -            results.append(f"Warning: Could not find an Oversampling parameter in EQ Eight")
    +            results.append("Warning: Could not find an Oversampling parameter in EQ Eight")
    📝 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
    @mcp.tool()
    def set_eq_global(ctx: Context, track_index: int, device_index: int,
    scale: Optional[float] = None, mode: Optional[Union[int, str]] = None,
    oversampling: Optional[bool] = None) -> str:
    """
    Set global parameters for an EQ Eight device.
    Parameters:
    - track_index: The index of the track containing the EQ Eight
    - device_index: The index of the EQ Eight device on the track
    - scale: The scale value to set (0.5 = 50%, 1.0 = 100%, 2.0 = 200%, etc.)
    - mode: The mode to set (either index or name: "Stereo" or "L/R" or "M/S")
    - oversampling: Whether to enable oversampling (true/false)
    Returns:
    - String with the result of the operation
    """
    try:
    ableton = get_ableton_connection()
    # First, verify that this is an EQ Eight device
    device_info = ableton.send_command("get_device_parameters", {
    "track_index": track_index,
    "device_index": device_index
    })
    if "device_name" not in device_info or "EQ Eight" not in device_info["device_name"]:
    return f"Error: Device at index {device_index} is not an EQ Eight device"
    # Set parameters as requested
    results = []
    # Set scale if provided
    if scale is not None:
    scale_result = ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": "Scale",
    "value": scale
    })
    results.append(f"Set Scale to {scale}")
    # Set mode if provided - Note: EQ Eight doesn't seem to have a "Mode" parameter
    # We'll check if there's a parameter with "Mode" in its name
    if mode is not None:
    # Get all parameters to find one that might be the mode
    all_params = device_info.get("parameters", [])
    mode_param = None
    for param in all_params:
    if "Mode" in param.get("name", ""):
    mode_param = param
    break
    if mode_param:
    mode_result = ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": mode_param["name"],
    "value": mode
    })
    results.append(f"Set {mode_param['name']} to {mode}")
    else:
    results.append(f"Warning: Could not find a Mode parameter in EQ Eight")
    # Set oversampling if provided - Note: EQ Eight doesn't seem to have an "Oversampling" parameter
    # We'll check if there's a parameter with "Oversampling" or "Hi Quality" in its name
    if oversampling is not None:
    # Get all parameters to find one that might be oversampling
    all_params = device_info.get("parameters", [])
    oversampling_param = None
    for param in all_params:
    param_name = param.get("name", "")
    if "Oversampling" in param_name or "Hi Quality" in param_name:
    oversampling_param = param
    break
    if oversampling_param:
    # Convert boolean to 0 or 1
    oversampling_value = 1 if oversampling else 0
    oversampling_result = ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": oversampling_param["name"],
    "value": oversampling_value
    })
    results.append(f"Set {oversampling_param['name']} to {'enabled' if oversampling else 'disabled'}")
    else:
    results.append(f"Warning: Could not find an Oversampling parameter in EQ Eight")
    if not results:
    return "No parameters were set"
    return "\n".join(results)
    except Exception as e:
    logger.error(f"Error setting EQ global parameters: {str(e)}")
    return f"Error setting EQ global parameters: {str(e)}"
    @mcp.tool()
    def set_eq_global(ctx: Context, track_index: int, device_index: int,
    scale: Optional[float] = None, mode: Optional[Union[int, str]] = None,
    oversampling: Optional[bool] = None) -> str:
    """
    Set global parameters for an EQ Eight device.
    Parameters:
    - track_index: The index of the track containing the EQ Eight
    - device_index: The index of the EQ Eight device on the track
    - scale: The scale value to set (0.5 = 50%, 1.0 = 100%, 2.0 = 200%, etc.)
    - mode: The mode to set (either index or name: "Stereo" or "L/R" or "M/S")
    - oversampling: Whether to enable oversampling (true/false)
    Returns:
    - String with the result of the operation
    """
    try:
    ableton = get_ableton_connection()
    # First, verify that this is an EQ Eight device
    device_info = ableton.send_command("get_device_parameters", {
    "track_index": track_index,
    "device_index": device_index
    })
    if "device_name" not in device_info or "EQ Eight" not in device_info["device_name"]:
    return f"Error: Device at index {device_index} is not an EQ Eight device"
    # Set parameters as requested
    results = []
    # Set scale if provided
    if scale is not None:
    ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": "Scale",
    "value": scale
    })
    results.append(f"Set Scale to {scale}")
    # Set mode if provided - Note: EQ Eight doesn't seem to have a "Mode" parameter
    # We'll check if there's a parameter with "Mode" in its name
    if mode is not None:
    all_params = device_info.get("parameters", [])
    mode_param = None
    for param in all_params:
    if "Mode" in param.get("name", ""):
    mode_param = param
    break
    if mode_param:
    ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": mode_param["name"],
    "value": mode
    })
    results.append(f"Set {mode_param['name']} to {mode}")
    else:
    results.append("Warning: Could not find a Mode parameter in EQ Eight")
    # Set oversampling if provided - Note: EQ Eight doesn't seem to have an "Oversampling" parameter
    # We'll check if there's a parameter with "Oversampling" or "Hi Quality" in its name
    if oversampling is not None:
    all_params = device_info.get("parameters", [])
    oversampling_param = None
    for param in all_params:
    param_name = param.get("name", "")
    if "Oversampling" in param_name or "Hi Quality" in param_name:
    oversampling_param = param
    break
    if oversampling_param:
    oversampling_value = 1 if oversampling else 0
    ableton.send_command("set_device_parameter", {
    "track_index": track_index,
    "device_index": device_index,
    "parameter_name": oversampling_param["name"],
    "value": oversampling_value
    })
    results.append(f"Set {oversampling_param['name']} to {'enabled' if oversampling else 'disabled'}")
    else:
    results.append("Warning: Could not find an Oversampling parameter in EQ Eight")
    if not results:
    return "No parameters were set"
    return "\n".join(results)
    except Exception as e:
    logger.error(f"Error setting EQ global parameters: {str(e)}")
    return f"Error setting EQ global parameters: {str(e)}"
    🧰 Tools
    🪛 Ruff (0.8.2)

    881-881: Local variable scale_result is assigned to but never used

    Remove assignment to unused variable scale_result

    (F841)


    902-902: Local variable mode_result is assigned to but never used

    Remove assignment to unused variable mode_result

    (F841)


    910-910: f-string without any placeholders

    Remove extraneous f prefix

    (F541)


    928-928: Local variable oversampling_result is assigned to but never used

    Remove assignment to unused variable oversampling_result

    (F841)


    936-936: f-string without any placeholders

    Remove extraneous f prefix

    (F541)

    Copy link

    @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 (8)
    README.md (8)

    1-2: Optional: Add repository link near the title
    The title and badge clearly communicate the project’s purpose. For even quicker navigation, consider adding a hyperlink to the GitHub repository at the top (e.g., next to the title or badge).


    10-15: Bullet list punctuation consistency
    The feature list mixes punctuation styles (some items end with plain text, one ends with an exclamation). For uniformity, either remove the exclamation from “And much more!” or add consistent punctuation to all items.


    80-80: Heading punctuation style
    The heading “#### Step 6: Start Creating Music with AI!” ends with an exclamation mark, which conflicts with markdownlint MD026. Remove the “!” so it reads:

    #### Step 6: Start Creating Music with AI
    🧰 Tools
    🪛 markdownlint-cli2 (0.17.2)

    80-80: Trailing punctuation in heading
    Punctuation: ':'

    (MD026, no-trailing-punctuation)


    102-102: Trailing punctuation in heading
    The heading “#### For Claude Desktop:” ends with a colon. Drop the trailing “:” to comply with MD026:

    #### For Claude Desktop
    🧰 Tools
    🪛 markdownlint-cli2 (0.17.2)

    102-102: Trailing punctuation in heading
    Punctuation: ':'

    (MD026, no-trailing-punctuation)


    103-103: Trailing punctuation in heading
    Similarly, change “#### For Cursor:” to “#### For Cursor” to adhere to markdown style guidelines (MD026).


    113-113: Trailing punctuation in heading
    The heading “#### For Other AI Tools:” also ends with a colon. Remove the colon to align with MD026:

    #### For Other AI Tools
    🧰 Tools
    🪛 markdownlint-cli2 (0.17.2)

    113-113: Trailing punctuation in heading
    Punctuation: ':'

    (MD026, no-trailing-punctuation)


    107-109: Specify language for fenced code block
    The code block here uses plain backticks without a language specifier. For better syntax highlighting, label it as bash:

    - ```
    + ```bash
      PATH_TO_YOUR_ENVIRONMENT/bin/ableton-mcp server
    
    <details>
    <summary>🧰 Tools</summary>
    
    <details>
    <summary>🪛 markdownlint-cli2 (0.17.2)</summary>
    
    107-107: Fenced code blocks should have a language specified
    null
    
    (MD040, fenced-code-language)
    
    </details>
    
    </details>
    
    ---
    
    `146-148`: **Comma usage in list item**  
    In the bullet  
    ```markdown
    - **"Could not connect to Ableton"**: Ensure Ableton is running and the Remote Script is enabled
    

    add a comma before “and” to separate independent clauses:

    - **"Could not connect to Ableton"**: Ensure Ableton is running, and the Remote Script is enabled
    🧰 Tools
    🪛 LanguageTool

    [uncategorized] ~147-~147: Use a comma before “and” if it connects two independent clauses (unless they are closely connected and short).
    Context: ...to Ableton"**: Ensure Ableton is running and the Remote Script is enabled - **"Remot...

    (COMMA_COMPOUND_SENTENCE_2)

    📜 Review details

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

    📥 Commits

    Reviewing files that changed from the base of the PR and between 0a45861 and 7a0216f.

    📒 Files selected for processing (1)
    • README.md (1 hunks)
    🧰 Additional context used
    🪛 LanguageTool
    README.md

    [grammar] ~21-~21: Did you mean “us”?
    Context: ...ude Desktop, the easiest way to install is with Smithery: ```bash npx -y @smither...

    (TO_VB_IS_IN)


    [uncategorized] ~147-~147: Use a comma before “and” if it connects two independent clauses (unless they are closely connected and short).
    Context: ...to Ableton"**: Ensure Ableton is running and the Remote Script is enabled - **"Remot...

    (COMMA_COMPOUND_SENTENCE_2)

    🪛 markdownlint-cli2 (0.17.2)
    README.md

    80-80: Trailing punctuation in heading
    Punctuation: ':'

    (MD026, no-trailing-punctuation)


    102-102: Trailing punctuation in heading
    Punctuation: ':'

    (MD026, no-trailing-punctuation)


    107-107: Fenced code blocks should have a language specified
    null

    (MD040, fenced-code-language)


    113-113: Trailing punctuation in heading
    Punctuation: ':'

    (MD026, no-trailing-punctuation)

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

    4-6: Clear introduction
    The overview succinctly describes the tool’s purpose and highlights the key enhancements in this fork. Nice and concise.

    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