Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 98 additions & 6 deletions AbletonMCP_Remote_Script/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,12 @@ def _process_command(self, command):
track_index = params.get("track_index", 0)
response["result"] = self._get_track_info(track_index)
# Commands that modify Live's state should be scheduled on the main thread
elif command_type in ["create_midi_track", "set_track_name",
"create_clip", "add_notes_to_clip", "set_clip_name",
elif command_type in ["create_midi_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"]:
"start_playback", "stop_playback", "load_browser_item",
"create_return_track", "duplicate_track",
"capture_midi", "tap_tempo", "set_track_color"]:
# Use a thread-safe approach with a response queue
response_queue = queue.Queue()

Expand Down Expand Up @@ -282,7 +284,21 @@ def main_thread_task():
track_index = params.get("track_index", 0)
item_uri = params.get("item_uri", "")
result = self._load_browser_item(track_index, item_uri)

elif command_type == "create_return_track":
result = self._create_return_track()
elif command_type == "duplicate_track":
track_index = params.get("track_index", 0)
result = self._duplicate_track(track_index)
elif command_type == "capture_midi":
result = self._capture_midi()
elif command_type == "tap_tempo":
result = self._tap_tempo()
elif command_type == "set_track_color":
track_index = params.get("track_index", 0)
color_index = params.get("color_index", -1)
color = params.get("color", -1)
result = self._set_track_color(track_index, color_index, color)

# Put the result in the queue
response_queue.put({"status": "success", "result": result})
except Exception as e:
Expand Down Expand Up @@ -628,15 +644,91 @@ def _stop_playback(self):
"""Stop playing the session"""
try:
self._song.stop_playing()

result = {
"playing": self._song.is_playing
}
return result
except Exception as e:
self.log_message("Error stopping playback: " + str(e))
raise


def _create_return_track(self):
"""Create a return track at the end. Returns its index."""
try:
self._song.create_return_track()
n = len(self._song.return_tracks)
return {
"created": True,
"return_track_index": n - 1,
"return_track_count": n,
}
except Exception as e:
self.log_message("Error creating return track: " + str(e))
raise

def _duplicate_track(self, track_index):
"""Duplicate a track (devices + clips). New track appears at track_index + 1."""
try:
if track_index < 0 or track_index >= len(self._song.tracks):
raise IndexError("Track index out of range")
self._song.duplicate_track(track_index)
return {
"duplicated": True,
"source_track_index": track_index,
"new_track_index": track_index + 1,
"track_count": len(self._song.tracks),
}
except Exception as e:
self.log_message("Error duplicating track: " + str(e))
raise

def _capture_midi(self):
"""Trigger Live's MIDI Capture — recovers MIDI played in the recent past."""
try:
self._song.capture_midi()
return {"captured": True}
except Exception as e:
self.log_message("Error capturing MIDI: " + str(e))
raise

def _tap_tempo(self):
"""Register a tap-tempo tap. Live gradually settles to the tap interval."""
try:
self._song.tap_tempo()
return {"tapped": True, "tempo": self._song.tempo}
except Exception as e:
self.log_message("Error tapping tempo: " + str(e))
raise

def _set_track_color(self, track_index, color_index, color):
"""Set track color.

``color_index`` is the preferred Live 10+ palette index (0-69).
``color`` (RGB integer 0xRRGGBB) is the legacy fallback.
Pass -1 for whichever you're not using.
"""
try:
if track_index < 0 or track_index >= len(self._song.tracks):
raise IndexError("Track index out of range")
track = self._song.tracks[track_index]
color_index = int(color_index)
color = int(color)
if color_index >= 0:
track.color_index = color_index
elif color >= 0:
track.color = color
else:
raise ValueError("Provide color_index (0-69) or color (RGB int).")
return {
"track_index": track_index,
"color_index": getattr(track, 'color_index', None),
"color": getattr(track, 'color', None),
}
except Exception as e:
self.log_message("Error setting track color: " + str(e))
raise
Comment on lines +704 to +730

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Ableton Live Object Model track.color_index valid range palette

💡 Result:

In Ableton Live's Live Object Model (LOM), used in Max for Live and Python Remote Scripts APIs, the Track object has a 'color_index' property, which is an integer representing the index of the track's color in Ableton's predefined color palette [1][2][3]. The valid range for track.color_index is 0 to 69 (inclusive), corresponding to the 70 colors available in the palette as of Live 11 and later [4][5][6]. This is evidenced by Ableton Remote Scripts for controllers like APC64 mapping color_index 0-69 to specific RGB values [5], Python scripts assigning indices up to 69 [6][7], and user reports confirming 70 selectable colors [4][8]. Setting color_index to values outside this range may snap to the nearest palette color or be invalid, similar to how arbitrary RGB values are quantized to the palette when setting the 'color' property (an int in 0x00rrggbb format) [9][2]. Official Cycling '74 LOM docs confirm the property exists but do not specify the range explicitly [2].

Citations:


🏁 Script executed:

# Search for master track or return track color handling in the file
rg -n "master|return" AbletonMCP_Remote_Script/__init__.py | head -30

# Also search for other color-related methods
rg -n "def.*color" AbletonMCP_Remote_Script/__init__.py

Repository: ahujasid/ableton-mcp

Length of output: 1525


🏁 Script executed:

