Skip to main content
Glama

AbletonMCP

by Milesy1
server.py32.6 kB
# MCP server for Ableton - updated for mcp SDK without `description` arg from mcp.server.fastmcp import FastMCP, Context import socket import json import logging from dataclasses import dataclass from contextlib import asynccontextmanager from typing import AsyncIterator, Dict, Any, List, Union # Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger("AbletonMCPServer") @dataclass class AbletonConnection: host: str port: int sock: socket.socket = None def connect(self) -> bool: """Connect to the Ableton Remote Script socket server""" if self.sock: return True try: self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((self.host, self.port)) logger.info(f"Connected to Ableton at {self.host}:{self.port}") return True except Exception as e: logger.error(f"Failed to connect to Ableton: {str(e)}") self.sock = None return False def disconnect(self): """Disconnect from the Ableton Remote Script""" if self.sock: try: self.sock.close() except Exception as e: logger.error(f"Error disconnecting from Ableton: {str(e)}") finally: self.sock = None def receive_full_response(self, sock, buffer_size=8192): """Receive the complete response, potentially in multiple chunks""" chunks = [] sock.settimeout(15.0) # Increased timeout for operations that might take longer try: while True: try: chunk = sock.recv(buffer_size) if not chunk: if not chunks: raise Exception("Connection closed before receiving any data") break chunks.append(chunk) # Check if we've received a complete JSON object try: data = b''.join(chunks) json.loads(data.decode('utf-8')) logger.info(f"Received complete response ({len(data)} bytes)") return data except json.JSONDecodeError: # Incomplete JSON, continue receiving continue except socket.timeout: logger.warning("Socket timeout during chunked receive") break except (ConnectionError, BrokenPipeError, ConnectionResetError) as e: logger.error(f"Socket connection error during receive: {str(e)}") raise except Exception as e: logger.error(f"Error during receive: {str(e)}") raise # If we get here, we either timed out or broke out of the loop if chunks: data = b''.join(chunks) logger.info(f"Returning data after receive completion ({len(data)} bytes)") try: json.loads(data.decode('utf-8')) return data except json.JSONDecodeError: raise Exception("Incomplete JSON response received") else: raise Exception("No data received") def send_command(self, command_type: str, params: Dict[str, Any] = None) -> Dict[str, Any]: """Send a command to Ableton and return the response""" if not self.sock and not self.connect(): raise ConnectionError("Not connected to Ableton") command = { "type": command_type, "params": params or {} } # Commands that modify state (give them extra tolerance) is_modifying_command = command_type in [ "create_midi_track", "create_audio_track", "set_track_name", "create_clip", "add_notes_to_clip", "set_clip_name", "set_tempo", "fire_clip", "stop_clip", "set_device_parameter", "start_playback", "stop_playback", "load_instrument_or_effect", "load_browser_item" ] try: logger.info(f"Sending command: {command_type} with params: {params}") self.sock.sendall(json.dumps(command).encode('utf-8')) logger.info("Command sent, waiting for response...") # small delay for modifying commands (gives Ableton a moment) if is_modifying_command: import time time.sleep(0.1) timeout = 15.0 if is_modifying_command else 10.0 self.sock.settimeout(timeout) response_data = self.receive_full_response(self.sock) logger.info(f"Received {len(response_data)} bytes of data") response = json.loads(response_data.decode('utf-8')) logger.info(f"Response parsed, status: {response.get('status', 'unknown')}") if response.get("status") == "error": logger.error(f"Ableton error: {response.get('message')}") raise Exception(response.get("message", "Unknown error from Ableton")) if is_modifying_command: import time time.sleep(0.1) return response.get("result", {}) except socket.timeout: logger.error("Socket timeout while waiting for response from Ableton") self.sock = None raise Exception("Timeout waiting for Ableton response") except (ConnectionError, BrokenPipeError, ConnectionResetError) as e: logger.error(f"Socket connection error: {str(e)}") self.sock = None raise Exception(f"Connection to Ableton lost: {str(e)}") except json.JSONDecodeError as e: logger.error(f"Invalid JSON response from Ableton: {str(e)}") if 'response_data' in locals() and response_data: logger.error(f"Raw response (first 200 bytes): {response_data[:200]}") self.sock = None raise Exception(f"Invalid response from Ableton: {str(e)}") except Exception as e: logger.error(f"Error communicating with Ableton: {str(e)}") self.sock = None raise Exception(f"Communication error with Ableton: {str(e)}") @asynccontextmanager async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]: """Manage server startup and shutdown lifecycle""" try: logger.info("AbletonMCP server starting up") try: ableton = get_ableton_connection() logger.info("Successfully connected to Ableton on startup") except Exception as e: logger.warning(f"Could not connect to Ableton on startup: {str(e)}") logger.warning("Make sure the Ableton Remote Script is running and Ableton is open") yield {} finally: global _ableton_connection if _ableton_connection: logger.info("Disconnecting from Ableton on shutdown") try: _ableton_connection.disconnect() except Exception: pass _ableton_connection = None logger.info("AbletonMCP server shut down") # Create the MCP server WITHOUT description (newer mcp SDKs removed that arg) mcp = FastMCP( name="AbletonMCP", lifespan=server_lifespan ) # Global connection holder _ableton_connection: AbletonConnection = None def get_ableton_connection(): """Get or create a persistent Ableton connection""" global _ableton_connection if _ableton_connection is not None: try: # quick liveness test (non-intrusive) _ableton_connection.sock.settimeout(0.5) _ableton_connection.sock.send(b'') # may raise if broken _ableton_connection.sock.settimeout(None) return _ableton_connection except Exception as e: logger.warning(f"Existing connection is no longer valid: {str(e)}") try: _ableton_connection.disconnect() except Exception: pass _ableton_connection = None # Try to establish a new persistent connection (retries) max_attempts = 3 for attempt in range(1, max_attempts + 1): try: logger.info(f"Connecting to Ableton (attempt {attempt}/{max_attempts})...") candidate = AbletonConnection(host="localhost", port=9877) if candidate.connect(): _ableton_connection = candidate # basic validation try: _ableton_connection.send_command("get_session_info") logger.info("Connection validated successfully") return _ableton_connection except Exception as e: logger.error(f"Connection validation failed: {str(e)}") _ableton_connection.disconnect() _ableton_connection = None else: _ableton_connection = None except Exception as e: logger.error(f"Connection attempt {attempt} failed: {str(e)}") if _ableton_connection: try: _ableton_connection.disconnect() except Exception: pass _ableton_connection = None if attempt < max_attempts: import time time.sleep(1.0) logger.error("Failed to connect to Ableton after multiple attempts") raise Exception("Could not connect to Ableton. Make sure the Remote Script is running and Ableton is open.") # Core tool endpoints @mcp.tool() def get_session_info(ctx: Context) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("get_session_info") return json.dumps(result, indent=2) except Exception as e: logger.error(f"Error getting session info: {str(e)}") return f"Error getting session info: {str(e)}" @mcp.tool() def get_track_info(ctx: Context, track_index: int) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("get_track_info", {"track_index": track_index}) return json.dumps(result, indent=2) except Exception as e: logger.error(f"Error getting track info: {str(e)}") return f"Error getting track info: {str(e)}" @mcp.tool() def create_midi_track(ctx: Context, index: int = -1) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("create_midi_track", {"index": index}) return f"Created new MIDI track: {result.get('name', 'unknown')}" except Exception as e: logger.error(f"Error creating MIDI track: {str(e)}") return f"Error creating MIDI track: {str(e)}" @mcp.tool() def set_track_name(ctx: Context, track_index: int, name: str) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("set_track_name", {"track_index": track_index, "name": name}) return f"Renamed track to: {result.get('name', name)}" except Exception as e: logger.error(f"Error setting track name: {str(e)}") return f"Error setting track name: {str(e)}" @mcp.tool() def create_clip(ctx: Context, track_index: int, clip_index: int, length: float = 4.0) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("create_clip", { "track_index": track_index, "clip_index": clip_index, "length": length }) return f"Created new clip at track {track_index}, slot {clip_index} with length {length} beats" except Exception as e: logger.error(f"Error creating clip: {str(e)}") return f"Error creating clip: {str(e)}" @mcp.tool() def add_notes_to_clip( ctx: Context, track_index: int, clip_index: int, notes: List[Dict[str, Union[int, float, bool]]] ) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("add_notes_to_clip", { "track_index": track_index, "clip_index": clip_index, "notes": notes }) return f"Added {len(notes)} notes to clip at track {track_index}, slot {clip_index}" except Exception as e: logger.error(f"Error adding notes to clip: {str(e)}") return f"Error adding notes to clip: {str(e)}" @mcp.tool() def set_clip_name(ctx: Context, track_index: int, clip_index: int, name: str) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("set_clip_name", { "track_index": track_index, "clip_index": clip_index, "name": name }) return f"Renamed clip at track {track_index}, slot {clip_index} to '{name}'" except Exception as e: logger.error(f"Error setting clip name: {str(e)}") return f"Error setting clip name: {str(e)}" @mcp.tool() def set_tempo(ctx: Context, tempo: float) -> str: try: ableton = get_ableton_connection() ableton.send_command("set_tempo", {"tempo": tempo}) return f"Set tempo to {tempo} BPM" except Exception as e: logger.error(f"Error setting tempo: {str(e)}") return f"Error setting tempo: {str(e)}" @mcp.tool() def load_instrument_or_effect(ctx: Context, track_index: int, uri: str) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("load_browser_item", { "track_index": track_index, "item_uri": uri }) if result.get("loaded", False): new_devices = result.get("new_devices", []) if new_devices: return f"Loaded instrument with URI '{uri}' on track {track_index}. New devices: {', '.join(new_devices)}" else: devices = result.get("devices_after", []) return f"Loaded instrument with URI '{uri}' on track {track_index}. Devices on track: {', '.join(devices)}" else: return f"Failed to load instrument with URI '{uri}'" except Exception as e: logger.error(f"Error loading instrument by URI: {str(e)}") return f"Error loading instrument by URI: {str(e)}" @mcp.tool() def fire_clip(ctx: Context, track_index: int, clip_index: int) -> str: try: ableton = get_ableton_connection() ableton.send_command("fire_clip", {"track_index": track_index, "clip_index": clip_index}) return f"Started playing clip at track {track_index}, slot {clip_index}" except Exception as e: logger.error(f"Error firing clip: {str(e)}") return f"Error firing clip: {str(e)}" @mcp.tool() def stop_clip(ctx: Context, track_index: int, clip_index: int) -> str: try: ableton = get_ableton_connection() ableton.send_command("stop_clip", {"track_index": track_index, "clip_index": clip_index}) return f"Stopped clip at track {track_index}, slot {clip_index}" except Exception as e: logger.error(f"Error stopping clip: {str(e)}") return f"Error stopping clip: {str(e)}" @mcp.tool() def start_playback(ctx: Context) -> str: try: ableton = get_ableton_connection() ableton.send_command("start_playback") return "Started playback" except Exception as e: logger.error(f"Error starting playback: {str(e)}") return f"Error starting playback: {str(e)}" @mcp.tool() def stop_playback(ctx: Context) -> str: try: ableton = get_ableton_connection() ableton.send_command("stop_playback") return "Stopped playback" except Exception as e: logger.error(f"Error stopping playback: {str(e)}") return f"Error stopping playback: {str(e)}" @mcp.tool() def get_browser_tree(ctx: Context, category_type: str = "all") -> str: try: ableton = get_ableton_connection() result = ableton.send_command("get_browser_tree", {"category_type": category_type}) if "available_categories" in result and len(result.get("categories", [])) == 0: available_cats = result.get("available_categories", []) return (f"No categories found for '{category_type}'. " f"Available browser categories: {', '.join(available_cats)}") total_folders = result.get("total_folders", 0) formatted_output = f"Browser tree for '{category_type}' (showing {total_folders} folders):\n\n" def format_tree(item, indent=0): output = "" if item: prefix = " " * indent name = item.get("name", "Unknown") path = item.get("path", "") has_more = item.get("has_more", False) output += f"{prefix}• {name}" if path: output += f" (path: {path})" if has_more: output += " [...]" output += "\n" for child in item.get("children", []): output += format_tree(child, indent + 1) return output for category in result.get("categories", []): formatted_output += format_tree(category) + "\n" return formatted_output except Exception as e: error_msg = str(e) if "Browser is not available" in error_msg: logger.error(f"Browser is not available in Ableton: {error_msg}") return f"Error: The Ableton browser is not available. Make sure Ableton Live is fully loaded and try again." elif "Could not access Live application" in error_msg: logger.error(f"Could not access Live application: {error_msg}") return f"Error: Could not access the Ableton Live application. Make sure Ableton Live is running and the Remote Script is loaded." else: logger.error(f"Error getting browser tree: {error_msg}") return f"Error getting browser tree: {error_msg}" @mcp.tool() def get_browser_items_at_path(ctx: Context, path: str) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("get_browser_items_at_path", {"path": path}) if "error" in result and "available_categories" in result: error = result.get("error", "") available_cats = result.get("available_categories", []) return (f"Error: {error}\n" f"Available browser categories: {', '.join(available_cats)}") return json.dumps(result, indent=2) except Exception as e: error_msg = str(e) if "Browser is not available" in error_msg: logger.error(f"Browser is not available in Ableton: {error_msg}") return f"Error: The Ableton browser is not available. Make sure Ableton Live is fully loaded and try again." elif "Could not access Live application" in error_msg: logger.error(f"Could not access Live application: {error_msg}") return f"Error: Could not access the Ableton Live application. Make sure Ableton Live is running and the Remote Script is loaded." elif "Unknown or unavailable category" in error_msg: logger.error(f"Invalid browser category: {error_msg}") return f"Error: {error_msg}. Please check the available categories using get_browser_tree." elif "Path part" in error_msg and "not found" in error_msg: logger.error(f"Path not found: {error_msg}") return f"Error: {error_msg}. Please check the path and try again." else: logger.error(f"Error getting browser items at path: {error_msg}") return f"Error getting browser items at path: {error_msg}" @mcp.tool() def load_drum_kit(ctx: Context, track_index: int, rack_uri: str, kit_path: str) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("load_browser_item", {"track_index": track_index, "item_uri": rack_uri}) if not result.get("loaded", False): return f"Failed to load drum rack with URI '{rack_uri}'" kit_result = ableton.send_command("get_browser_items_at_path", {"path": kit_path}) if "error" in kit_result: return f"Loaded drum rack but failed to find drum kit: {kit_result.get('error')}" kit_items = kit_result.get("items", []) loadable_kits = [item for item in kit_items if item.get("is_loadable", False)] if not loadable_kits: return f"Loaded drum rack but no loadable drum kits found at '{kit_path}'" kit_uri = loadable_kits[0].get("uri") load_result = ableton.send_command("load_browser_item", {"track_index": track_index, "item_uri": kit_uri}) return f"Loaded drum rack and kit '{loadable_kits[0].get('name')}' on track {track_index}" except Exception as e: logger.error(f"Error loading drum kit: {str(e)}") return f"Error loading drum kit: {str(e)}" # New Track Management Tools @mcp.tool() def create_audio_track(ctx: Context, index: int = -1) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("create_audio_track", {"index": index}) return f"Created new audio track: {result.get('name', 'unknown')} at index {result.get('index')}" except Exception as e: logger.error(f"Error creating audio track: {str(e)}") return f"Error creating audio track: {str(e)}" @mcp.tool() def delete_track(ctx: Context, track_index: int) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("delete_track", {"track_index": track_index}) return f"Deleted track '{result.get('name')}' at index {result.get('index')}" except Exception as e: logger.error(f"Error deleting track: {str(e)}") return f"Error deleting track: {str(e)}" @mcp.tool() def duplicate_track(ctx: Context, track_index: int) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("duplicate_track", {"track_index": track_index}) return f"Duplicated track '{result.get('original_name')}' to '{result.get('new_name')}' at index {result.get('new_index')}" except Exception as e: logger.error(f"Error duplicating track: {str(e)}") return f"Error duplicating track: {str(e)}" @mcp.tool() def set_track_arm(ctx: Context, track_index: int, arm: bool = True) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("set_track_arm", {"track_index": track_index, "arm": arm}) status = "armed" if result.get('arm') else "disarmed" return f"Track '{result.get('track_name')}' is now {status}" except Exception as e: logger.error(f"Error setting track arm: {str(e)}") return f"Error setting track arm: {str(e)}" @mcp.tool() def set_track_mute(ctx: Context, track_index: int, mute: bool = False) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("set_track_mute", {"track_index": track_index, "mute": mute}) status = "muted" if result.get('mute') else "unmuted" return f"Track '{result.get('track_name')}' is now {status}" except Exception as e: logger.error(f"Error setting track mute: {str(e)}") return f"Error setting track mute: {str(e)}" @mcp.tool() def set_track_solo(ctx: Context, track_index: int, solo: bool = False) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("set_track_solo", {"track_index": track_index, "solo": solo}) status = "soloed" if result.get('solo') else "unsoloed" return f"Track '{result.get('track_name')}' is now {status}" except Exception as e: logger.error(f"Error setting track solo: {str(e)}") return f"Error setting track solo: {str(e)}" @mcp.tool() def set_track_volume(ctx: Context, track_index: int, volume: float) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("set_track_volume", {"track_index": track_index, "volume": volume}) return f"Set track '{result.get('track_name')}' volume to {result.get('volume'):.2f}" except Exception as e: logger.error(f"Error setting track volume: {str(e)}") return f"Error setting track volume: {str(e)}" @mcp.tool() def set_track_panning(ctx: Context, track_index: int, panning: float) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("set_track_panning", {"track_index": track_index, "panning": panning}) return f"Set track '{result.get('track_name')}' panning to {result.get('panning'):.2f}" except Exception as e: logger.error(f"Error setting track panning: {str(e)}") return f"Error setting track panning: {str(e)}" @mcp.tool() def set_track_send(ctx: Context, track_index: int, send_index: int, send_value: float) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("set_track_send", { "track_index": track_index, "send_index": send_index, "send_value": send_value }) return f"Set track '{result.get('track_name')}' send {result.get('send_index')} to {result.get('send_value'):.2f}" except Exception as e: logger.error(f"Error setting track send: {str(e)}") return f"Error setting track send: {str(e)}" # New Clip Management Tools @mcp.tool() def copy_clip(ctx: Context, source_track: int, source_clip: int, target_track: int, target_clip: int) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("copy_clip", { "source_track": source_track, "source_clip": source_clip, "target_track": target_track, "target_clip": target_clip }) return f"Copied clip '{result.get('clip_name')}' from track {source_track}, slot {source_clip} to track {target_track}, slot {target_clip}" except Exception as e: logger.error(f"Error copying clip: {str(e)}") return f"Error copying clip: {str(e)}" @mcp.tool() def delete_clip(ctx: Context, track_index: int, clip_index: int) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("delete_clip", {"track_index": track_index, "clip_index": clip_index}) return f"Deleted clip '{result.get('clip_name')}' from track '{result.get('track_name')}'" except Exception as e: logger.error(f"Error deleting clip: {str(e)}") return f"Error deleting clip: {str(e)}" # New Scene Management Tools @mcp.tool() def fire_scene(ctx: Context, scene_index: int) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("fire_scene", {"scene_index": scene_index}) return f"Fired scene '{result.get('scene_name')}' at index {result.get('scene_index')}" except Exception as e: logger.error(f"Error firing scene: {str(e)}") return f"Error firing scene: {str(e)}" @mcp.tool() def create_scene(ctx: Context, index: int = -1) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("create_scene", {"index": index}) return f"Created new scene '{result.get('name')}' at index {result.get('index')}" except Exception as e: logger.error(f"Error creating scene: {str(e)}") return f"Error creating scene: {str(e)}" @mcp.tool() def delete_scene(ctx: Context, scene_index: int) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("delete_scene", {"scene_index": scene_index}) return f"Deleted scene '{result.get('name')}' at index {result.get('index')}" except Exception as e: logger.error(f"Error deleting scene: {str(e)}") return f"Error deleting scene: {str(e)}" # New Recording Tools @mcp.tool() def start_recording(ctx: Context) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("start_recording") status = "recording" if result.get('recording') else "not recording" return f"Started recording - Status: {status}, Playing: {result.get('playing')}" except Exception as e: logger.error(f"Error starting recording: {str(e)}") return f"Error starting recording: {str(e)}" @mcp.tool() def stop_recording(ctx: Context) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("stop_recording") status = "recording" if result.get('recording') else "not recording" return f"Stopped recording - Status: {status}, Playing: {result.get('playing')}" except Exception as e: logger.error(f"Error stopping recording: {str(e)}") return f"Error stopping recording: {str(e)}" # New Device Management Tools @mcp.tool() def add_device(ctx: Context, track_index: int, device_type: str) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("add_device", {"track_index": track_index, "device_type": device_type}) return f"Added device type '{result.get('device_type')}' to track '{result.get('track_name')}' - Note: {result.get('note')}" except Exception as e: logger.error(f"Error adding device: {str(e)}") return f"Error adding device: {str(e)}" @mcp.tool() def remove_device(ctx: Context, track_index: int, device_index: int) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("remove_device", {"track_index": track_index, "device_index": device_index}) return f"Removed device '{result.get('device_name')}' from track '{result.get('track_name')}'" except Exception as e: logger.error(f"Error removing device: {str(e)}") return f"Error removing device: {str(e)}" @mcp.tool() def set_device_parameter(ctx: Context, track_index: int, device_index: int, parameter_index: int, value: float) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("set_device_parameter", { "track_index": track_index, "device_index": device_index, "parameter_index": parameter_index, "value": value }) return f"Set device '{result.get('device_name')}' parameter '{result.get('parameter_name')}' to {result.get('parameter_value')} on track '{result.get('track_name')}'" except Exception as e: logger.error(f"Error setting device parameter: {str(e)}") return f"Error setting device parameter: {str(e)}" @mcp.tool() def set_device_active(ctx: Context, track_index: int, device_index: int, active: bool = True) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("set_device_active", { "track_index": track_index, "device_index": device_index, "active": active }) status = "active" if result.get('active') else "inactive" return f"Device '{result.get('device_name')}' on track '{result.get('track_name')}' is now {status}" except Exception as e: logger.error(f"Error setting device active state: {str(e)}") return f"Error setting device active state: {str(e)}" @mcp.tool() def get_device_parameters(ctx: Context, track_index: int, device_index: int) -> str: try: ableton = get_ableton_connection() result = ableton.send_command("get_device_parameters", {"track_index": track_index, "device_index": device_index}) params = result.get('parameters', []) param_info = [] for param in params: param_info.append(f" [{param['index']}] {param['name']}: {param['value']:.2f} (range: {param['min']:.2f}-{param['max']:.2f})") return f"Device '{result.get('device_name')}' on track '{result.get('track_name')}' parameters:\n" + "\n".join(param_info) except Exception as e: logger.error(f"Error getting device parameters: {str(e)}") return f"Error getting device parameters: {str(e)}" # Main execution def main(): """Run the MCP server""" logger.info("Starting AbletonMCP FastMCP server...") mcp.run() if __name__ == "__main__": main()

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Milesy1/MCP-Ableton-API'

If you have feedback or need assistance with the MCP directory API, please join our Discord server