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:
@@ -4,9 +4,9 @@ A full-featured [Model Context Protocol (MCP)](https://modelcontextprotocol.io/)
|
|||||||
|
|
||||||
## Overview
|
## 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
|
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
|
## Requirements
|
||||||
|
|
||||||
- **Ableton Live 11 or later**
|
- **Ableton Live 11 or later**
|
||||||
- **Python 3.10+** (tested with 3.10.6 via pyenv)
|
- **Python 3.10+**
|
||||||
- **AbletonOSC** installed as a MIDI Remote Script (see below)
|
- **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. Install AbletonOSC in Ableton Live
|
||||||
|
|
||||||
1. Download or clone [AbletonOSC](https://github.com/ideoforms/AbletonOSC)
|
1. Copy the `AbletonOSC` folder into your Ableton User Library Remote Scripts directory:
|
||||||
2. Copy the `AbletonOSC` folder into your Ableton User Library Remote Scripts directory:
|
|
||||||
- **Windows:** `%USERPROFILE%\Documents\Ableton\User Library\Remote Scripts`
|
- **Windows:** `%USERPROFILE%\Documents\Ableton\User Library\Remote Scripts`
|
||||||
- **macOS:** `~/Music/Ableton/User Library/Remote Scripts`
|
- **macOS:** `~/Music/Ableton/User Library/Remote Scripts`
|
||||||
3. Restart Ableton Live
|
2. Restart Ableton Live
|
||||||
4. Open **Preferences → Link, Tempo & MIDI → MIDI** and set one of the Control Surface slots to **AbletonOSC**
|
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
|
### 2. Install ableton-mcp
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repo (rename the folder after cloning)
|
|
||||||
git clone https://github.com/your-user/ableton-mcp
|
git clone https://github.com/your-user/ableton-mcp
|
||||||
cd ableton-mcp
|
cd ableton-mcp
|
||||||
|
|
||||||
# Create and activate a virtual environment
|
|
||||||
python -m venv .venv
|
python -m venv .venv
|
||||||
.venv\Scripts\activate # Windows
|
.venv\Scripts\activate # Windows
|
||||||
# source .venv/bin/activate # macOS/Linux
|
# source .venv/bin/activate # macOS/Linux
|
||||||
|
|
||||||
# Install
|
|
||||||
pip install -e .
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Configure Claude Desktop
|
### 3. Configure Claude Code
|
||||||
|
|
||||||
Add the server to your Claude Desktop configuration file:
|
Add to `~/.claude/settings.json`:
|
||||||
|
|
||||||
**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
||||||
**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"ableton": {
|
"ableton-mcp": {
|
||||||
"command": "C:\\path\\to\\ableton-mcp\\.venv\\Scripts\\ableton-mcp.exe"
|
"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
|
## Configuration
|
||||||
|
|
||||||
All settings can be overridden via environment variables:
|
|
||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `ABLETON_HOST` | `127.0.0.1` | AbletonOSC host address |
|
| `ABLETON_HOST` | `127.0.0.1` | AbletonOSC host address |
|
||||||
| `ABLETON_SEND_PORT` | `11000` | Port to send OSC commands to Live |
|
| `ABLETON_SEND_PORT` | `11000` | Port to send OSC to Live |
|
||||||
| `ABLETON_RECEIVE_PORT` | `11001` | Port to receive OSC replies from Live |
|
| `ABLETON_RECEIVE_PORT` | `11001` | Port to receive OSC from Live |
|
||||||
| `ABLETON_TIMEOUT` | `5.0` | Query timeout in seconds |
|
| `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
|
## Tool Reference
|
||||||
|
|
||||||
### System (7 tools)
|
### System (5 tools)
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `system_test_connection` | Ping AbletonOSC; returns `{connected, latency_ms}` |
|
| `system_test_connection` | Ping AbletonOSC; returns `{connected, latency_ms}` |
|
||||||
| `system_get_version` | Get Ableton Live version `{major, minor}` |
|
| `system_get_version` | Get Ableton Live version `{major, minor}` |
|
||||||
| `system_get_cpu_usage` | Get average CPU process usage (0.0–1.0) |
|
| `system_get_cpu_usage` | Average CPU process usage (0.0–1.0) |
|
||||||
| `system_show_message` | Display a message in the Ableton status bar |
|
| `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 |
|
| `system_reload_api` | Hot-reload all AbletonOSC modules |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Song / Transport (48 tools)
|
### Song / Transport (38 tools)
|
||||||
|
|
||||||
#### Playback
|
#### Playback
|
||||||
| Tool | Description |
|
| 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_start_playing` | Start playback from current position |
|
||||||
| `song_stop_playing` | Stop playback |
|
| `song_stop_playing` | Stop playback |
|
||||||
| `song_continue_playing` | Resume without resetting to start |
|
| `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 (20–300) |
|
| `song_get_tempo` / `song_set_tempo` | Get/set BPM (20–300) |
|
||||||
| `song_get_time` / `song_set_time` | Get/jump playhead position in beats |
|
| `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 |
|
| `song_get_time_signature` / `song_set_time_signature` | Get/set numerator + denominator |
|
||||||
|
|
||||||
#### Loop
|
#### Loop
|
||||||
@@ -163,38 +142,20 @@ Example (Claude Desktop config with custom port):
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `song_undo` / `song_redo` | Undo/redo |
|
| `song_undo` / `song_redo` | Undo/redo |
|
||||||
|
|
||||||
#### Tracks
|
#### Tracks & Scenes
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `song_get_tracks` | List all tracks with index and name |
|
| `song_get_tracks` | List all tracks with index and name |
|
||||||
| `song_create_audio_track` | Create audio track at index |
|
| `song_get_return_tracks` | List all return tracks with index and name |
|
||||||
| `song_create_midi_track` | Create MIDI track at index |
|
| `song_create_audio_track` / `song_create_midi_track` / `song_create_return_track` | Create tracks |
|
||||||
| `song_create_return_track` | Create return/aux track |
|
| `song_delete_track` / `song_delete_return_track` / `song_duplicate_track` | Delete/duplicate tracks |
|
||||||
| `song_delete_track` / `song_delete_return_track` | Delete track |
|
|
||||||
| `song_duplicate_track` | Duplicate a track |
|
|
||||||
|
|
||||||
#### Scenes
|
|
||||||
| Tool | Description |
|
|
||||||
|---|---|
|
|
||||||
| `song_get_scenes` | List all scenes with index and name |
|
| `song_get_scenes` | List all scenes with index and name |
|
||||||
| `song_create_scene` / `song_delete_scene` / `song_duplicate_scene` | Scene CRUD |
|
| `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
|
#### Cue Points
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `song_get_cue_points` | List all cue points |
|
| `song_get_cue_points` | List all cue points `{index, name, time}` |
|
||||||
| `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 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -202,52 +163,87 @@ Example (Claude Desktop config with custom port):
|
|||||||
|
|
||||||
| Tool | Description |
|
| 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_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_pan` | Panning (−1.0=left, 0.0=center, +1.0=right) |
|
||||||
| `track_set_mute` | Mute / unmute |
|
| `track_set_mute` / `track_set_solo` / `track_set_arm` | Mute/solo/arm |
|
||||||
| `track_set_solo` | Solo / unsolo |
|
|
||||||
| `track_set_arm` | Arm / disarm for recording |
|
|
||||||
| `track_get_send` / `track_set_send` | Get/set send level to a return track |
|
| `track_get_send` / `track_set_send` | Get/set send level to a return track |
|
||||||
| `track_set_name` | Rename a track |
|
| `track_set_name` | Rename a track |
|
||||||
| `track_set_color` | Set track color by palette index (0–69) |
|
| `track_set_color` | Set track color by palette index (0–69) |
|
||||||
| `track_stop_clips` | Stop all clips on a track |
|
| `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_set_fold` | Fold/unfold a group track |
|
||||||
| `track_get_clips` | List session clips: index, name, length, color |
|
| `track_get_clips` | Session clips: index, name, length, color, is_playing |
|
||||||
| `track_get_arrangement_clips` | List arrangement clips: index, name, length, start_time |
|
| `track_get_arrangement_clips` | Arrangement clips: index, name, length, start_time |
|
||||||
| `track_get_devices` | List devices: index, name, type, class_name |
|
| `track_get_devices` | Devices: index, name, type, class_name, can_have_chains |
|
||||||
| `track_get_meter` | Output meter levels: left, right, level |
|
| `track_get_meter` | Output meter: level, left, right, peak_left, peak_right |
|
||||||
| `track_delete_device` | Remove a device from the chain |
|
| `track_delete_device` / `track_duplicate_device` | Remove/duplicate device |
|
||||||
| `track_get_available_input_routing_types` | List available input routing options |
|
| `track_get_input_routing` | Current input type + channel + available options |
|
||||||
| `track_set_input_routing_type` / `track_set_output_routing_type` | Set I/O routing |
|
| `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 |
|
| Tool | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `clip_fire` | Launch a clip |
|
| `return_track_get_info` | All properties: name, mute, solo, color, volume, panning |
|
||||||
| `clip_stop` | Stop a playing clip |
|
| `return_track_set_volume` / `return_track_set_panning` | Volume/pan |
|
||||||
| `clip_get_info` | All clip properties: name, length, playing state, loop, markers, pitch, gain, launch mode, warp, color |
|
| `return_track_set_mute` / `return_track_set_solo` | Mute/solo |
|
||||||
| `clip_set_name` | Rename a clip |
|
| `return_track_set_name` / `return_track_set_color` | Rename/recolor |
|
||||||
| `clip_set_color` | Set clip color by palette index (0–69) |
|
| `return_track_get_send` / `return_track_set_send` | Inter-return sends |
|
||||||
| `clip_set_gain` | Set clip gain in dB |
|
| `return_track_get_meter` | Output meter: level, left, right |
|
||||||
| `clip_set_muted` | Mute/unmute a clip |
|
| `return_track_get_devices` | List devices on the return track |
|
||||||
| `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) |
|
### 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_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_warping` | Enable/disable warping |
|
||||||
| `clip_set_launch_mode` | Launch mode: 0=Trigger, 1=Gate, 2=Toggle, 3=Repeat |
|
| `clip_set_launch_mode` | 0=Trigger, 1=Gate, 2=Toggle, 3=Repeat |
|
||||||
| `clip_set_launch_quantization` | Per-clip launch quantization (0–14) |
|
| `clip_set_launch_quantization` | Per-clip quantization (0–14) |
|
||||||
| `clip_set_legato` | Enable/disable legato mode |
|
| `clip_set_legato` | Enable/disable legato |
|
||||||
| `clip_duplicate_loop` | Double loop length by duplicating content |
|
| `clip_set_velocity_amount` | Scale note velocities (0.0–1.0) |
|
||||||
| `clip_get_notes` | Get MIDI notes (filterable by pitch range + time range) |
|
| `clip_set_ram_mode` | Enable/disable RAM mode for audio clips |
|
||||||
| `clip_add_notes` | Add MIDI notes: `[{pitch, start, duration, velocity, mute}, …]` |
|
| `clip_duplicate_loop` | Double loop by duplicating content |
|
||||||
| `clip_remove_notes` | Remove MIDI notes by pitch/time range |
|
| `clip_quantize` | Quantize MIDI notes (quantization enum + strength 0.0–1.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 |
|
| Tool | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `clip_slot_get_info` | Slot state: has_clip, is_playing, is_triggered, will_record, is_group_slot |
|
| `clip_slot_get_info` | 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_fire` / `clip_slot_stop` | Trigger/stop a slot |
|
||||||
| `clip_slot_stop` | Stop a slot |
|
| `clip_slot_create_clip` | Create empty MIDI clip with given length in beats |
|
||||||
| `clip_slot_create_clip` | Create a new empty MIDI clip with a given length in beats |
|
|
||||||
| `clip_slot_delete_clip` | Delete the clip in a slot |
|
| `clip_slot_delete_clip` | Delete the clip in a slot |
|
||||||
| `clip_slot_duplicate_to` | Copy clip from one slot to another |
|
| `clip_slot_duplicate_to` | Copy clip to another slot |
|
||||||
| `clip_slot_set_stop_button` | Enable/disable the stop button for a slot |
|
| `clip_slot_set_stop_button` | Enable/disable slot stop button |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Scenes (7 tools)
|
### Scenes (9 tools)
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `scene_fire` | Launch a scene by index |
|
| `scene_fire` | Launch a scene by index |
|
||||||
| `scene_fire_selected` | Launch the currently selected scene |
|
| `scene_fire_as_selected` | Fire scene respecting follow actions |
|
||||||
| `scene_get_info` | All scene properties: name, color, tempo, tempo_enabled, time sig, is_empty, is_triggered |
|
| `scene_get_info` | All properties: name, color, tempo, time sig, is_empty, is_triggered |
|
||||||
| `scene_set_name` | Rename a scene |
|
| `scene_get_num_clips` | Number of clips in the scene |
|
||||||
| `scene_set_color` | Set scene color by palette index (0–69) |
|
| `scene_get_clip_slots` | Per-slot occupancy: `{slot_index, has_clip}` |
|
||||||
| `scene_set_tempo` | Override tempo for a scene (with enable toggle) |
|
| `scene_set_name` / `scene_set_color` | Rename/recolor |
|
||||||
| `scene_set_time_signature` | Override time signature for a scene |
|
| `scene_set_tempo` | Override tempo (with enable toggle) |
|
||||||
|
| `scene_set_time_signature` | Override time signature |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Devices (6 tools)
|
### Devices (15 tools)
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `device_get_info` | Device info: name, type (audio_effect / instrument / midi_effect), class_name, num_parameters |
|
| `device_get_info` | Name, type, class_name, num_parameters, is_active |
|
||||||
| `device_get_parameters` | All parameters: index, name, value, min, max, is_quantized |
|
| `device_get_is_active` / `device_set_is_active` | Get/set bypass state |
|
||||||
| `device_get_parameter` | Single parameter: name, value, value_string (display value) |
|
| `device_get_parameters` | All params: index, name, value, min, max, default_value, is_quantized |
|
||||||
| `device_set_parameter` | Set a parameter value (normalized 0.0–1.0) |
|
| `device_get_parameter` | Single param: name, value, min, max, default_value, value_string |
|
||||||
| `device_set_parameters_bulk` | Set multiple parameters in one call: `[{index, value}, …]` |
|
| `device_set_parameter` | Set a parameter value (0.0–1.0 normalized) |
|
||||||
| `device_map_midi_cc` | Map a MIDI CC to a device parameter |
|
| `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 |
|
| Tool | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `view_get_selection` | Current selection: selected_track, selected_scene, selected_clip, selected_device |
|
| `view_get_selection` | Selected track, scene, clip, and device |
|
||||||
| `view_set_selected_track` | Select a track in the Ableton UI |
|
| `view_get_focused_document_view` | Active view name (e.g. `'Session'`, `'Arranger'`) |
|
||||||
| `view_set_selected_scene` | Select a scene in the Ableton UI |
|
| `view_set_selected_track` | Select a track |
|
||||||
| `view_set_selected_clip` | Select a clip in the Ableton UI |
|
| `view_set_selected_scene` | Select a scene |
|
||||||
| `view_set_selected_device` | Select a device in the Ableton UI |
|
| `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.0–1.0) by name |
|
||||||
|
| `groove_set_amount` | Set groove intensity (0.0–1.0) by name |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -310,7 +338,7 @@ Subscribe to property changes from Ableton Live and poll them at any time.
|
|||||||
|
|
||||||
| Tool | Description |
|
| 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_stop` | Unregister a listener |
|
||||||
| `listener_get_events` | Drain queued events (up to `max_events`); each event is a list of OSC args |
|
| `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` | any read/write property | requires `track_index` + `clip_index` |
|
||||||
| `clip_slot` | any 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
|
## 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
|
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.
|
(C3, Eb3, G3) on beats 1 and 3 with velocity 90.
|
||||||
|
|
||||||
Set every device on track 2 to a random-ish sound:
|
Load a reverb from the browser onto the first return track and set the
|
||||||
randomize all parameters whose name contains "Filter".
|
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/
|
src/ableton_mcp/
|
||||||
├── server.py # FastMCP app; registers all tool modules
|
├── server.py # FastMCP app; registers all tool modules
|
||||||
├── osc_client.py # Thread-safe OSCClient singleton
|
├── osc_client.py # Thread-safe OSCClient singleton
|
||||||
│ # cmd() – fire-and-forget
|
│ # cmd() – fire-and-forget
|
||||||
│ # query() – synchronous request/response
|
│ # query() – synchronous request/response
|
||||||
│ # start_listener() / drain_listener() – real-time events
|
│ # start_listener() / drain_listener() – real-time events
|
||||||
├── config.py # Port/host/timeout from env vars
|
├── config.py # Port/host/timeout from env vars
|
||||||
└── tools/
|
└── tools/
|
||||||
├── song.py # Transport, tempo, loop, scenes, tracks, cues, scale, Link
|
├── song.py # Transport, tempo, loop, scenes, tracks, cues
|
||||||
├── track.py # Mixing, routing, devices list, meter
|
├── track.py # Mixing, routing, devices list, meter
|
||||||
├── clip.py # Playback, MIDI notes, pitch, warp, loop, launch
|
├── clip.py # Playback, MIDI notes, pitch, warp, loop, launch, envelopes
|
||||||
├── clip_slot.py # Slot-level create/fire/delete/duplicate
|
├── clip_slot.py # Slot-level create/fire/delete/duplicate
|
||||||
├── scene.py # Scene fire, tempo/time-sig overrides
|
├── scene.py # Scene fire, info, clip slots, tempo/time-sig overrides
|
||||||
├── device.py # Parameter get/set (individual + bulk), MIDI CC mapping
|
├── device.py # Parameters (individual + bulk), chains, drum pads
|
||||||
├── view.py # Selected track/scene/clip/device
|
├── return_track.py# Return/aux track mixing and devices
|
||||||
├── system.py # Connectivity, version, CPU, status bar, logging
|
├── browser.py # Content library browsing, load, preview, hotswap
|
||||||
└── listener.py # Real-time property change subscriptions
|
├── 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
|
## Troubleshooting
|
||||||
|
|
||||||
**`system_test_connection` returns `connected: false`**
|
**`system_test_connection` returns `connected: false`**
|
||||||
- Confirm AbletonOSC is enabled in Ableton → Preferences → Control Surfaces
|
- Confirm AbletonOSC is enabled in Preferences → Control Surfaces
|
||||||
- Make sure a Live project is open (AbletonOSC only activates with an open set)
|
- A Live project must be open (AbletonOSC only activates with an open set)
|
||||||
- Check that no firewall is blocking UDP 11000/11001 on localhost
|
- Check that no firewall blocks UDP 11000/11001 on localhost
|
||||||
|
|
||||||
**Timeout errors on queries**
|
**Timeout errors**
|
||||||
- Increase `ABLETON_TIMEOUT` env var (default 5 s)
|
- 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'`**
|
**`No module named 'ableton_mcp'`**
|
||||||
- Make sure you installed with `pip install -e .` inside the correct `.venv`
|
- Install with `pip install -e .` inside the correct `.venv`
|
||||||
- The Claude Desktop config `command` path must point to the `.venv` executable, not the system Python
|
- The MCP config `command` path must point to the `.venv` executable
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ from ableton_mcp.tools import (
|
|||||||
device,
|
device,
|
||||||
view,
|
view,
|
||||||
listener,
|
listener,
|
||||||
|
return_track,
|
||||||
|
browser,
|
||||||
|
groove,
|
||||||
)
|
)
|
||||||
|
|
||||||
mcp = FastMCP("ableton-mcp")
|
mcp = FastMCP("ableton-mcp")
|
||||||
@@ -25,6 +28,9 @@ scene.register(mcp)
|
|||||||
device.register(mcp)
|
device.register(mcp)
|
||||||
view.register(mcp)
|
view.register(mcp)
|
||||||
listener.register(mcp)
|
listener.register(mcp)
|
||||||
|
return_track.register(mcp)
|
||||||
|
browser.register(mcp)
|
||||||
|
groove.register(mcp)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -114,11 +114,36 @@ def register(mcp):
|
|||||||
"""Enable or disable legato mode for a clip."""
|
"""Enable or disable legato mode for a clip."""
|
||||||
get_client().cmd("/live/clip/set/legato", track_index, clip_index, int(legato))
|
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.0–1.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()
|
@mcp.tool()
|
||||||
def clip_duplicate_loop(track_index: int, clip_index: int) -> None:
|
def clip_duplicate_loop(track_index: int, clip_index: int) -> None:
|
||||||
"""Double the clip loop length by duplicating its content."""
|
"""Double the clip loop length by duplicating its content."""
|
||||||
get_client().cmd("/live/clip/duplicate_loop", track_index, clip_index)
|
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.0–1.0 (1.0 = fully quantized)."""
|
||||||
|
get_client().cmd(
|
||||||
|
"/live/clip/quantize",
|
||||||
|
track_index, clip_index, quantization, strength,
|
||||||
|
)
|
||||||
|
|
||||||
# --- MIDI Notes ---
|
# --- MIDI Notes ---
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
@@ -148,6 +173,37 @@ def register(mcp):
|
|||||||
})
|
})
|
||||||
return notes
|
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()
|
@mcp.tool()
|
||||||
def clip_add_notes(track_index: int, clip_index: int, notes: list) -> None:
|
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}.
|
"""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)
|
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()
|
@mcp.tool()
|
||||||
def clip_remove_notes(
|
def clip_remove_notes(
|
||||||
track_index: int,
|
track_index: int,
|
||||||
@@ -179,3 +253,67 @@ def register(mcp):
|
|||||||
pitch_start, pitch_span,
|
pitch_start, pitch_span,
|
||||||
time_start, time_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
@@ -7,7 +7,7 @@ def register(mcp):
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def device_get_info(track_index: int, device_index: int) -> dict:
|
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()
|
c = get_client()
|
||||||
type_map = {1: "audio_effect", 2: "instrument", 4: "midi_effect"}
|
type_map = {1: "audio_effect", 2: "instrument", 4: "midi_effect"}
|
||||||
raw_type = c.query("/live/device/get/type", track_index, device_index)[0]
|
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),
|
"type": type_map.get(raw_type, raw_type),
|
||||||
"class_name": c.query("/live/device/get/class_name", track_index, device_index)[0],
|
"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],
|
"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()
|
@mcp.tool()
|
||||||
def device_get_parameters(track_index: int, device_index: int) -> list:
|
def device_get_parameters(track_index: int, device_index: int) -> list:
|
||||||
"""Get all parameters of a device.
|
"""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()
|
c = get_client()
|
||||||
names = c.query("/live/device/get/parameters/name", track_index, device_index)
|
names = c.query("/live/device/get/parameters/name", track_index, device_index)
|
||||||
values = c.query("/live/device/get/parameters/value", 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)
|
mins = c.query("/live/device/get/parameters/min", track_index, device_index)
|
||||||
maxs = c.query("/live/device/get/parameters/max", 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)
|
quantized = c.query("/live/device/get/parameters/is_quantized", track_index, device_index)
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -35,18 +47,22 @@ def register(mcp):
|
|||||||
"value": v,
|
"value": v,
|
||||||
"min": mn,
|
"min": mn,
|
||||||
"max": mx,
|
"max": mx,
|
||||||
|
"default_value": dv,
|
||||||
"is_quantized": bool(q),
|
"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()
|
@mcp.tool()
|
||||||
def device_get_parameter(track_index: int, device_index: int, param_index: int) -> dict:
|
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()
|
c = get_client()
|
||||||
return {
|
return {
|
||||||
"name": c.query("/live/device/get/parameter/name", track_index, device_index, param_index)[0],
|
"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],
|
"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],
|
"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)
|
get_client().cmd("/live/device/set/parameters/value", *args)
|
||||||
|
|
||||||
@mcp.tool()
|
@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,
|
track_index: int,
|
||||||
device_index: int,
|
device_index: int,
|
||||||
|
chain_index: int,
|
||||||
|
sub_device_index: int,
|
||||||
param_index: int,
|
param_index: int,
|
||||||
channel: int,
|
) -> float:
|
||||||
cc: int,
|
"""Get a parameter value from a device inside a rack chain."""
|
||||||
) -> None:
|
result = get_client().query(
|
||||||
"""Map a MIDI CC to a device parameter. channel: 0–15, cc: 0–127."""
|
"/live/device/chain/get/parameter/value",
|
||||||
get_client().cmd(
|
track_index, device_index, chain_index, sub_device_index, param_index,
|
||||||
"/live/midimap/map_cc",
|
)
|
||||||
track_index, device_index, param_index, channel, cc,
|
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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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.0–1.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.0–1.0)."""
|
||||||
|
get_client().cmd("/live/groove/set/amount", groove_name, amount)
|
||||||
@@ -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 (0–69)."""
|
||||||
|
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.0–1.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)]
|
||||||
@@ -11,9 +11,9 @@ def register(mcp):
|
|||||||
get_client().cmd("/live/scene/fire", scene_index)
|
get_client().cmd("/live/scene/fire", scene_index)
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def scene_fire_selected() -> None:
|
def scene_fire_as_selected(scene_index: int) -> None:
|
||||||
"""Launch the currently selected scene."""
|
"""Fire a scene as if it were selected (respects follow actions)."""
|
||||||
get_client().cmd("/live/scene/fire_selected")
|
get_client().cmd("/live/scene/fire_as_selected", scene_index)
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def scene_get_info(scene_index: int) -> dict:
|
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]),
|
"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()
|
@mcp.tool()
|
||||||
def scene_set_name(scene_index: int, name: str) -> None:
|
def scene_set_name(scene_index: int, name: str) -> None:
|
||||||
"""Rename a scene."""
|
"""Rename a scene."""
|
||||||
|
|||||||
@@ -29,10 +29,8 @@ def register(mcp):
|
|||||||
"can_redo": bool(c.query("/live/song/get/can_redo")[0]),
|
"can_redo": bool(c.query("/live/song/get/can_redo")[0]),
|
||||||
"num_tracks": c.query("/live/song/get/num_tracks")[0],
|
"num_tracks": c.query("/live/song/get/num_tracks")[0],
|
||||||
"num_scenes": c.query("/live/song/get/num_scenes")[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],
|
"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()
|
@mcp.tool()
|
||||||
@@ -107,6 +105,21 @@ def register(mcp):
|
|||||||
"""Jump the playhead to a position in beats."""
|
"""Jump the playhead to a position in beats."""
|
||||||
get_client().cmd("/live/song/set/current_song_time", 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()
|
@mcp.tool()
|
||||||
def song_get_time_signature() -> dict:
|
def song_get_time_signature() -> dict:
|
||||||
"""Get the global time signature. Returns {numerator, denominator}."""
|
"""Get the global time signature. Returns {numerator, denominator}."""
|
||||||
@@ -194,18 +207,6 @@ def register(mcp):
|
|||||||
"""Enable or disable punch-out recording."""
|
"""Enable or disable punch-out recording."""
|
||||||
get_client().cmd("/live/song/set/punch_out", int(enabled))
|
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 ---
|
# --- Tracks ---
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
@@ -214,6 +215,16 @@ def register(mcp):
|
|||||||
names = get_client().query("/live/song/get/track_names")
|
names = get_client().query("/live/song/get/track_names")
|
||||||
return [{"index": i, "name": n} for i, n in enumerate(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()
|
@mcp.tool()
|
||||||
def song_create_audio_track(index: int = -1) -> None:
|
def song_create_audio_track(index: int = -1) -> None:
|
||||||
"""Create a new audio track at the given index (-1 = end)."""
|
"""Create a new audio track at the given index (-1 = end)."""
|
||||||
@@ -249,7 +260,7 @@ def register(mcp):
|
|||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def song_get_scenes() -> list:
|
def song_get_scenes() -> list:
|
||||||
"""Get all scene names. Returns list of {index, name}."""
|
"""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)]
|
return [{"index": i, "name": n} for i, n in enumerate(names)]
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
@@ -267,48 +278,13 @@ def register(mcp):
|
|||||||
"""Duplicate the scene at scene_index."""
|
"""Duplicate the scene at scene_index."""
|
||||||
get_client().cmd("/live/song/duplicate_scene", 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 ---
|
# --- Cue Points ---
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def song_get_cue_points() -> list:
|
def song_get_cue_points() -> list:
|
||||||
"""Get all cue point names. Returns list of {index, name}."""
|
"""Get all cue points. Returns list of {index, name, time}."""
|
||||||
names = get_client().query("/live/song/get/cue_points")
|
result = get_client().query("/live/song/get/cue_points")
|
||||||
return [{"index": i, "name": n} for i, n in enumerate(names)]
|
cues = []
|
||||||
|
for i in range(0, len(result), 2):
|
||||||
@mcp.tool()
|
cues.append({"index": i // 2, "name": result[i], "time": result[i + 1]})
|
||||||
def song_jump_to_cue(index_or_name: str) -> None:
|
return cues
|
||||||
"""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))
|
|
||||||
|
|||||||
@@ -30,17 +30,6 @@ def register(mcp):
|
|||||||
"""Display a message in the Ableton Live status bar."""
|
"""Display a message in the Ableton Live status bar."""
|
||||||
get_client().cmd("/live/api/show_message", message)
|
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()
|
@mcp.tool()
|
||||||
def system_reload_api() -> None:
|
def system_reload_api() -> None:
|
||||||
"""Reload all AbletonOSC modules (useful after script updates)."""
|
"""Reload all AbletonOSC modules (useful after script updates)."""
|
||||||
|
|||||||
@@ -93,38 +93,54 @@ def register(mcp):
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def track_get_clips(track_index: int) -> list:
|
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()
|
c = get_client()
|
||||||
names = c.query("/live/track/get/clips/name", track_index)
|
names = c.query("/live/track/get/clips/name", track_index)
|
||||||
lengths = c.query("/live/track/get/clips/length", track_index)
|
lengths = c.query("/live/track/get/clips/length", track_index)
|
||||||
colors = c.query("/live/track/get/clips/color", track_index)
|
colors = c.query("/live/track/get/clips/color", track_index)
|
||||||
|
playing = c.query("/live/track/get/clips/is_playing", track_index)
|
||||||
result = []
|
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:
|
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
|
return result
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def track_get_devices(track_index: int) -> list:
|
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()
|
c = get_client()
|
||||||
names = c.query("/live/track/get/devices/name", track_index)
|
names = c.query("/live/track/get/devices/name", track_index)
|
||||||
types = c.query("/live/track/get/devices/type", track_index)
|
types = c.query("/live/track/get/devices/type", track_index)
|
||||||
class_names = c.query("/live/track/get/devices/class_name", 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"}
|
type_map = {1: "audio_effect", 2: "instrument", 4: "midi_effect"}
|
||||||
return [
|
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()
|
@mcp.tool()
|
||||||
def track_get_meter(track_index: int) -> dict:
|
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()
|
c = get_client()
|
||||||
return {
|
return {
|
||||||
"level": c.query("/live/track/get/output_meter_level", track_index)[0],
|
"level": c.query("/live/track/get/output_meter_level", track_index)[0],
|
||||||
"left": c.query("/live/track/get/output_meter_left", 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],
|
"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()
|
@mcp.tool()
|
||||||
@@ -145,17 +161,52 @@ def register(mcp):
|
|||||||
get_client().cmd("/live/track/delete_device", track_index, device_index)
|
get_client().cmd("/live/track/delete_device", track_index, device_index)
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def track_get_available_input_routing_types(track_index: int) -> list:
|
def track_duplicate_device(track_index: int, device_index: int) -> None:
|
||||||
"""Get available input routing types for a track."""
|
"""Duplicate a device on a track."""
|
||||||
result = get_client().query("/live/track/get/available_input_routing_types", track_index)
|
get_client().cmd("/live/track/duplicate_device", track_index, device_index)
|
||||||
return list(result)
|
|
||||||
|
# --- Input Routing ---
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def track_set_input_routing_type(track_index: int, routing_type: int) -> None:
|
def track_get_input_routing(track_index: int) -> dict:
|
||||||
"""Set the input routing type for a track."""
|
"""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)
|
get_client().cmd("/live/track/set/input_routing_type", track_index, routing_type)
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def track_set_output_routing_type(track_index: int, routing_type: int) -> None:
|
def track_set_input_routing_channel(track_index: int, channel: str) -> None:
|
||||||
"""Set the output routing type for a track."""
|
"""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)
|
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)
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ def register(mcp):
|
|||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def view_get_selection() -> dict:
|
def view_get_selection() -> dict:
|
||||||
"""Get the currently selected track, scene, clip, and device.
|
"""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()
|
c = get_client()
|
||||||
scene = c.query("/live/view/get/selected_scene")[0]
|
scene = c.query("/live/view/get/selected_scene")[0]
|
||||||
track = c.query("/live/view/get/selected_track")[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,
|
"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()
|
@mcp.tool()
|
||||||
def view_set_selected_track(track_index: int) -> None:
|
def view_set_selected_track(track_index: int) -> None:
|
||||||
"""Select a track in the Ableton UI."""
|
"""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)
|
get_client().cmd("/live/view/set/selected_clip", track_index, clip_index)
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def view_set_selected_device(track_index: int, device_index: int) -> None:
|
def view_show_clip_detail() -> None:
|
||||||
"""Select a device in the Ableton UI."""
|
"""Show the clip detail view (bottom panel) in Ableton."""
|
||||||
get_client().cmd("/live/view/set/selected_device", track_index, device_index)
|
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")
|
||||||
|
|||||||
Reference in New Issue
Block a user