# Check if there are other methods that set colors on master or return tracks
rg -A 10 "def.*color" AbletonMCP_Remote_Script/__init__.py

# Look for any code that sets color on master_track or return_tracks
rg "master_track.*color|return_tracks.*color" AbletonMCP_Remote_Script/__init__.py

Repository: ahujasid/ableton-mcp

Length of output: 553


🏁 Script executed:

# Check what track operations are available and their scope
rg -B 2 -A 15 "def _get_track_info|def _set_track" AbletonMCP_Remote_Script/__init__.py | head -60

# Also check the overall structure of track handling
rg -n "_song\.tracks|_song\.return_tracks|_song\.master_track" AbletonMCP_Remote_Script/__init__.py | head -20

Repository: ahujasid/ableton-mcp

Length of output: 3353


Validate color_index upper bound (0-69).

The color_index parameter is checked for >= 0 but not against the upper bound of 69. Ableton Live's palette supports exactly 70 colors (indices 0–69). A simple range check prevents silent truncation or unexpected behavior when an out-of-range index is passed:

Proposed range guard
             track = self._song.tracks[track_index]
             color_index = int(color_index)
             color = int(color)
             if color_index >= 0:
+                if color_index > 69:
+                    raise ValueError("color_index must be in 0-69")
                 track.color_index = color_index
             elif color >= 0:
                 track.color = color
             else:
                 raise ValueError("Provide color_index (0-69) or color (RGB int).")

Note: This method only indexes self._song.tracks (regular tracks) and does not support return or master tracks, though they also have color_index properties. If master/return track coloring is needed, either expand the lookup or document this scope in the docstring.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AbletonMCP_Remote_Script/__init__.py` around lines 704 - 730, The method
_set_track_color currently only checks color_index >= 0 but not the upper bound;
add a range check such that if color_index is provided it must be between 0 and
69 inclusive (and raise ValueError with a clear message if out of range) before
assigning track.color_index, leaving the existing fallback to track.color
intact; update the validation around color_index/int conversion near the start
of _set_track_color and ensure the error is logged via self.log_message and
re-raised as the existing except block does.


def _get_browser_item(self, uri, path):
"""Get a browser item by URI or path"""
try:
Expand Down
82 changes: 82 additions & 0 deletions MCP_Server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,88 @@ def load_drum_kit(ctx: Context, track_index: int, rack_uri: str, kit_path: str)
logger.error(f"Error loading drum kit: {str(e)}")
return f"Error loading drum kit: {str(e)}"

@mcp.tool()
def create_return_track(ctx: Context) -> str:
"""
Create a return track at the end of the session.

Returns the index of the newly created return track.
"""
try:
ableton = get_ableton_connection()
result = ableton.send_command("create_return_track", {})
return f"Created return track at index {result.get('return_track_index')} (total returns: {result.get('return_track_count')})"
except Exception as e:
logger.error(f"Error creating return track: {str(e)}")
return f"Error creating return track: {str(e)}"

@mcp.tool()
def duplicate_track(ctx: Context, track_index: int) -> str:
"""
Duplicate a track (devices + clips). The new track appears at track_index + 1.

Parameters:
- track_index: The index of the track to duplicate
"""
try:
ableton = get_ableton_connection()
result = ableton.send_command("duplicate_track", {"track_index": track_index})
return f"Duplicated track {track_index} -> {result.get('new_track_index')} (total tracks: {result.get('track_count')})"
except Exception as e:
logger.error(f"Error duplicating track: {str(e)}")
return f"Error duplicating track: {str(e)}"

@mcp.tool()
def capture_midi(ctx: Context) -> str:
"""
Trigger Live's MIDI Capture, which recovers MIDI played in the recent past
on the armed track.
"""
try:
ableton = get_ableton_connection()
ableton.send_command("capture_midi", {})
return "Captured MIDI"
except Exception as e:
logger.error(f"Error capturing MIDI: {str(e)}")
return f"Error capturing MIDI: {str(e)}"

@mcp.tool()
def tap_tempo(ctx: Context) -> str:
"""
Register a tap-tempo tap. Live gradually settles to the tap interval.
"""
try:
ableton = get_ableton_connection()
result = ableton.send_command("tap_tempo", {})
return f"Tap registered (current tempo: {result.get('tempo')})"
except Exception as e:
logger.error(f"Error tapping tempo: {str(e)}")
return f"Error tapping tempo: {str(e)}"

@mcp.tool()
def set_track_color(ctx: Context, track_index: int, color_index: int = -1, color: int = -1) -> str:
"""
Set the color of a track.

Parameters:
- track_index: The index of the track
- color_index: Live's palette index (0-69). Preferred. Pass -1 to skip.
- color: Raw RGB integer (0xRRGGBB). Legacy fallback. Pass -1 to skip.

Provide either color_index or color (not both).
"""
try:
ableton = get_ableton_connection()
result = ableton.send_command("set_track_color", {
"track_index": track_index,
"color_index": color_index,
"color": color,
})
return f"Track {track_index} color set (color_index: {result.get('color_index')}, color: {result.get('color')})"
except Exception as e:
logger.error(f"Error setting track color: {str(e)}")
return f"Error setting track color: {str(e)}"

# Main execution
def main():
"""Run the MCP server"""
Expand Down