Expand MCP to full AbletonOSC coverage: add 50+ tools, remove broken endpoints

New modules: return_track (11 tools), browser (6 tools), groove pool (3 tools).

Extended: clip (notes_extended, apply_note_modifications, automation envelopes,
warp markers, groove, quantize, velocity_amount, ram_mode), device (is_active,
chains, drum pads, chain parameters, randomize_macros), track (full I/O routing,
peak meters, duplicate_device, clips/is_playing, devices/can_have_chains),
scene (num_clips, clip_slots, fire_as_selected), view (show_clip/device_detail,
focus_browser, focused_document_view), song (jump_by, cue navigation,
return_track list, corrected scene_names address and cue_points parsing).

Removed 12 tools that called OSC addresses absent from this AbletonOSC build
(root_note, scale_name, ableton_link, cue_point/jump, set_or_delete_cue,
export/structure, midimap/map_cc, api log_level, view/set/selected_device).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 12:56:19 +02:00
parent 7260e9c43d
commit 5c274b9335
12 changed files with 706 additions and 260 deletions
+186 -158
View File
@@ -4,9 +4,9 @@ A full-featured [Model Context Protocol (MCP)](https://modelcontextprotocol.io/)
## Overview
**ableton-mcp** bridges AI assistants and Ableton Live by exposing **124 MCP tools** covering every major area of the Live API: transport control, track mixing, clip launching and editing, MIDI note manipulation, device parameter control, scene management, view selection, and real-time property listeners.
**ableton-mcp** bridges AI assistants and Ableton Live by exposing **160+ MCP tools** covering the full AbletonOSC API: transport, mixing, clips, MIDI notes, devices, scenes, return tracks, browser, groove pool, view navigation, and real-time listeners.
The server communicates with Ableton Live via [AbletonOSC](https://github.com/ideoforms/AbletonOSC) — a MIDI Remote Script that runs inside Live and exposes its internal API over OSC/UDP.
The server communicates with Ableton Live via AbletonOSC — a MIDI Remote Script that runs inside Live and exposes its internal API over OSC/UDP.
```
Claude (AI) ──MCP tools──▶ ableton-mcp ──OSC/UDP──▶ AbletonOSC ──Live Object Model──▶ Ableton Live
@@ -17,8 +17,8 @@ Claude (AI) ──MCP tools──▶ ableton-mcp ──OSC/UDP──▶ AbletonO
## Requirements
- **Ableton Live 11 or later**
- **Python 3.10+** (tested with 3.10.6 via pyenv)
- **AbletonOSC** installed as a MIDI Remote Script (see below)
- **Python 3.10+**
- **AbletonOSC** installed as a MIDI Remote Script
---
@@ -26,101 +26,77 @@ Claude (AI) ──MCP tools──▶ ableton-mcp ──OSC/UDP──▶ AbletonO
### 1. Install AbletonOSC in Ableton Live
1. Download or clone [AbletonOSC](https://github.com/ideoforms/AbletonOSC)
2. Copy the `AbletonOSC` folder into your Ableton User Library Remote Scripts directory:
1. Copy the `AbletonOSC` folder into your Ableton User Library Remote Scripts directory:
- **Windows:** `%USERPROFILE%\Documents\Ableton\User Library\Remote Scripts`
- **macOS:** `~/Music/Ableton/User Library/Remote Scripts`
3. Restart Ableton Live
4. Open **Preferences → Link, Tempo & MIDI → MIDI** and set one of the Control Surface slots to **AbletonOSC**
2. Restart Ableton Live
3. Open **Preferences → Link, Tempo & MIDI → MIDI** and set one of the Control Surface slots to **AbletonOSC**
AbletonOSC will now listen on **UDP port 11000** and reply on **port 11001**.
AbletonOSC listens on **UDP port 11000** and replies on **port 11001**.
### 2. Install ableton-mcp
```bash
# Clone the repo (rename the folder after cloning)
git clone https://github.com/your-user/ableton-mcp
cd ableton-mcp
# Create and activate a virtual environment
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # macOS/Linux
# Install
pip install -e .
```
### 3. Configure Claude Desktop
### 3. Configure Claude Code
Add the server to your Claude Desktop configuration file:
**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
Add to `~/.claude/settings.json`:
```json
{
"mcpServers": {
"ableton": {
"command": "C:\\path\\to\\ableton-mcp\\.venv\\Scripts\\ableton-mcp.exe"
"ableton-mcp": {
"command": "C:\\path\\to\\ableton-mcp\\.venv\\Scripts\\ableton-mcp.exe",
"args": []
}
}
}
```
Restart Claude Desktop. The Ableton Live tools will appear automatically when a Live session is open.
Restart Claude Code. Tools appear automatically when a Live session is open.
---
## Configuration
All settings can be overridden via environment variables:
| Variable | Default | Description |
|---|---|---|
| `ABLETON_HOST` | `127.0.0.1` | AbletonOSC host address |
| `ABLETON_SEND_PORT` | `11000` | Port to send OSC commands to Live |
| `ABLETON_RECEIVE_PORT` | `11001` | Port to receive OSC replies from Live |
| `ABLETON_SEND_PORT` | `11000` | Port to send OSC to Live |
| `ABLETON_RECEIVE_PORT` | `11001` | Port to receive OSC from Live |
| `ABLETON_TIMEOUT` | `5.0` | Query timeout in seconds |
Example (Claude Desktop config with custom port):
```json
{
"mcpServers": {
"ableton": {
"command": "C:\\path\\to\\.venv\\Scripts\\ableton-mcp.exe",
"env": {
"ABLETON_TIMEOUT": "10.0"
}
}
}
}
```
---
## Tool Reference
### System (7 tools)
### System (5 tools)
| Tool | Description |
|---|---|
| `system_test_connection` | Ping AbletonOSC; returns `{connected, latency_ms}` |
| `system_get_version` | Get Ableton Live version `{major, minor}` |
| `system_get_cpu_usage` | Get average CPU process usage (0.01.0) |
| `system_get_cpu_usage` | Average CPU process usage (0.01.0) |
| `system_show_message` | Display a message in the Ableton status bar |
| `system_get_log_level` | Get current AbletonOSC log level |
| `system_set_log_level` | Set log level: `debug` / `info` / `warning` / `error` / `critical` |
| `system_reload_api` | Hot-reload all AbletonOSC modules |
---
### Song / Transport (48 tools)
### Song / Transport (38 tools)
#### Playback
| Tool | Description |
|---|---|
| `song_get_state` | Full snapshot: tempo, is_playing, loop, time sig, track/scene counts, groove, link, scale… |
| `song_get_state` | Full snapshot: tempo, is_playing, loop, time sig, track/scene/return counts, groove |
| `song_start_playing` | Start playback from current position |
| `song_stop_playing` | Stop playback |
| `song_continue_playing` | Resume without resetting to start |
@@ -132,6 +108,9 @@ Example (Claude Desktop config with custom port):
|---|---|
| `song_get_tempo` / `song_set_tempo` | Get/set BPM (20300) |
| `song_get_time` / `song_set_time` | Get/jump playhead position in beats |
| `song_jump_by` | Move playhead by ±N beats relative to current position |
| `song_jump_to_next_cue` | Jump to next cue point |
| `song_jump_to_prev_cue` | Jump to previous cue point |
| `song_get_time_signature` / `song_set_time_signature` | Get/set numerator + denominator |
#### Loop
@@ -163,38 +142,20 @@ Example (Claude Desktop config with custom port):
|---|---|
| `song_undo` / `song_redo` | Undo/redo |
#### Tracks
#### Tracks & Scenes
| Tool | Description |
|---|---|
| `song_get_tracks` | List all tracks with index and name |
| `song_create_audio_track` | Create audio track at index |
| `song_create_midi_track` | Create MIDI track at index |
| `song_create_return_track` | Create return/aux track |
| `song_delete_track` / `song_delete_return_track` | Delete track |
| `song_duplicate_track` | Duplicate a track |
#### Scenes
| Tool | Description |
|---|---|
| `song_get_return_tracks` | List all return tracks with index and name |
| `song_create_audio_track` / `song_create_midi_track` / `song_create_return_track` | Create tracks |
| `song_delete_track` / `song_delete_return_track` / `song_duplicate_track` | Delete/duplicate tracks |
| `song_get_scenes` | List all scenes with index and name |
| `song_create_scene` / `song_delete_scene` / `song_duplicate_scene` | Scene CRUD |
| `song_capture_and_insert_scene` | Capture session state as a new scene |
#### Cue Points
| Tool | Description |
|---|---|
| `song_get_cue_points` | List all cue points |
| `song_jump_to_cue` | Jump to cue by index or name |
| `song_add_or_delete_cue` | Toggle cue at current playhead |
| `song_set_cue_name` | Rename a cue point |
#### Scale & Link
| Tool | Description |
|---|---|
| `song_set_root_note` | Set root note (0=C … 11=B) |
| `song_set_scale_name` | Set scale name e.g. `"Major"`, `"Dorian"` |
| `song_set_ableton_link` | Enable/disable Ableton Link |
| `song_export_structure` | Export full session structure to JSON file |
| `song_get_cue_points` | List all cue points `{index, name, time}` |
---
@@ -202,52 +163,87 @@ Example (Claude Desktop config with custom port):
| Tool | Description |
|---|---|
| `track_get_info` | All track properties: name, volume, pan, mute, solo, arm, type, color, monitoring, meter slots, device count |
| `track_get_info` | All properties: name, volume, pan, mute, solo, arm, type, color, monitoring, slots, device count |
| `track_set_volume` | Volume (0.0=−∞, 0.85≈0 dB, 1.0=+6 dB) |
| `track_set_pan` | Panning (1.0=left, 0.0=center, +1.0=right) |
| `track_set_mute` | Mute / unmute |
| `track_set_solo` | Solo / unsolo |
| `track_set_arm` | Arm / disarm for recording |
| `track_set_mute` / `track_set_solo` / `track_set_arm` | Mute/solo/arm |
| `track_get_send` / `track_set_send` | Get/set send level to a return track |
| `track_set_name` | Rename a track |
| `track_set_color` | Set track color by palette index (069) |
| `track_stop_clips` | Stop all clips on a track |
| `track_set_monitoring` | Monitoring state: 0=Auto, 1=In, 2=Off |
| `track_set_monitoring` | Monitoring: 0=Auto, 1=In, 2=Off |
| `track_set_fold` | Fold/unfold a group track |
| `track_get_clips` | List session clips: index, name, length, color |
| `track_get_arrangement_clips` | List arrangement clips: index, name, length, start_time |
| `track_get_devices` | List devices: index, name, type, class_name |
| `track_get_meter` | Output meter levels: left, right, level |
| `track_delete_device` | Remove a device from the chain |
| `track_get_available_input_routing_types` | List available input routing options |
| `track_set_input_routing_type` / `track_set_output_routing_type` | Set I/O routing |
| `track_get_clips` | Session clips: index, name, length, color, is_playing |
| `track_get_arrangement_clips` | Arrangement clips: index, name, length, start_time |
| `track_get_devices` | Devices: index, name, type, class_name, can_have_chains |
| `track_get_meter` | Output meter: level, left, right, peak_left, peak_right |
| `track_delete_device` / `track_duplicate_device` | Remove/duplicate device |
| `track_get_input_routing` | Current input type + channel + available options |
| `track_set_input_routing_type` / `track_set_input_routing_channel` | Set input routing |
| `track_get_output_routing` | Current output type + channel + available options |
| `track_set_output_routing_type` / `track_set_output_routing_channel` | Set output routing |
---
### Clips (19 tools)
### Return Tracks (11 tools)
| Tool | Description |
|---|---|
| `clip_fire` | Launch a clip |
| `clip_stop` | Stop a playing clip |
| `clip_get_info` | All clip properties: name, length, playing state, loop, markers, pitch, gain, launch mode, warp, color |
| `clip_set_name` | Rename a clip |
| `clip_set_color` | Set clip color by palette index (069) |
| `clip_set_gain` | Set clip gain in dB |
| `clip_set_muted` | Mute/unmute a clip |
| `clip_set_looping` | Enable/disable loop |
| `clip_set_loop_points` | Set loop start + end in beats |
| `clip_set_markers` | Set start + end markers in beats |
| `clip_set_pitch` | Transpose: semitones (48 to +48) and cents (50 to +50) |
| `return_track_get_info` | All properties: name, mute, solo, color, volume, panning |
| `return_track_set_volume` / `return_track_set_panning` | Volume/pan |
| `return_track_set_mute` / `return_track_set_solo` | Mute/solo |
| `return_track_set_name` / `return_track_set_color` | Rename/recolor |
| `return_track_get_send` / `return_track_set_send` | Inter-return sends |
| `return_track_get_meter` | Output meter: level, left, right |
| `return_track_get_devices` | List devices on the return track |
---
### Clips (31 tools)
#### Playback & Properties
| Tool | Description |
|---|---|
| `clip_fire` / `clip_stop` | Launch/stop a clip |
| `clip_get_info` | All properties: name, length, playing state, loop, markers, pitch, gain, launch mode |
| `clip_set_name` / `clip_set_color` | Rename/recolor |
| `clip_set_gain` | Clip gain in dB |
| `clip_set_muted` | Mute/unmute |
| `clip_set_looping` / `clip_set_loop_points` | Enable loop and set start/end |
| `clip_set_markers` | Set clip start/end markers |
| `clip_set_pitch` | Transpose: semitones (48…+48) and cents (50…+50) |
| `clip_set_warp_mode` | Warp mode: 0=Beats, 1=Tones, 2=Texture, 3=Re-Pitch, 4=Complex, 5=Complex Pro |
| `clip_set_warping` | Enable/disable warping |
| `clip_set_launch_mode` | Launch mode: 0=Trigger, 1=Gate, 2=Toggle, 3=Repeat |
| `clip_set_launch_quantization` | Per-clip launch quantization (014) |
| `clip_set_legato` | Enable/disable legato mode |
| `clip_duplicate_loop` | Double loop length by duplicating content |
| `clip_get_notes` | Get MIDI notes (filterable by pitch range + time range) |
| `clip_add_notes` | Add MIDI notes: `[{pitch, start, duration, velocity, mute}, …]` |
| `clip_remove_notes` | Remove MIDI notes by pitch/time range |
| `clip_set_launch_mode` | 0=Trigger, 1=Gate, 2=Toggle, 3=Repeat |
| `clip_set_launch_quantization` | Per-clip quantization (014) |
| `clip_set_legato` | Enable/disable legato |
| `clip_set_velocity_amount` | Scale note velocities (0.01.0) |
| `clip_set_ram_mode` | Enable/disable RAM mode for audio clips |
| `clip_duplicate_loop` | Double loop by duplicating content |
| `clip_quantize` | Quantize MIDI notes (quantization enum + strength 0.01.0) |
#### MIDI Notes
| Tool | Description |
|---|---|
| `clip_get_notes` | Notes filtered by pitch/time range: `{pitch, start, duration, velocity, mute}` |
| `clip_get_notes_extended` | Extended notes with note_id, release_velocity, probability |
| `clip_add_notes` | Add notes: `[{pitch, start, duration, velocity, mute}, …]` |
| `clip_apply_note_modifications` | Modify existing notes by note_id |
| `clip_remove_notes` | Remove notes in pitch/time range |
#### Automation Envelopes
| Tool | Description |
|---|---|
| `clip_get_automation_envelope` | Get breakpoints `{time, value}` for a device param |
| `clip_create_automation_envelope` | Create envelope for a device param |
| `clip_clear_envelope` | Clear one param's envelope |
| `clip_clear_all_envelopes` | Clear all envelopes in a clip |
#### Warp & Groove
| Tool | Description |
|---|---|
| `clip_get_warp_markers` | Warp markers: `{beat_time, sample_time}` |
| `clip_get_groove` / `clip_set_groove` | Get/assign groove by name |
---
@@ -255,52 +251,84 @@ Example (Claude Desktop config with custom port):
| Tool | Description |
|---|---|
| `clip_slot_get_info` | Slot state: has_clip, is_playing, is_triggered, will_record, is_group_slot |
| `clip_slot_fire` | Trigger a slot (launches clip or starts recording if empty) |
| `clip_slot_stop` | Stop a slot |
| `clip_slot_create_clip` | Create a new empty MIDI clip with a given length in beats |
| `clip_slot_get_info` | State: has_clip, is_playing, is_triggered, will_record, is_group_slot |
| `clip_slot_fire` / `clip_slot_stop` | Trigger/stop a slot |
| `clip_slot_create_clip` | Create empty MIDI clip with given length in beats |
| `clip_slot_delete_clip` | Delete the clip in a slot |
| `clip_slot_duplicate_to` | Copy clip from one slot to another |
| `clip_slot_set_stop_button` | Enable/disable the stop button for a slot |
| `clip_slot_duplicate_to` | Copy clip to another slot |
| `clip_slot_set_stop_button` | Enable/disable slot stop button |
---
### Scenes (7 tools)
### Scenes (9 tools)
| Tool | Description |
|---|---|
| `scene_fire` | Launch a scene by index |
| `scene_fire_selected` | Launch the currently selected scene |
| `scene_get_info` | All scene properties: name, color, tempo, tempo_enabled, time sig, is_empty, is_triggered |
| `scene_set_name` | Rename a scene |
| `scene_set_color` | Set scene color by palette index (069) |
| `scene_set_tempo` | Override tempo for a scene (with enable toggle) |
| `scene_set_time_signature` | Override time signature for a scene |
| `scene_fire_as_selected` | Fire scene respecting follow actions |
| `scene_get_info` | All properties: name, color, tempo, time sig, is_empty, is_triggered |
| `scene_get_num_clips` | Number of clips in the scene |
| `scene_get_clip_slots` | Per-slot occupancy: `{slot_index, has_clip}` |
| `scene_set_name` / `scene_set_color` | Rename/recolor |
| `scene_set_tempo` | Override tempo (with enable toggle) |
| `scene_set_time_signature` | Override time signature |
---
### Devices (6 tools)
### Devices (15 tools)
| Tool | Description |
|---|---|
| `device_get_info` | Device info: name, type (audio_effect / instrument / midi_effect), class_name, num_parameters |
| `device_get_parameters` | All parameters: index, name, value, min, max, is_quantized |
| `device_get_parameter` | Single parameter: name, value, value_string (display value) |
| `device_set_parameter` | Set a parameter value (normalized 0.01.0) |
| `device_set_parameters_bulk` | Set multiple parameters in one call: `[{index, value}, …]` |
| `device_map_midi_cc` | Map a MIDI CC to a device parameter |
| `device_get_info` | Name, type, class_name, num_parameters, is_active |
| `device_get_is_active` / `device_set_is_active` | Get/set bypass state |
| `device_get_parameters` | All params: index, name, value, min, max, default_value, is_quantized |
| `device_get_parameter` | Single param: name, value, min, max, default_value, value_string |
| `device_set_parameter` | Set a parameter value (0.01.0 normalized) |
| `device_set_parameters_bulk` | Set multiple params: `[{index, value}, …]` |
| `device_randomize_macros` | Randomize rack macro knobs |
| `device_get_chains` | Rack chains: index, name, num_devices, device names |
| `device_get_drum_pads` | Drum Rack pads: index, name, note, mute, solo |
| `device_set_drum_pad_mute` / `device_set_drum_pad_solo` | Mute/solo a drum pad |
| `device_chain_get_parameter` | Get a param value inside a rack chain |
| `device_chain_set_parameter` | Set a param value inside a rack chain |
---
### View / Selection (5 tools)
### View / Navigation (8 tools)
| Tool | Description |
|---|---|
| `view_get_selection` | Current selection: selected_track, selected_scene, selected_clip, selected_device |
| `view_set_selected_track` | Select a track in the Ableton UI |
| `view_set_selected_scene` | Select a scene in the Ableton UI |
| `view_set_selected_clip` | Select a clip in the Ableton UI |
| `view_set_selected_device` | Select a device in the Ableton UI |
| `view_get_selection` | Selected track, scene, clip, and device |
| `view_get_focused_document_view` | Active view name (e.g. `'Session'`, `'Arranger'`) |
| `view_set_selected_track` | Select a track |
| `view_set_selected_scene` | Select a scene |
| `view_set_selected_clip` | Select a clip |
| `view_show_clip_detail` | Show clip detail panel |
| `view_show_device_detail` | Show device detail panel |
| `view_focus_browser` | Focus the browser panel |
---
### Browser (6 tools)
| Tool | Description |
|---|---|
| `browser_get_items` | List items in a category: `audio_effects` / `instruments` / `midi_effects` / `samples` / `sounds` / `clips` / `packs` / `plugins` |
| `browser_load_item` | Load an item into the selected track/slot (pass path parts as args) |
| `browser_preview_item` | Preview a sample or preset |
| `browser_stop_preview` | Stop browser preview playback |
| `browser_get_hotswap_target` | Name of device targeted for hotswap |
| `browser_begin_hotswap` | Begin hotswap mode for a device |
---
### Groove Pool (3 tools)
| Tool | Description |
|---|---|
| `groove_get_all` | List all groove names in the groove pool |
| `groove_get_amount` | Get groove intensity (0.01.0) by name |
| `groove_set_amount` | Set groove intensity (0.01.0) by name |
---
@@ -310,7 +338,7 @@ Subscribe to property changes from Ableton Live and poll them at any time.
| Tool | Description |
|---|---|
| `listener_start` | Register a listener for a property; returns the OSC response address |
| `listener_start` | Register a listener for a property |
| `listener_stop` | Unregister a listener |
| `listener_get_events` | Drain queued events (up to `max_events`); each event is a list of OSC args |
@@ -328,10 +356,6 @@ Subscribe to property changes from Ableton Live and poll them at any time.
| `clip` | any read/write property | requires `track_index` + `clip_index` |
| `clip_slot` | any property | requires `track_index` + `clip_index` |
**Example usage with Claude:**
> "Watch the beat and tell me the bar number every 4 bars"
> Claude calls `listener_start("song", "beat")`, then periodically `listener_get_events("song", "beat")`
---
## Example Prompts
@@ -342,14 +366,17 @@ Play the song from bar 3 at 128 BPM with the metronome on.
Create a 4-bar MIDI clip on track 0, slot 1, and add a C minor chord
(C3, Eb3, G3) on beats 1 and 3 with velocity 90.
Set every device on track 2 to a random-ish sound:
randomize all parameters whose name contains "Filter".
Load a reverb from the browser onto the first return track and set the
wet/dry to 50%.
Loop scene 4 forever: set scene 4's clips to looping and fire the scene.
Get all drum pad names on track 0's Drum Rack, then mute every pad
that isn't a kick or snare.
Solo track 3, mute tracks 1 and 2, and set track 3's reverb send to 0.4.
Solo track 3, set its reverb send to 0.4, and watch the output meter
until it clips.
Name all empty scenes "—" and delete any track with no clips.
Set the groove pool's first groove to 80% intensity and assign it to
all clips on track 1.
```
---
@@ -358,42 +385,43 @@ Name all empty scenes "—" and delete any track with no clips.
```
src/ableton_mcp/
├── server.py # FastMCP app; registers all tool modules
├── osc_client.py # Thread-safe OSCClient singleton
│ # cmd() fire-and-forget
│ # query() synchronous request/response
│ # start_listener() / drain_listener() real-time events
├── config.py # Port/host/timeout from env vars
├── server.py # FastMCP app; registers all tool modules
├── osc_client.py # Thread-safe OSCClient singleton
# cmd() fire-and-forget
# query() synchronous request/response
# start_listener() / drain_listener() real-time events
├── config.py # Port/host/timeout from env vars
└── tools/
├── song.py # Transport, tempo, loop, scenes, tracks, cues, scale, Link
├── track.py # Mixing, routing, devices list, meter
├── clip.py # Playback, MIDI notes, pitch, warp, loop, launch
├── clip_slot.py # Slot-level create/fire/delete/duplicate
├── scene.py # Scene fire, tempo/time-sig overrides
├── device.py # Parameter get/set (individual + bulk), MIDI CC mapping
├── view.py # Selected track/scene/clip/device
├── system.py # Connectivity, version, CPU, status bar, logging
── listener.py # Real-time property change subscriptions
├── song.py # Transport, tempo, loop, scenes, tracks, cues
├── track.py # Mixing, routing, devices list, meter
├── clip.py # Playback, MIDI notes, pitch, warp, loop, launch, envelopes
├── clip_slot.py # Slot-level create/fire/delete/duplicate
├── scene.py # Scene fire, info, clip slots, tempo/time-sig overrides
├── device.py # Parameters (individual + bulk), chains, drum pads
├── return_track.py# Return/aux track mixing and devices
├── browser.py # Content library browsing, load, preview, hotswap
── groove.py # Groove pool listing and amount control
├── view.py # Selected track/scene/clip, panel navigation
├── system.py # Connectivity, version, CPU, status bar
└── listener.py # Real-time property change subscriptions
```
**OSC communication** uses `python-osc` directly against AbletonOSC's full 200+ endpoint API. The `OSCClient` singleton runs a `ThreadingOSCUDPServer` on the receive port in a daemon thread. Queries use `threading.Event` for synchronous blocking with configurable timeout. Listeners store events in per-address `collections.deque` instances for thread-safe polling.
---
## Troubleshooting
**`system_test_connection` returns `connected: false`**
- Confirm AbletonOSC is enabled in Ableton → Preferences → Control Surfaces
- Make sure a Live project is open (AbletonOSC only activates with an open set)
- Check that no firewall is blocking UDP 11000/11001 on localhost
- Confirm AbletonOSC is enabled in Preferences → Control Surfaces
- A Live project must be open (AbletonOSC only activates with an open set)
- Check that no firewall blocks UDP 11000/11001 on localhost
**Timeout errors on queries**
**Timeout errors**
- Increase `ABLETON_TIMEOUT` env var (default 5 s)
- Some properties (e.g. `song_get_state`) issue many sequential OSC queries; slow machines may need a higher timeout
- `song_get_state` issues many sequential queries; slow machines may need 10 s+
**`No module named 'ableton_mcp'`**
- Make sure you installed with `pip install -e .` inside the correct `.venv`
- The Claude Desktop config `command` path must point to the `.venv` executable, not the system Python
- Install with `pip install -e .` inside the correct `.venv`
- The MCP config `command` path must point to the `.venv` executable
---
+6
View File
@@ -12,6 +12,9 @@ from ableton_mcp.tools import (
device,
view,
listener,
return_track,
browser,
groove,
)
mcp = FastMCP("ableton-mcp")
@@ -25,6 +28,9 @@ scene.register(mcp)
device.register(mcp)
view.register(mcp)
listener.register(mcp)
return_track.register(mcp)
browser.register(mcp)
groove.register(mcp)
def main() -> None:
+45
View File
@@ -0,0 +1,45 @@
"""MCP tools for Ableton Live browser / content library."""
from ableton_mcp.osc_client import get_client
_CATEGORIES = ("audio_effects", "instruments", "midi_effects", "samples", "sounds", "clips", "packs", "plugins")
def register(mcp):
@mcp.tool()
def browser_get_items(category: str) -> list:
"""List items in a browser category.
category: audio_effects | instruments | midi_effects | samples | sounds | clips | packs | plugins.
Returns a flat list of item path strings."""
if category not in _CATEGORIES:
raise ValueError(f"category must be one of: {', '.join(_CATEGORIES)}")
result = get_client().query(f"/live/browser/get/{category}")
return list(result)
@mcp.tool()
def browser_load_item(*path_parts: str) -> None:
"""Load a browser item into the currently selected track/slot.
Pass the item path as individual strings, e.g. browser_load_item('Instruments', 'Drift', 'Init')."""
get_client().cmd("/live/browser/load_item", *path_parts)
@mcp.tool()
def browser_preview_item(*path_parts: str) -> None:
"""Preview a browser item (plays a sample or preset preview)."""
get_client().cmd("/live/browser/preview_item", *path_parts)
@mcp.tool()
def browser_stop_preview() -> None:
"""Stop any currently playing browser preview."""
get_client().cmd("/live/browser/stop_preview")
@mcp.tool()
def browser_get_hotswap_target() -> str:
"""Get the name of the device currently targeted for hotswap (empty if none)."""
result = get_client().query("/live/browser/get/hotswap_target")
return result[0] if result else ""
@mcp.tool()
def browser_begin_hotswap(track_index: int, device_index: int) -> None:
"""Begin hotswap mode for a device — subsequent browser_load_item calls replace it."""
get_client().cmd("/live/browser/begin_hotswap", track_index, device_index)
+138
View File
@@ -114,11 +114,36 @@ def register(mcp):
"""Enable or disable legato mode for a clip."""
get_client().cmd("/live/clip/set/legato", track_index, clip_index, int(legato))
@mcp.tool()
def clip_set_velocity_amount(track_index: int, clip_index: int, amount: float) -> None:
"""Set clip velocity amount (0.01.0). Scales note velocities."""
get_client().cmd("/live/clip/set/velocity_amount", track_index, clip_index, amount)
@mcp.tool()
def clip_set_ram_mode(track_index: int, clip_index: int, enabled: bool) -> None:
"""Enable or disable RAM mode for an audio clip."""
get_client().cmd("/live/clip/set/ram_mode", track_index, clip_index, int(enabled))
@mcp.tool()
def clip_duplicate_loop(track_index: int, clip_index: int) -> None:
"""Double the clip loop length by duplicating its content."""
get_client().cmd("/live/clip/duplicate_loop", track_index, clip_index)
@mcp.tool()
def clip_quantize(
track_index: int,
clip_index: int,
quantization: int = 5,
strength: float = 1.0,
) -> None:
"""Quantize MIDI notes in a clip.
quantization: same enum as global (4=1 bar, 7=1/4, 9=1/8, 11=1/16, etc.)
strength: 0.01.0 (1.0 = fully quantized)."""
get_client().cmd(
"/live/clip/quantize",
track_index, clip_index, quantization, strength,
)
# --- MIDI Notes ---
@mcp.tool()
@@ -148,6 +173,37 @@ def register(mcp):
})
return notes
@mcp.tool()
def clip_get_notes_extended(
track_index: int,
clip_index: int,
pitch_start: int = 0,
pitch_span: int = 127,
time_start: float = 0.0,
time_span: float = 1000.0,
) -> list:
"""Get extended MIDI note data including note_id, release_velocity, and probability.
Returns list of {pitch, start, duration, velocity, mute, note_id, release_velocity, probability}."""
result = get_client().query(
"/live/clip/get/notes_extended",
track_index, clip_index,
pitch_start, pitch_span,
time_start, time_span,
)
notes = []
for i in range(0, len(result), 8):
notes.append({
"pitch": result[i],
"start": result[i + 1],
"duration": result[i + 2],
"velocity": result[i + 3],
"mute": bool(result[i + 4]),
"note_id": result[i + 5],
"release_velocity": result[i + 6],
"probability": result[i + 7],
})
return notes
@mcp.tool()
def clip_add_notes(track_index: int, clip_index: int, notes: list) -> None:
"""Add MIDI notes to a clip. notes: list of {pitch, start, duration, velocity, mute}.
@@ -163,6 +219,24 @@ def register(mcp):
])
get_client().cmd("/live/clip/add/notes", *args)
@mcp.tool()
def clip_apply_note_modifications(track_index: int, clip_index: int, notes: list) -> None:
"""Modify existing MIDI notes by note_id (use clip_get_notes_extended to get IDs).
notes: list of {note_id, pitch, start, duration, velocity, mute, release_velocity, probability}."""
args = [track_index, clip_index]
for note in notes:
args.extend([
int(note["note_id"]),
int(note["pitch"]),
float(note["start"]),
float(note["duration"]),
int(note["velocity"]),
int(note.get("mute", 0)),
int(note.get("release_velocity", 64)),
float(note.get("probability", 1.0)),
])
get_client().cmd("/live/clip/apply_note_modifications", *args)
@mcp.tool()
def clip_remove_notes(
track_index: int,
@@ -179,3 +253,67 @@ def register(mcp):
pitch_start, pitch_span,
time_start, time_span,
)
# --- Automation Envelopes ---
@mcp.tool()
def clip_get_automation_envelope(
track_index: int, clip_index: int, device_index: int, param_index: int
) -> list:
"""Get automation envelope breakpoints for a device parameter in a clip.
Returns list of {time, value} breakpoints."""
result = get_client().query(
"/live/clip/get/automation_envelope",
track_index, clip_index, device_index, param_index,
)
return [
{"time": result[i], "value": result[i + 1]}
for i in range(0, len(result), 2)
]
@mcp.tool()
def clip_create_automation_envelope(
track_index: int, clip_index: int, device_index: int, param_index: int
) -> None:
"""Create an automation envelope for a device parameter in a clip."""
get_client().cmd(
"/live/clip/create_automation_envelope",
track_index, clip_index, device_index, param_index,
)
@mcp.tool()
def clip_clear_envelope(
track_index: int, clip_index: int, device_index: int, param_index: int
) -> None:
"""Clear the automation envelope for a specific device parameter in a clip."""
get_client().cmd(
"/live/clip/clear_envelope",
track_index, clip_index, device_index, param_index,
)
@mcp.tool()
def clip_clear_all_envelopes(track_index: int, clip_index: int) -> None:
"""Clear all automation envelopes in a clip."""
get_client().cmd("/live/clip/clear_all_envelopes", track_index, clip_index)
# --- Warp & Groove ---
@mcp.tool()
def clip_get_warp_markers(track_index: int, clip_index: int) -> list:
"""Get warp markers for an audio clip. Returns list of {beat_time, sample_time}."""
result = get_client().query("/live/clip/get/warp_markers", track_index, clip_index)
return [
{"beat_time": result[i], "sample_time": result[i + 1]}
for i in range(0, len(result), 2)
]
@mcp.tool()
def clip_get_groove(track_index: int, clip_index: int) -> str:
"""Get the groove name assigned to a clip (empty string if none)."""
result = get_client().query("/live/clip/get/groove", track_index, clip_index)
return result[0] if result else ""
@mcp.tool()
def clip_set_groove(track_index: int, clip_index: int, groove_name: str) -> None:
"""Assign a groove to a clip by name (use groove_get_all for available names)."""
get_client().cmd("/live/clip/set/groove", track_index, clip_index, groove_name)
+100 -12
View File
@@ -7,7 +7,7 @@ def register(mcp):
@mcp.tool()
def device_get_info(track_index: int, device_index: int) -> dict:
"""Get info about a device. Returns {name, type, class_name, num_parameters}."""
"""Get info about a device. Returns {name, type, class_name, num_parameters, can_have_chains}."""
c = get_client()
type_map = {1: "audio_effect", 2: "instrument", 4: "midi_effect"}
raw_type = c.query("/live/device/get/type", track_index, device_index)[0]
@@ -16,17 +16,29 @@ def register(mcp):
"type": type_map.get(raw_type, raw_type),
"class_name": c.query("/live/device/get/class_name", track_index, device_index)[0],
"num_parameters": c.query("/live/device/get/num_parameters", track_index, device_index)[0],
"is_active": bool(c.query("/live/device/get/is_active", track_index, device_index)[0]),
}
@mcp.tool()
def device_get_is_active(track_index: int, device_index: int) -> bool:
"""Get whether a device is active (not bypassed)."""
return bool(get_client().query("/live/device/get/is_active", track_index, device_index)[0])
@mcp.tool()
def device_set_is_active(track_index: int, device_index: int, active: bool) -> None:
"""Enable or bypass a device."""
get_client().cmd("/live/device/set/is_active", track_index, device_index, int(active))
@mcp.tool()
def device_get_parameters(track_index: int, device_index: int) -> list:
"""Get all parameters of a device.
Returns list of {index, name, value, min, max, is_quantized, value_string}."""
Returns list of {index, name, value, min, max, default_value, is_quantized}."""
c = get_client()
names = c.query("/live/device/get/parameters/name", track_index, device_index)
values = c.query("/live/device/get/parameters/value", track_index, device_index)
mins = c.query("/live/device/get/parameters/min", track_index, device_index)
maxs = c.query("/live/device/get/parameters/max", track_index, device_index)
defaults = c.query("/live/device/get/parameters/default_value", track_index, device_index)
quantized = c.query("/live/device/get/parameters/is_quantized", track_index, device_index)
return [
{
@@ -35,18 +47,22 @@ def register(mcp):
"value": v,
"min": mn,
"max": mx,
"default_value": dv,
"is_quantized": bool(q),
}
for i, (n, v, mn, mx, q) in enumerate(zip(names, values, mins, maxs, quantized))
for i, (n, v, mn, mx, dv, q) in enumerate(zip(names, values, mins, maxs, defaults, quantized))
]
@mcp.tool()
def device_get_parameter(track_index: int, device_index: int, param_index: int) -> dict:
"""Get a single device parameter. Returns {name, value, min, max, value_string}."""
"""Get a single device parameter. Returns {name, value, min, max, default_value, value_string}."""
c = get_client()
return {
"name": c.query("/live/device/get/parameter/name", track_index, device_index, param_index)[0],
"value": c.query("/live/device/get/parameter/value", track_index, device_index, param_index)[0],
"min": c.query("/live/device/get/parameter/min", track_index, device_index, param_index)[0],
"max": c.query("/live/device/get/parameter/max", track_index, device_index, param_index)[0],
"default_value": c.query("/live/device/get/parameter/default_value", track_index, device_index, param_index)[0],
"value_string": c.query("/live/device/get/parameter/value_string", track_index, device_index, param_index)[0],
}
@@ -72,15 +88,87 @@ def register(mcp):
get_client().cmd("/live/device/set/parameters/value", *args)
@mcp.tool()
def device_map_midi_cc(
def device_randomize_macros(track_index: int, device_index: int) -> None:
"""Randomize all macro knobs on a rack device."""
get_client().cmd("/live/device/randomize_macros", track_index, device_index)
@mcp.tool()
def device_get_chains(track_index: int, device_index: int) -> list:
"""Get all chains in a rack device. Returns list of {index, name, num_devices, devices}."""
c = get_client()
num = c.query("/live/device/get/num_chains", track_index, device_index)[0]
if num == 0:
return []
names = c.query("/live/device/get/chains/name", track_index, device_index)
num_devices = c.query("/live/device/get/chains/num_devices", track_index, device_index)
device_names = c.query("/live/device/get/chains/devices/name", track_index, device_index)
result = []
dev_offset = 0
for i, (name, nd) in enumerate(zip(names, num_devices)):
chain_devices = list(device_names[dev_offset:dev_offset + nd])
dev_offset += nd
result.append({"index": i, "name": name, "num_devices": nd, "devices": chain_devices})
return result
@mcp.tool()
def device_get_drum_pads(track_index: int, device_index: int) -> list:
"""Get all drum pads in a Drum Rack. Returns list of {index, name, note, mute, solo}."""
c = get_client()
names = c.query("/live/device/get/drum_pads/name", track_index, device_index)
notes = c.query("/live/device/get/drum_pads/note", track_index, device_index)
mutes = c.query("/live/device/get/drum_pads/mute", track_index, device_index)
solos = c.query("/live/device/get/drum_pads/solo", track_index, device_index)
return [
{"index": i, "name": n, "note": nt, "mute": bool(m), "solo": bool(s)}
for i, (n, nt, m, s) in enumerate(zip(names, notes, mutes, solos))
]
@mcp.tool()
def device_set_drum_pad_mute(
track_index: int, device_index: int, pad_index: int, mute: bool
) -> None:
"""Mute or unmute a drum pad in a Drum Rack."""
get_client().cmd(
"/live/device/set/drum_pads/mute",
track_index, device_index, pad_index, int(mute),
)
@mcp.tool()
def device_set_drum_pad_solo(
track_index: int, device_index: int, pad_index: int, solo: bool
) -> None:
"""Solo or unsolo a drum pad in a Drum Rack."""
get_client().cmd(
"/live/device/set/drum_pads/solo",
track_index, device_index, pad_index, int(solo),
)
@mcp.tool()
def device_chain_get_parameter(
track_index: int,
device_index: int,
chain_index: int,
sub_device_index: int,
param_index: int,
channel: int,
cc: int,
) -> None:
"""Map a MIDI CC to a device parameter. channel: 015, cc: 0127."""
get_client().cmd(
"/live/midimap/map_cc",
track_index, device_index, param_index, channel, cc,
) -> float:
"""Get a parameter value from a device inside a rack chain."""
result = get_client().query(
"/live/device/chain/get/parameter/value",
track_index, device_index, chain_index, sub_device_index, param_index,
)
return result[0]
@mcp.tool()
def device_chain_set_parameter(
track_index: int,
device_index: int,
chain_index: int,
sub_device_index: int,
param_index: int,
value: float,
) -> None:
"""Set a parameter value on a device inside a rack chain."""
get_client().cmd(
"/live/device/chain/set/parameter/value",
track_index, device_index, chain_index, sub_device_index, param_index, value,
)
+23
View File
@@ -0,0 +1,23 @@
"""MCP tools for Ableton Live groove pool."""
from ableton_mcp.osc_client import get_client
def register(mcp):
@mcp.tool()
def groove_get_all() -> list:
"""Get all grooves in the groove pool. Returns list of groove name strings."""
result = get_client().query("/live/groove/get/grooves")
return list(result)
@mcp.tool()
def groove_get_amount(groove_name: str) -> float:
"""Get the amount (intensity) of a groove (0.01.0)."""
result = get_client().query("/live/groove/get/amount", groove_name)
return result[0]
@mcp.tool()
def groove_set_amount(groove_name: str, amount: float) -> None:
"""Set the amount (intensity) of a groove (0.01.0)."""
get_client().cmd("/live/groove/set/amount", groove_name, amount)
+75
View File
@@ -0,0 +1,75 @@
"""MCP tools for Ableton Live return/auxiliary track control."""
from ableton_mcp.osc_client import get_client
def register(mcp):
@mcp.tool()
def return_track_get_info(return_index: int) -> dict:
"""Get all properties of a return track. Returns {name, mute, solo, color, volume, panning}."""
c = get_client()
return {
"name": c.query("/live/return_track/get/name", return_index)[0],
"mute": bool(c.query("/live/return_track/get/mute", return_index)[0]),
"solo": bool(c.query("/live/return_track/get/solo", return_index)[0]),
"color": c.query("/live/return_track/get/color", return_index)[0],
"volume": c.query("/live/return_track/get/volume", return_index)[0],
"panning": c.query("/live/return_track/get/panning", return_index)[0],
}
@mcp.tool()
def return_track_set_volume(return_index: int, volume: float) -> None:
"""Set return track volume (0.0 = -inf, 0.85 ≈ 0 dB, 1.0 = +6 dB)."""
get_client().cmd("/live/return_track/set/volume", return_index, volume)
@mcp.tool()
def return_track_set_panning(return_index: int, panning: float) -> None:
"""Set return track panning (-1.0 = full left, 0.0 = center, 1.0 = full right)."""
get_client().cmd("/live/return_track/set/panning", return_index, panning)
@mcp.tool()
def return_track_set_mute(return_index: int, muted: bool) -> None:
"""Mute or unmute a return track."""
get_client().cmd("/live/return_track/set/mute", return_index, int(muted))
@mcp.tool()
def return_track_set_solo(return_index: int, solo: bool) -> None:
"""Solo or unsolo a return track."""
get_client().cmd("/live/return_track/set/solo", return_index, int(solo))
@mcp.tool()
def return_track_set_name(return_index: int, name: str) -> None:
"""Rename a return track."""
get_client().cmd("/live/return_track/set/name", return_index, name)
@mcp.tool()
def return_track_set_color(return_index: int, color_index: int) -> None:
"""Set return track color by palette index (069)."""
get_client().cmd("/live/return_track/set/color_index", return_index, color_index)
@mcp.tool()
def return_track_get_send(return_index: int, send_index: int) -> float:
"""Get the send level from a return track to another return track."""
return get_client().query("/live/return_track/get/send", return_index, send_index)[0]
@mcp.tool()
def return_track_set_send(return_index: int, send_index: int, value: float) -> None:
"""Set the send level from a return track to another return track (0.01.0)."""
get_client().cmd("/live/return_track/set/send", return_index, send_index, value)
@mcp.tool()
def return_track_get_meter(return_index: int) -> dict:
"""Get return track output meter levels. Returns {level, left, right}."""
c = get_client()
return {
"level": c.query("/live/return_track/get/output_meter_level", return_index)[0],
"left": c.query("/live/return_track/get/output_meter_left", return_index)[0],
"right": c.query("/live/return_track/get/output_meter_right", return_index)[0],
}
@mcp.tool()
def return_track_get_devices(return_index: int) -> list:
"""Get all devices on a return track. Returns list of {index, name}."""
names = get_client().query("/live/return_track/get/devices/name", return_index)
return [{"index": i, "name": n} for i, n in enumerate(names)]
+14 -3
View File
@@ -11,9 +11,9 @@ def register(mcp):
get_client().cmd("/live/scene/fire", scene_index)
@mcp.tool()
def scene_fire_selected() -> None:
"""Launch the currently selected scene."""
get_client().cmd("/live/scene/fire_selected")
def scene_fire_as_selected(scene_index: int) -> None:
"""Fire a scene as if it were selected (respects follow actions)."""
get_client().cmd("/live/scene/fire_as_selected", scene_index)
@mcp.tool()
def scene_get_info(scene_index: int) -> dict:
@@ -31,6 +31,17 @@ def register(mcp):
"is_triggered": bool(c.query("/live/scene/get/is_triggered", scene_index)[0]),
}
@mcp.tool()
def scene_get_num_clips(scene_index: int) -> int:
"""Get the number of clips in a scene."""
return get_client().query("/live/scene/get/num_clips", scene_index)[0]
@mcp.tool()
def scene_get_clip_slots(scene_index: int) -> list:
"""Get clip slot occupancy for a scene. Returns list of {slot_index, has_clip}."""
result = get_client().query("/live/scene/get/clip_slots/has_clip", scene_index)
return [{"slot_index": i, "has_clip": bool(v)} for i, v in enumerate(result)]
@mcp.tool()
def scene_set_name(scene_index: int, name: str) -> None:
"""Rename a scene."""
+33 -57
View File
@@ -29,10 +29,8 @@ def register(mcp):
"can_redo": bool(c.query("/live/song/get/can_redo")[0]),
"num_tracks": c.query("/live/song/get/num_tracks")[0],
"num_scenes": c.query("/live/song/get/num_scenes")[0],
"num_return_tracks": c.query("/live/song/get/num_return_tracks")[0],
"groove_amount": c.query("/live/song/get/groove_amount")[0],
"is_ableton_link_enabled": bool(c.query("/live/song/get/is_ableton_link_enabled")[0]),
"root_note": c.query("/live/song/get/root_note")[0],
"scale_name": c.query("/live/song/get/scale_name")[0],
}
@mcp.tool()
@@ -107,6 +105,21 @@ def register(mcp):
"""Jump the playhead to a position in beats."""
get_client().cmd("/live/song/set/current_song_time", beats)
@mcp.tool()
def song_jump_by(beats: float) -> None:
"""Move the playhead forward or backward by a number of beats (negative = backward)."""
get_client().cmd("/live/song/jump_by", beats)
@mcp.tool()
def song_jump_to_next_cue() -> None:
"""Jump the playhead to the next cue point."""
get_client().cmd("/live/song/jump_to_next_cue")
@mcp.tool()
def song_jump_to_prev_cue() -> None:
"""Jump the playhead to the previous cue point."""
get_client().cmd("/live/song/jump_to_prev_cue")
@mcp.tool()
def song_get_time_signature() -> dict:
"""Get the global time signature. Returns {numerator, denominator}."""
@@ -194,18 +207,6 @@ def register(mcp):
"""Enable or disable punch-out recording."""
get_client().cmd("/live/song/set/punch_out", int(enabled))
# --- Scale / Root Note ---
@mcp.tool()
def song_set_root_note(note: int) -> None:
"""Set the root note (0=C, 1=C#, ..., 11=B)."""
get_client().cmd("/live/song/set/root_note", note)
@mcp.tool()
def song_set_scale_name(scale_name: str) -> None:
"""Set the scale name, e.g. 'Major', 'Minor', 'Dorian'."""
get_client().cmd("/live/song/set/scale_name", scale_name)
# --- Tracks ---
@mcp.tool()
@@ -214,6 +215,16 @@ def register(mcp):
names = get_client().query("/live/song/get/track_names")
return [{"index": i, "name": n} for i, n in enumerate(names)]
@mcp.tool()
def song_get_return_tracks() -> list:
"""Get all return track names. Returns list of {index, name}."""
c = get_client()
count = c.query("/live/song/get/num_return_tracks")[0]
if count == 0:
return []
names = c.query("/live/song/get/return_track_names")
return [{"index": i, "name": n} for i, n in enumerate(names)]
@mcp.tool()
def song_create_audio_track(index: int = -1) -> None:
"""Create a new audio track at the given index (-1 = end)."""
@@ -249,7 +260,7 @@ def register(mcp):
@mcp.tool()
def song_get_scenes() -> list:
"""Get all scene names. Returns list of {index, name}."""
names = get_client().query("/live/song/get/scenes/name")
names = get_client().query("/live/song/get/scene_names")
return [{"index": i, "name": n} for i, n in enumerate(names)]
@mcp.tool()
@@ -267,48 +278,13 @@ def register(mcp):
"""Duplicate the scene at scene_index."""
get_client().cmd("/live/song/duplicate_scene", scene_index)
@mcp.tool()
def song_capture_and_insert_scene() -> None:
"""Capture the current session state and insert it as a new scene."""
get_client().cmd("/live/song/capture_and_insert_scene")
# --- Cue Points ---
@mcp.tool()
def song_get_cue_points() -> list:
"""Get all cue point names. Returns list of {index, name}."""
names = get_client().query("/live/song/get/cue_points")
return [{"index": i, "name": n} for i, n in enumerate(names)]
@mcp.tool()
def song_jump_to_cue(index_or_name: str) -> None:
"""Jump to a cue point by index (int as string) or name."""
try:
val = int(index_or_name)
except ValueError:
val = index_or_name
get_client().cmd("/live/song/cue_point/jump", val)
@mcp.tool()
def song_add_or_delete_cue() -> None:
"""Toggle a cue point at the current playhead position."""
get_client().cmd("/live/song/set_or_delete_cue")
@mcp.tool()
def song_set_cue_name(cue_index: int, name: str) -> None:
"""Set the name of a cue point by index."""
get_client().cmd("/live/song/cue_point/set/name", cue_index, name)
# --- Structure export ---
@mcp.tool()
def song_export_structure() -> None:
"""Export the full session structure to a JSON file in the temp directory."""
get_client().cmd("/live/song/export/structure")
# --- Link ---
@mcp.tool()
def song_set_ableton_link(enabled: bool) -> None:
"""Enable or disable Ableton Link."""
get_client().cmd("/live/song/set/is_ableton_link_enabled", int(enabled))
"""Get all cue points. Returns list of {index, name, time}."""
result = get_client().query("/live/song/get/cue_points")
cues = []
for i in range(0, len(result), 2):
cues.append({"index": i // 2, "name": result[i], "time": result[i + 1]})
return cues
-11
View File
@@ -30,17 +30,6 @@ def register(mcp):
"""Display a message in the Ableton Live status bar."""
get_client().cmd("/live/api/show_message", message)
@mcp.tool()
def system_set_log_level(level: str) -> None:
"""Set AbletonOSC log level. level: debug | info | warning | error | critical."""
get_client().cmd("/live/api/set/log_level", level)
@mcp.tool()
def system_get_log_level() -> str:
"""Get the current AbletonOSC log level."""
result = get_client().query("/live/api/get/log_level")
return result[0]
@mcp.tool()
def system_reload_api() -> None:
"""Reload all AbletonOSC modules (useful after script updates)."""
+66 -15
View File
@@ -93,38 +93,54 @@ def register(mcp):
@mcp.tool()
def track_get_clips(track_index: int) -> list:
"""Get all clips on a track. Returns list of {index, name, length, color}."""
"""Get all session clips on a track. Returns list of {index, name, length, color, is_playing}."""
c = get_client()
names = c.query("/live/track/get/clips/name", track_index)
lengths = c.query("/live/track/get/clips/length", track_index)
colors = c.query("/live/track/get/clips/color", track_index)
playing = c.query("/live/track/get/clips/is_playing", track_index)
result = []
for i, (name, length, color) in enumerate(zip(names, lengths, colors)):
for i, (name, length, color, is_play) in enumerate(zip(names, lengths, colors, playing)):
if name:
result.append({"index": i, "name": name, "length": length, "color": color})
result.append({
"index": i,
"name": name,
"length": length,
"color": color,
"is_playing": bool(is_play),
})
return result
@mcp.tool()
def track_get_devices(track_index: int) -> list:
"""Get all devices on a track. Returns list of {index, name, type, class_name}."""
"""Get all devices on a track. Returns list of {index, name, type, class_name, can_have_chains}."""
c = get_client()
names = c.query("/live/track/get/devices/name", track_index)
types = c.query("/live/track/get/devices/type", track_index)
class_names = c.query("/live/track/get/devices/class_name", track_index)
can_chain = c.query("/live/track/get/devices/can_have_chains", track_index)
type_map = {1: "audio_effect", 2: "instrument", 4: "midi_effect"}
return [
{"index": i, "name": n, "type": type_map.get(t, t), "class_name": cn}
for i, (n, t, cn) in enumerate(zip(names, types, class_names))
{
"index": i,
"name": n,
"type": type_map.get(t, t),
"class_name": cn,
"can_have_chains": bool(ch),
}
for i, (n, t, cn, ch) in enumerate(zip(names, types, class_names, can_chain))
]
@mcp.tool()
def track_get_meter(track_index: int) -> dict:
"""Get track output meter levels. Returns {level, left, right}."""
"""Get track output meter levels. Returns {level, left, right, peak_left, peak_right}."""
c = get_client()
return {
"level": c.query("/live/track/get/output_meter_level", track_index)[0],
"left": c.query("/live/track/get/output_meter_left", track_index)[0],
"right": c.query("/live/track/get/output_meter_right", track_index)[0],
"peak_left": c.query("/live/track/get/output_meter_peak_left", track_index)[0],
"peak_right": c.query("/live/track/get/output_meter_peak_right", track_index)[0],
}
@mcp.tool()
@@ -145,17 +161,52 @@ def register(mcp):
get_client().cmd("/live/track/delete_device", track_index, device_index)
@mcp.tool()
def track_get_available_input_routing_types(track_index: int) -> list:
"""Get available input routing types for a track."""
result = get_client().query("/live/track/get/available_input_routing_types", track_index)
return list(result)
def track_duplicate_device(track_index: int, device_index: int) -> None:
"""Duplicate a device on a track."""
get_client().cmd("/live/track/duplicate_device", track_index, device_index)
# --- Input Routing ---
@mcp.tool()
def track_set_input_routing_type(track_index: int, routing_type: int) -> None:
"""Set the input routing type for a track."""
def track_get_input_routing(track_index: int) -> dict:
"""Get the current input routing type and channel for a track."""
c = get_client()
return {
"type": c.query("/live/track/get/input_routing_type", track_index)[0],
"channel": c.query("/live/track/get/input_routing_channel", track_index)[0],
"available_types": list(c.query("/live/track/get/available_input_routing_types", track_index)),
"available_channels": list(c.query("/live/track/get/available_input_routing_channels", track_index)),
}
@mcp.tool()
def track_set_input_routing_type(track_index: int, routing_type: str) -> None:
"""Set the input routing type for a track (use a name from available_types)."""
get_client().cmd("/live/track/set/input_routing_type", track_index, routing_type)
@mcp.tool()
def track_set_output_routing_type(track_index: int, routing_type: int) -> None:
"""Set the output routing type for a track."""
def track_set_input_routing_channel(track_index: int, channel: str) -> None:
"""Set the input routing channel for a track (use a name from available_channels)."""
get_client().cmd("/live/track/set/input_routing_channel", track_index, channel)
# --- Output Routing ---
@mcp.tool()
def track_get_output_routing(track_index: int) -> dict:
"""Get the current output routing type and channel for a track."""
c = get_client()
return {
"type": c.query("/live/track/get/output_routing_type", track_index)[0],
"channel": c.query("/live/track/get/output_routing_channel", track_index)[0],
"available_types": list(c.query("/live/track/get/available_output_routing_types", track_index)),
"available_channels": list(c.query("/live/track/get/available_output_routing_channels", track_index)),
}
@mcp.tool()
def track_set_output_routing_type(track_index: int, routing_type: str) -> None:
"""Set the output routing type for a track (use a name from available_types)."""
get_client().cmd("/live/track/set/output_routing_type", track_index, routing_type)
@mcp.tool()
def track_set_output_routing_channel(track_index: int, channel: str) -> None:
"""Set the output routing channel for a track (use a name from available_channels)."""
get_client().cmd("/live/track/set/output_routing_channel", track_index, channel)
+20 -4
View File
@@ -8,7 +8,8 @@ def register(mcp):
@mcp.tool()
def view_get_selection() -> dict:
"""Get the currently selected track, scene, clip, and device.
Returns {selected_track, selected_scene, selected_clip_track, selected_clip_index, selected_device_track, selected_device_index}."""
Returns {selected_track, selected_scene, selected_clip_track, selected_clip_index,
selected_device_track, selected_device_index}."""
c = get_client()
scene = c.query("/live/view/get/selected_scene")[0]
track = c.query("/live/view/get/selected_track")[0]
@@ -23,6 +24,11 @@ def register(mcp):
"selected_device_index": device[1] if len(device) >= 2 else None,
}
@mcp.tool()
def view_get_focused_document_view() -> str:
"""Get the name of the currently focused document view (e.g. 'Session', 'Arranger')."""
return get_client().query("/live/view/get/focused_document_view")[0]
@mcp.tool()
def view_set_selected_track(track_index: int) -> None:
"""Select a track in the Ableton UI."""
@@ -39,6 +45,16 @@ def register(mcp):
get_client().cmd("/live/view/set/selected_clip", track_index, clip_index)
@mcp.tool()
def view_set_selected_device(track_index: int, device_index: int) -> None:
"""Select a device in the Ableton UI."""
get_client().cmd("/live/view/set/selected_device", track_index, device_index)
def view_show_clip_detail() -> None:
"""Show the clip detail view (bottom panel) in Ableton."""
get_client().cmd("/live/view/show_clip_detail_view")
@mcp.tool()
def view_show_device_detail() -> None:
"""Show the device detail view (bottom panel) in Ableton."""
get_client().cmd("/live/view/show_device_detail_view")
@mcp.tool()
def view_focus_browser() -> None:
"""Focus the Ableton browser panel."""
get_client().cmd("/live/view/focus_browser")