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:
@@ -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:
|
||||
|
||||
@@ -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."""
|
||||
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()
|
||||
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.0–1.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
@@ -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: 0–15, cc: 0–127."""
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user