From 668195b88007c28aa35153f5f361d22f30021f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Mon, 1 Jun 2026 13:38:03 +0200 Subject: [PATCH] Add browser_swap_device_preset and browser_list_children tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit browser_swap_device_preset(track, device, *path) hotswaps a preset onto an existing device without touching clips — fixes the clip-deletion bug that occurred when loading presets directly via browser_load_item. browser_list_children(category, *path) exposes the new /live/browser/list_children OSC endpoint for navigating the browser hierarchy before loading. Also documents the clip-deletion risk on browser_load_item. Co-Authored-By: Claude Sonnet 4.6 --- src/ableton_mcp/tools/browser.py | 50 +++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/src/ableton_mcp/tools/browser.py b/src/ableton_mcp/tools/browser.py index f4130e6..86f7511 100644 --- a/src/ableton_mcp/tools/browser.py +++ b/src/ableton_mcp/tools/browser.py @@ -9,20 +9,61 @@ def register(mcp): @mcp.tool() def browser_get_items(category: str) -> list: - """List items in a browser category. + """List top-level items in a browser category. category: audio_effects | instruments | midi_effects | samples | sounds | clips | packs | plugins. - Returns a flat list of item path strings.""" + Returns a flat list of child names. Use browser_list_children to navigate deeper.""" 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_list_children(category: str, *path_parts: str) -> list: + """List children at a specific path inside a browser category. + Use this to navigate the browser hierarchy before loading. + + Examples: + browser_list_children('packs') -> ['Core Library'] + browser_list_children('packs', 'Core Library', 'Racks') -> ['Audio Effect Racks', 'Drum Racks', ...] + browser_list_children('packs', 'Core Library', 'Racks', 'Drum Racks', 'Drum Machines') + -> ['505 Core Kit.adg', '808 Core Kit.adg', '909 Core Kit.adg', ...] + browser_list_children('instruments', 'Drum Rack') -> preset names inside Drum Rack + """ + if category not in _CATEGORIES: + raise ValueError(f"category must be one of: {', '.join(_CATEGORIES)}") + result = get_client().query("/live/browser/list_children", category, *path_parts) + 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').""" + WARNING: loading a full preset (.adg) from packs replaces the entire track and may + delete existing clips. Use browser_swap_device_preset to safely hot-swap a device + preset without touching clips. + + Pass path as individual strings searched across all browser categories: + browser_load_item('Drum Rack') + browser_load_item('Core Library', 'Racks', 'Drum Racks', 'Drum Machines', '808 Core Kit.adg') + """ get_client().cmd("/live/browser/load_item", *path_parts) + @mcp.tool() + def browser_swap_device_preset( + track_index: int, device_index: int, *path_parts: str + ) -> None: + """Safely swap a device preset without touching clips on the track. + Targets the device for hotswap first, then loads the new preset — clips are preserved. + + Example — swap the Drum Rack on track 0 to the 808 Core Kit: + browser_swap_device_preset(0, 0, + 'Core Library', 'Racks', 'Drum Racks', 'Drum Machines', '808 Core Kit.adg') + + Use browser_list_children to discover available preset paths first. + """ + c = get_client() + c.cmd("/live/browser/begin_hotswap", track_index, device_index) + c.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).""" @@ -41,5 +82,6 @@ def register(mcp): @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.""" + """Target a device for hotswap. The next browser_load_item call will replace + only that device and leave all clips intact.""" get_client().cmd("/live/browser/begin_hotswap", track_index, device_index)