ableton-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ """Ableton Live integration through the Model Context Protocol."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ # Expose key classes and functions for easier imports
6
+ from .server import AbletonConnection, get_ableton_connection
@@ -0,0 +1,661 @@
1
+ # ableton_mcp_server.py
2
+ from mcp.server.fastmcp import FastMCP, Context
3
+ import socket
4
+ import json
5
+ import logging
6
+ from dataclasses import dataclass
7
+ from contextlib import asynccontextmanager
8
+ from typing import AsyncIterator, Dict, Any, List, Union
9
+
10
+ # Configure logging
11
+ logging.basicConfig(level=logging.INFO,
12
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
13
+ logger = logging.getLogger("AbletonMCPServer")
14
+
15
+ @dataclass
16
+ class AbletonConnection:
17
+ host: str
18
+ port: int
19
+ sock: socket.socket = None
20
+
21
+ def connect(self) -> bool:
22
+ """Connect to the Ableton Remote Script socket server"""
23
+ if self.sock:
24
+ return True
25
+
26
+ try:
27
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
28
+ self.sock.connect((self.host, self.port))
29
+ logger.info(f"Connected to Ableton at {self.host}:{self.port}")
30
+ return True
31
+ except Exception as e:
32
+ logger.error(f"Failed to connect to Ableton: {str(e)}")
33
+ self.sock = None
34
+ return False
35
+
36
+ def disconnect(self):
37
+ """Disconnect from the Ableton Remote Script"""
38
+ if self.sock:
39
+ try:
40
+ self.sock.close()
41
+ except Exception as e:
42
+ logger.error(f"Error disconnecting from Ableton: {str(e)}")
43
+ finally:
44
+ self.sock = None
45
+
46
+ def receive_full_response(self, sock, buffer_size=8192):
47
+ """Receive the complete response, potentially in multiple chunks"""
48
+ chunks = []
49
+ sock.settimeout(15.0) # Increased timeout for operations that might take longer
50
+
51
+ try:
52
+ while True:
53
+ try:
54
+ chunk = sock.recv(buffer_size)
55
+ if not chunk:
56
+ if not chunks:
57
+ raise Exception("Connection closed before receiving any data")
58
+ break
59
+
60
+ chunks.append(chunk)
61
+
62
+ # Check if we've received a complete JSON object
63
+ try:
64
+ data = b''.join(chunks)
65
+ json.loads(data.decode('utf-8'))
66
+ logger.info(f"Received complete response ({len(data)} bytes)")
67
+ return data
68
+ except json.JSONDecodeError:
69
+ # Incomplete JSON, continue receiving
70
+ continue
71
+ except socket.timeout:
72
+ logger.warning("Socket timeout during chunked receive")
73
+ break
74
+ except (ConnectionError, BrokenPipeError, ConnectionResetError) as e:
75
+ logger.error(f"Socket connection error during receive: {str(e)}")
76
+ raise
77
+ except Exception as e:
78
+ logger.error(f"Error during receive: {str(e)}")
79
+ raise
80
+
81
+ # If we get here, we either timed out or broke out of the loop
82
+ if chunks:
83
+ data = b''.join(chunks)
84
+ logger.info(f"Returning data after receive completion ({len(data)} bytes)")
85
+ try:
86
+ json.loads(data.decode('utf-8'))
87
+ return data
88
+ except json.JSONDecodeError:
89
+ raise Exception("Incomplete JSON response received")
90
+ else:
91
+ raise Exception("No data received")
92
+
93
+ def send_command(self, command_type: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
94
+ """Send a command to Ableton and return the response"""
95
+ if not self.sock and not self.connect():
96
+ raise ConnectionError("Not connected to Ableton")
97
+
98
+ command = {
99
+ "type": command_type,
100
+ "params": params or {}
101
+ }
102
+
103
+ # Check if this is a state-modifying command
104
+ is_modifying_command = command_type in [
105
+ "create_midi_track", "create_audio_track", "set_track_name",
106
+ "create_clip", "add_notes_to_clip", "set_clip_name",
107
+ "set_tempo", "fire_clip", "stop_clip", "set_device_parameter",
108
+ "start_playback", "stop_playback", "load_instrument_or_effect"
109
+ ]
110
+
111
+ try:
112
+ logger.info(f"Sending command: {command_type} with params: {params}")
113
+
114
+ # Send the command
115
+ self.sock.sendall(json.dumps(command).encode('utf-8'))
116
+ logger.info(f"Command sent, waiting for response...")
117
+
118
+ # For state-modifying commands, add a small delay to give Ableton time to process
119
+ if is_modifying_command:
120
+ import time
121
+ time.sleep(0.1) # 100ms delay
122
+
123
+ # Set timeout based on command type
124
+ timeout = 15.0 if is_modifying_command else 10.0
125
+ self.sock.settimeout(timeout)
126
+
127
+ # Receive the response
128
+ response_data = self.receive_full_response(self.sock)
129
+ logger.info(f"Received {len(response_data)} bytes of data")
130
+
131
+ # Parse the response
132
+ response = json.loads(response_data.decode('utf-8'))
133
+ logger.info(f"Response parsed, status: {response.get('status', 'unknown')}")
134
+
135
+ if response.get("status") == "error":
136
+ logger.error(f"Ableton error: {response.get('message')}")
137
+ raise Exception(response.get("message", "Unknown error from Ableton"))
138
+
139
+ # For state-modifying commands, add another small delay after receiving response
140
+ if is_modifying_command:
141
+ import time
142
+ time.sleep(0.1) # 100ms delay
143
+
144
+ return response.get("result", {})
145
+ except socket.timeout:
146
+ logger.error("Socket timeout while waiting for response from Ableton")
147
+ self.sock = None
148
+ raise Exception("Timeout waiting for Ableton response")
149
+ except (ConnectionError, BrokenPipeError, ConnectionResetError) as e:
150
+ logger.error(f"Socket connection error: {str(e)}")
151
+ self.sock = None
152
+ raise Exception(f"Connection to Ableton lost: {str(e)}")
153
+ except json.JSONDecodeError as e:
154
+ logger.error(f"Invalid JSON response from Ableton: {str(e)}")
155
+ if 'response_data' in locals() and response_data:
156
+ logger.error(f"Raw response (first 200 bytes): {response_data[:200]}")
157
+ self.sock = None
158
+ raise Exception(f"Invalid response from Ableton: {str(e)}")
159
+ except Exception as e:
160
+ logger.error(f"Error communicating with Ableton: {str(e)}")
161
+ self.sock = None
162
+ raise Exception(f"Communication error with Ableton: {str(e)}")
163
+
164
+ @asynccontextmanager
165
+ async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
166
+ """Manage server startup and shutdown lifecycle"""
167
+ try:
168
+ logger.info("AbletonMCP server starting up")
169
+
170
+ try:
171
+ ableton = get_ableton_connection()
172
+ logger.info("Successfully connected to Ableton on startup")
173
+ except Exception as e:
174
+ logger.warning(f"Could not connect to Ableton on startup: {str(e)}")
175
+ logger.warning("Make sure the Ableton Remote Script is running")
176
+
177
+ yield {}
178
+ finally:
179
+ global _ableton_connection
180
+ if _ableton_connection:
181
+ logger.info("Disconnecting from Ableton on shutdown")
182
+ _ableton_connection.disconnect()
183
+ _ableton_connection = None
184
+ logger.info("AbletonMCP server shut down")
185
+
186
+ # Create the MCP server with lifespan support
187
+ mcp = FastMCP(
188
+ "AbletonMCP",
189
+ description="Ableton Live integration through the Model Context Protocol",
190
+ lifespan=server_lifespan
191
+ )
192
+
193
+ # Global connection for resources
194
+ _ableton_connection = None
195
+
196
+ def get_ableton_connection():
197
+ """Get or create a persistent Ableton connection"""
198
+ global _ableton_connection
199
+
200
+ if _ableton_connection is not None:
201
+ try:
202
+ # Test the connection with a simple ping
203
+ # We'll try to send an empty message, which should fail if the connection is dead
204
+ # but won't affect Ableton if it's alive
205
+ _ableton_connection.sock.settimeout(1.0)
206
+ _ableton_connection.sock.sendall(b'')
207
+ return _ableton_connection
208
+ except Exception as e:
209
+ logger.warning(f"Existing connection is no longer valid: {str(e)}")
210
+ try:
211
+ _ableton_connection.disconnect()
212
+ except:
213
+ pass
214
+ _ableton_connection = None
215
+
216
+ # Connection doesn't exist or is invalid, create a new one
217
+ if _ableton_connection is None:
218
+ # Try to connect up to 3 times with a short delay between attempts
219
+ max_attempts = 3
220
+ for attempt in range(1, max_attempts + 1):
221
+ try:
222
+ logger.info(f"Connecting to Ableton (attempt {attempt}/{max_attempts})...")
223
+ _ableton_connection = AbletonConnection(host="localhost", port=9877)
224
+ if _ableton_connection.connect():
225
+ logger.info("Created new persistent connection to Ableton")
226
+
227
+ # Validate connection with a simple command
228
+ try:
229
+ # Get session info as a test
230
+ _ableton_connection.send_command("get_session_info")
231
+ logger.info("Connection validated successfully")
232
+ return _ableton_connection
233
+ except Exception as e:
234
+ logger.error(f"Connection validation failed: {str(e)}")
235
+ _ableton_connection.disconnect()
236
+ _ableton_connection = None
237
+ # Continue to next attempt
238
+ else:
239
+ _ableton_connection = None
240
+ except Exception as e:
241
+ logger.error(f"Connection attempt {attempt} failed: {str(e)}")
242
+ if _ableton_connection:
243
+ _ableton_connection.disconnect()
244
+ _ableton_connection = None
245
+
246
+ # Wait before trying again, but only if we have more attempts left
247
+ if attempt < max_attempts:
248
+ import time
249
+ time.sleep(1.0)
250
+
251
+ # If we get here, all connection attempts failed
252
+ if _ableton_connection is None:
253
+ logger.error("Failed to connect to Ableton after multiple attempts")
254
+ raise Exception("Could not connect to Ableton. Make sure the Remote Script is running.")
255
+
256
+ return _ableton_connection
257
+
258
+
259
+ # Core Tool endpoints
260
+
261
+ @mcp.tool()
262
+ def get_session_info(ctx: Context) -> str:
263
+ """Get detailed information about the current Ableton session"""
264
+ try:
265
+ ableton = get_ableton_connection()
266
+ result = ableton.send_command("get_session_info")
267
+ return json.dumps(result, indent=2)
268
+ except Exception as e:
269
+ logger.error(f"Error getting session info from Ableton: {str(e)}")
270
+ return f"Error getting session info: {str(e)}"
271
+
272
+ @mcp.tool()
273
+ def get_track_info(ctx: Context, track_index: int) -> str:
274
+ """
275
+ Get detailed information about a specific track in Ableton.
276
+
277
+ Parameters:
278
+ - track_index: The index of the track to get information about
279
+ """
280
+ try:
281
+ ableton = get_ableton_connection()
282
+ result = ableton.send_command("get_track_info", {"track_index": track_index})
283
+ return json.dumps(result, indent=2)
284
+ except Exception as e:
285
+ logger.error(f"Error getting track info from Ableton: {str(e)}")
286
+ return f"Error getting track info: {str(e)}"
287
+
288
+ @mcp.tool()
289
+ def create_midi_track(ctx: Context, index: int = -1) -> str:
290
+ """
291
+ Create a new MIDI track in the Ableton session.
292
+
293
+ Parameters:
294
+ - index: The index to insert the track at (-1 = end of list)
295
+ """
296
+ try:
297
+ ableton = get_ableton_connection()
298
+ result = ableton.send_command("create_midi_track", {"index": index})
299
+ return f"Created new MIDI track: {result.get('name', 'unknown')}"
300
+ except Exception as e:
301
+ logger.error(f"Error creating MIDI track: {str(e)}")
302
+ return f"Error creating MIDI track: {str(e)}"
303
+
304
+
305
+ @mcp.tool()
306
+ def set_track_name(ctx: Context, track_index: int, name: str) -> str:
307
+ """
308
+ Set the name of a track.
309
+
310
+ Parameters:
311
+ - track_index: The index of the track to rename
312
+ - name: The new name for the track
313
+ """
314
+ try:
315
+ ableton = get_ableton_connection()
316
+ result = ableton.send_command("set_track_name", {"track_index": track_index, "name": name})
317
+ return f"Renamed track to: {result.get('name', name)}"
318
+ except Exception as e:
319
+ logger.error(f"Error setting track name: {str(e)}")
320
+ return f"Error setting track name: {str(e)}"
321
+
322
+ @mcp.tool()
323
+ def create_clip(ctx: Context, track_index: int, clip_index: int, length: float = 4.0) -> str:
324
+ """
325
+ Create a new MIDI clip in the specified track and clip slot.
326
+
327
+ Parameters:
328
+ - track_index: The index of the track to create the clip in
329
+ - clip_index: The index of the clip slot to create the clip in
330
+ - length: The length of the clip in beats (default: 4.0)
331
+ """
332
+ try:
333
+ ableton = get_ableton_connection()
334
+ result = ableton.send_command("create_clip", {
335
+ "track_index": track_index,
336
+ "clip_index": clip_index,
337
+ "length": length
338
+ })
339
+ return f"Created new clip at track {track_index}, slot {clip_index} with length {length} beats"
340
+ except Exception as e:
341
+ logger.error(f"Error creating clip: {str(e)}")
342
+ return f"Error creating clip: {str(e)}"
343
+
344
+ @mcp.tool()
345
+ def add_notes_to_clip(
346
+ ctx: Context,
347
+ track_index: int,
348
+ clip_index: int,
349
+ notes: List[Dict[str, Union[int, float, bool]]]
350
+ ) -> str:
351
+ """
352
+ Add MIDI notes to a clip.
353
+
354
+ Parameters:
355
+ - track_index: The index of the track containing the clip
356
+ - clip_index: The index of the clip slot containing the clip
357
+ - notes: List of note dictionaries, each with pitch, start_time, duration, velocity, and mute
358
+ """
359
+ try:
360
+ ableton = get_ableton_connection()
361
+ result = ableton.send_command("add_notes_to_clip", {
362
+ "track_index": track_index,
363
+ "clip_index": clip_index,
364
+ "notes": notes
365
+ })
366
+ return f"Added {len(notes)} notes to clip at track {track_index}, slot {clip_index}"
367
+ except Exception as e:
368
+ logger.error(f"Error adding notes to clip: {str(e)}")
369
+ return f"Error adding notes to clip: {str(e)}"
370
+
371
+ @mcp.tool()
372
+ def set_clip_name(ctx: Context, track_index: int, clip_index: int, name: str) -> str:
373
+ """
374
+ Set the name of a clip.
375
+
376
+ Parameters:
377
+ - track_index: The index of the track containing the clip
378
+ - clip_index: The index of the clip slot containing the clip
379
+ - name: The new name for the clip
380
+ """
381
+ try:
382
+ ableton = get_ableton_connection()
383
+ result = ableton.send_command("set_clip_name", {
384
+ "track_index": track_index,
385
+ "clip_index": clip_index,
386
+ "name": name
387
+ })
388
+ return f"Renamed clip at track {track_index}, slot {clip_index} to '{name}'"
389
+ except Exception as e:
390
+ logger.error(f"Error setting clip name: {str(e)}")
391
+ return f"Error setting clip name: {str(e)}"
392
+
393
+ @mcp.tool()
394
+ def set_tempo(ctx: Context, tempo: float) -> str:
395
+ """
396
+ Set the tempo of the Ableton session.
397
+
398
+ Parameters:
399
+ - tempo: The new tempo in BPM
400
+ """
401
+ try:
402
+ ableton = get_ableton_connection()
403
+ result = ableton.send_command("set_tempo", {"tempo": tempo})
404
+ return f"Set tempo to {tempo} BPM"
405
+ except Exception as e:
406
+ logger.error(f"Error setting tempo: {str(e)}")
407
+ return f"Error setting tempo: {str(e)}"
408
+
409
+
410
+ @mcp.tool()
411
+ def load_instrument_or_effect(ctx: Context, track_index: int, uri: str) -> str:
412
+ """
413
+ Load an instrument or effect onto a track using its URI.
414
+
415
+ Parameters:
416
+ - track_index: The index of the track to load the instrument on
417
+ - uri: The URI of the instrument or effect to load (e.g., 'query:Synths#Instrument%20Rack:Bass:FileId_5116')
418
+ """
419
+ try:
420
+ ableton = get_ableton_connection()
421
+ result = ableton.send_command("load_browser_item", {
422
+ "track_index": track_index,
423
+ "item_uri": uri
424
+ })
425
+
426
+ # Check if the instrument was loaded successfully
427
+ if result.get("loaded", False):
428
+ new_devices = result.get("new_devices", [])
429
+ if new_devices:
430
+ return f"Loaded instrument with URI '{uri}' on track {track_index}. New devices: {', '.join(new_devices)}"
431
+ else:
432
+ devices = result.get("devices_after", [])
433
+ return f"Loaded instrument with URI '{uri}' on track {track_index}. Devices on track: {', '.join(devices)}"
434
+ else:
435
+ return f"Failed to load instrument with URI '{uri}'"
436
+ except Exception as e:
437
+ logger.error(f"Error loading instrument by URI: {str(e)}")
438
+ return f"Error loading instrument by URI: {str(e)}"
439
+
440
+ @mcp.tool()
441
+ def fire_clip(ctx: Context, track_index: int, clip_index: int) -> str:
442
+ """
443
+ Start playing a clip.
444
+
445
+ Parameters:
446
+ - track_index: The index of the track containing the clip
447
+ - clip_index: The index of the clip slot containing the clip
448
+ """
449
+ try:
450
+ ableton = get_ableton_connection()
451
+ result = ableton.send_command("fire_clip", {
452
+ "track_index": track_index,
453
+ "clip_index": clip_index
454
+ })
455
+ return f"Started playing clip at track {track_index}, slot {clip_index}"
456
+ except Exception as e:
457
+ logger.error(f"Error firing clip: {str(e)}")
458
+ return f"Error firing clip: {str(e)}"
459
+
460
+ @mcp.tool()
461
+ def stop_clip(ctx: Context, track_index: int, clip_index: int) -> str:
462
+ """
463
+ Stop playing a clip.
464
+
465
+ Parameters:
466
+ - track_index: The index of the track containing the clip
467
+ - clip_index: The index of the clip slot containing the clip
468
+ """
469
+ try:
470
+ ableton = get_ableton_connection()
471
+ result = ableton.send_command("stop_clip", {
472
+ "track_index": track_index,
473
+ "clip_index": clip_index
474
+ })
475
+ return f"Stopped clip at track {track_index}, slot {clip_index}"
476
+ except Exception as e:
477
+ logger.error(f"Error stopping clip: {str(e)}")
478
+ return f"Error stopping clip: {str(e)}"
479
+
480
+ @mcp.tool()
481
+ def start_playback(ctx: Context) -> str:
482
+ """Start playing the Ableton session."""
483
+ try:
484
+ ableton = get_ableton_connection()
485
+ result = ableton.send_command("start_playback")
486
+ return "Started playback"
487
+ except Exception as e:
488
+ logger.error(f"Error starting playback: {str(e)}")
489
+ return f"Error starting playback: {str(e)}"
490
+
491
+ @mcp.tool()
492
+ def stop_playback(ctx: Context) -> str:
493
+ """Stop playing the Ableton session."""
494
+ try:
495
+ ableton = get_ableton_connection()
496
+ result = ableton.send_command("stop_playback")
497
+ return "Stopped playback"
498
+ except Exception as e:
499
+ logger.error(f"Error stopping playback: {str(e)}")
500
+ return f"Error stopping playback: {str(e)}"
501
+
502
+ @mcp.tool()
503
+ def get_browser_tree(ctx: Context, category_type: str = "all") -> str:
504
+ """
505
+ Get a hierarchical tree of browser categories from Ableton.
506
+
507
+ Parameters:
508
+ - category_type: Type of categories to get ('all', 'instruments', 'sounds', 'drums', 'audio_effects', 'midi_effects')
509
+ """
510
+ try:
511
+ ableton = get_ableton_connection()
512
+ result = ableton.send_command("get_browser_tree", {
513
+ "category_type": category_type
514
+ })
515
+
516
+ # Check if we got any categories
517
+ if "available_categories" in result and len(result.get("categories", [])) == 0:
518
+ available_cats = result.get("available_categories", [])
519
+ return (f"No categories found for '{category_type}'. "
520
+ f"Available browser categories: {', '.join(available_cats)}")
521
+
522
+ # Format the tree in a more readable way
523
+ total_folders = result.get("total_folders", 0)
524
+ formatted_output = f"Browser tree for '{category_type}' (showing {total_folders} folders):\n\n"
525
+
526
+ def format_tree(item, indent=0):
527
+ output = ""
528
+ if item:
529
+ prefix = " " * indent
530
+ name = item.get("name", "Unknown")
531
+ path = item.get("path", "")
532
+ has_more = item.get("has_more", False)
533
+
534
+ # Add this item
535
+ output += f"{prefix}• {name}"
536
+ if path:
537
+ output += f" (path: {path})"
538
+ if has_more:
539
+ output += " [...]"
540
+ output += "\n"
541
+
542
+ # Add children
543
+ for child in item.get("children", []):
544
+ output += format_tree(child, indent + 1)
545
+ return output
546
+
547
+ # Format each category
548
+ for category in result.get("categories", []):
549
+ formatted_output += format_tree(category)
550
+ formatted_output += "\n"
551
+
552
+ return formatted_output
553
+ except Exception as e:
554
+ error_msg = str(e)
555
+ if "Browser is not available" in error_msg:
556
+ logger.error(f"Browser is not available in Ableton: {error_msg}")
557
+ return f"Error: The Ableton browser is not available. Make sure Ableton Live is fully loaded and try again."
558
+ elif "Could not access Live application" in error_msg:
559
+ logger.error(f"Could not access Live application: {error_msg}")
560
+ return f"Error: Could not access the Ableton Live application. Make sure Ableton Live is running and the Remote Script is loaded."
561
+ else:
562
+ logger.error(f"Error getting browser tree: {error_msg}")
563
+ return f"Error getting browser tree: {error_msg}"
564
+
565
+ @mcp.tool()
566
+ def get_browser_items_at_path(ctx: Context, path: str) -> str:
567
+ """
568
+ Get browser items at a specific path in Ableton's browser.
569
+
570
+ Parameters:
571
+ - path: Path in the format "category/folder/subfolder"
572
+ where category is one of the available browser categories in Ableton
573
+ """
574
+ try:
575
+ ableton = get_ableton_connection()
576
+ result = ableton.send_command("get_browser_items_at_path", {
577
+ "path": path
578
+ })
579
+
580
+ # Check if there was an error with available categories
581
+ if "error" in result and "available_categories" in result:
582
+ error = result.get("error", "")
583
+ available_cats = result.get("available_categories", [])
584
+ return (f"Error: {error}\n"
585
+ f"Available browser categories: {', '.join(available_cats)}")
586
+
587
+ return json.dumps(result, indent=2)
588
+ except Exception as e:
589
+ error_msg = str(e)
590
+ if "Browser is not available" in error_msg:
591
+ logger.error(f"Browser is not available in Ableton: {error_msg}")
592
+ return f"Error: The Ableton browser is not available. Make sure Ableton Live is fully loaded and try again."
593
+ elif "Could not access Live application" in error_msg:
594
+ logger.error(f"Could not access Live application: {error_msg}")
595
+ return f"Error: Could not access the Ableton Live application. Make sure Ableton Live is running and the Remote Script is loaded."
596
+ elif "Unknown or unavailable category" in error_msg:
597
+ logger.error(f"Invalid browser category: {error_msg}")
598
+ return f"Error: {error_msg}. Please check the available categories using get_browser_tree."
599
+ elif "Path part" in error_msg and "not found" in error_msg:
600
+ logger.error(f"Path not found: {error_msg}")
601
+ return f"Error: {error_msg}. Please check the path and try again."
602
+ else:
603
+ logger.error(f"Error getting browser items at path: {error_msg}")
604
+ return f"Error getting browser items at path: {error_msg}"
605
+
606
+ @mcp.tool()
607
+ def load_drum_kit(ctx: Context, track_index: int, rack_uri: str, kit_path: str) -> str:
608
+ """
609
+ Load a drum rack and then load a specific drum kit into it.
610
+
611
+ Parameters:
612
+ - track_index: The index of the track to load on
613
+ - rack_uri: The URI of the drum rack to load (e.g., 'Drums/Drum Rack')
614
+ - kit_path: Path to the drum kit inside the browser (e.g., 'drums/acoustic/kit1')
615
+ """
616
+ try:
617
+ ableton = get_ableton_connection()
618
+
619
+ # Step 1: Load the drum rack
620
+ result = ableton.send_command("load_browser_item", {
621
+ "track_index": track_index,
622
+ "item_uri": rack_uri
623
+ })
624
+
625
+ if not result.get("loaded", False):
626
+ return f"Failed to load drum rack with URI '{rack_uri}'"
627
+
628
+ # Step 2: Get the drum kit items at the specified path
629
+ kit_result = ableton.send_command("get_browser_items_at_path", {
630
+ "path": kit_path
631
+ })
632
+
633
+ if "error" in kit_result:
634
+ return f"Loaded drum rack but failed to find drum kit: {kit_result.get('error')}"
635
+
636
+ # Step 3: Find a loadable drum kit
637
+ kit_items = kit_result.get("items", [])
638
+ loadable_kits = [item for item in kit_items if item.get("is_loadable", False)]
639
+
640
+ if not loadable_kits:
641
+ return f"Loaded drum rack but no loadable drum kits found at '{kit_path}'"
642
+
643
+ # Step 4: Load the first loadable kit
644
+ kit_uri = loadable_kits[0].get("uri")
645
+ load_result = ableton.send_command("load_browser_item", {
646
+ "track_index": track_index,
647
+ "item_uri": kit_uri
648
+ })
649
+
650
+ return f"Loaded drum rack and kit '{loadable_kits[0].get('name')}' on track {track_index}"
651
+ except Exception as e:
652
+ logger.error(f"Error loading drum kit: {str(e)}")
653
+ return f"Error loading drum kit: {str(e)}"
654
+
655
+ # Main execution
656
+ def main():
657
+ """Run the MCP server"""
658
+ mcp.run()
659
+
660
+ if __name__ == "__main__":
661
+ main()
@@ -0,0 +1,166 @@
1
+ Metadata-Version: 2.4
2
+ Name: ableton-mcp
3
+ Version: 0.1.0
4
+ Summary: Ableton Live integration through the Model Context Protocol
5
+ Author-email: Siddharth Ahuja <ahujasid@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ahujasid/ableton-mcp
8
+ Project-URL: Bug Tracker, https://github.com/ahujasid/ableton-mcp/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: mcp[cli]>=1.4.0
15
+
16
+ # AbletonMCP - Ableton Live Model Context Protocol Integration
17
+
18
+ AbletonMCP connects Ableton Live to Claude AI through the Model Context Protocol (MCP), allowing Claude to directly interact with and control Ableton Live. This integration enables prompt-assisted music production, track creation, and Live session manipulation.
19
+
20
+ ## Join the Community
21
+ Give feedback, get inspired, and build on top of the MCP: [Discord](https://discord.gg/claudeai)
22
+
23
+ ## Features
24
+
25
+ - **Two-way communication**: Connect Claude AI to Ableton Live through a socket-based server
26
+ - **Track manipulation**: Create, modify, and manipulate MIDI and audio tracks
27
+ - **Instrument and effect selection**: Claude can access and load the right instruments, effects and sounds from Ableton's library
28
+ - **Clip creation**: Create and edit MIDI clips with notes
29
+ - **Session control**: Start and stop playback, fire clips, and control transport
30
+
31
+ ## Components
32
+
33
+ The system consists of two main components:
34
+
35
+ 1. **Ableton Remote Script** (`AbletonMCP/__init__.py`): A MIDI Remote Script for Ableton Live that creates a socket server to receive and execute commands
36
+ 2. **MCP Server** (`server.py`): A Python server that implements the Model Context Protocol and connects to the Ableton Remote Script
37
+
38
+ ## Installation
39
+
40
+ ### Prerequisites
41
+
42
+ - Ableton Live 10 or newer
43
+ - Python 3.8 or newer
44
+ - [uv package manager](https://astral.sh/uv)
45
+
46
+ If you're on Mac, please install uv as:
47
+ ```
48
+ brew install uv
49
+ ```
50
+
51
+ On Windows:
52
+ ```
53
+ powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
54
+ ```
55
+ and then:
56
+ ```
57
+ set Path=C:\Users\[username]\.local\bin;%Path%
58
+ ```
59
+
60
+ ⚠️ Do not proceed before installing UV
61
+
62
+ ### Claude for Desktop Integration
63
+
64
+ 1. Go to Claude > Settings > Developer > Edit Config > claude_desktop_config.json to include the following:
65
+
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "ableton": {
70
+ "command": "uvx",
71
+ "args": [
72
+ "ableton-mcp"
73
+ ]
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ ### Cursor Integration
80
+
81
+ Run ableton-mcp without installing it permanently through uvx. Go to Cursor Settings > MCP and paste this as a command:
82
+
83
+ ```
84
+ uvx ableton-mcp
85
+ ```
86
+
87
+ ⚠️ Only run one instance of the MCP server (either on Cursor or Claude Desktop), not both
88
+
89
+ ### Installing the Ableton Remote Script
90
+
91
+ 1. Download the `AbletonMCP` folder from this repo
92
+ 2. Copy the folder to Ableton's MIDI Remote Scripts directory:
93
+ - **macOS**: `/Applications/Ableton Live XX.app/Contents/App-Resources/MIDI Remote Scripts/`
94
+ - **Windows**: `C:\ProgramData\Ableton\Live XX\Resources\MIDI Remote Scripts\`
95
+ - (Replace XX with your Ableton version number)
96
+ 3. Launch Ableton Live
97
+ 4. Go to Preferences > Link/MIDI
98
+ 5. In the Control Surface dropdown, select "AbletonMCP"
99
+ 6. Set Input and Output to "None"
100
+
101
+ ## Usage
102
+
103
+ ### Starting the Connection
104
+
105
+ 1. Ensure the Ableton Remote Script is loaded in Ableton Live
106
+ 2. Make sure the MCP server is configured in Claude Desktop or Cursor
107
+ 3. The connection should be established automatically when you interact with Claude
108
+
109
+ ### Using with Claude
110
+
111
+ Once the config file has been set on Claude, and the remote script is running in Ableton, you will see a hammer icon with tools for the Ableton MCP.
112
+
113
+ ## Capabilities
114
+
115
+ - Get session and track information
116
+ - Create and modify MIDI and audio tracks
117
+ - Create, edit, and trigger clips
118
+ - Control playback
119
+ - Load instruments and effects from Ableton's browser
120
+ - Add notes to MIDI clips
121
+ - Change tempo and other session parameters
122
+
123
+ ## Example Commands
124
+
125
+ Here are some examples of what you can ask Claude to do:
126
+
127
+ - "Create an 80s synthwave track"
128
+ - "Create a Metro Boomin style hip-hop beat"
129
+ - "Create a new MIDI track with a synth bass instrument"
130
+ - "Add reverb to my drums"
131
+ - "Create a 4-bar MIDI clip with a simple melody"
132
+ - "Get information about the current Ableton session"
133
+ - "Load a 808 drum rack into the selected track"
134
+ - "Add a jazz chord progression to the clip in track 1"
135
+ - "Set the tempo to 120 BPM"
136
+ - "Play the clip in track 2"
137
+
138
+
139
+ ## Troubleshooting
140
+
141
+ - **Connection issues**: Make sure the Ableton Remote Script is loaded, and the MCP server is configured on Claude
142
+ - **Timeout errors**: Try simplifying your requests or breaking them into smaller steps
143
+ - **Have you tried turning it off and on again?**: If you're still having connection errors, try restarting both Claude and Ableton Live
144
+
145
+ ## Technical Details
146
+
147
+ ### Communication Protocol
148
+
149
+ The system uses a simple JSON-based protocol over TCP sockets:
150
+
151
+ - Commands are sent as JSON objects with a `type` and optional `params`
152
+ - Responses are JSON objects with a `status` and `result` or `message`
153
+
154
+ ### Limitations & Security Considerations
155
+
156
+ - Creating complex musical arrangements might need to be broken down into smaller steps
157
+ - The tool is designed to work with Ableton's default devices and browser items
158
+ - Always save your work before extensive experimentation
159
+
160
+ ## Contributing
161
+
162
+ Contributions are welcome! Please feel free to submit a Pull Request.
163
+
164
+ ## Disclaimer
165
+
166
+ This is a third-party integration and not made by Ableton.
@@ -0,0 +1,151 @@
1
+ # AbletonMCP - Ableton Live Model Context Protocol Integration
2
+
3
+ AbletonMCP connects Ableton Live to Claude AI through the Model Context Protocol (MCP), allowing Claude to directly interact with and control Ableton Live. This integration enables prompt-assisted music production, track creation, and Live session manipulation.
4
+
5
+ ## Join the Community
6
+ Give feedback, get inspired, and build on top of the MCP: [Discord](https://discord.gg/claudeai)
7
+
8
+ ## Features
9
+
10
+ - **Two-way communication**: Connect Claude AI to Ableton Live through a socket-based server
11
+ - **Track manipulation**: Create, modify, and manipulate MIDI and audio tracks
12
+ - **Instrument and effect selection**: Claude can access and load the right instruments, effects and sounds from Ableton's library
13
+ - **Clip creation**: Create and edit MIDI clips with notes
14
+ - **Session control**: Start and stop playback, fire clips, and control transport
15
+
16
+ ## Components
17
+
18
+ The system consists of two main components:
19
+
20
+ 1. **Ableton Remote Script** (`AbletonMCP/__init__.py`): A MIDI Remote Script for Ableton Live that creates a socket server to receive and execute commands
21
+ 2. **MCP Server** (`server.py`): A Python server that implements the Model Context Protocol and connects to the Ableton Remote Script
22
+
23
+ ## Installation
24
+
25
+ ### Prerequisites
26
+
27
+ - Ableton Live 10 or newer
28
+ - Python 3.8 or newer
29
+ - [uv package manager](https://astral.sh/uv)
30
+
31
+ If you're on Mac, please install uv as:
32
+ ```
33
+ brew install uv
34
+ ```
35
+
36
+ On Windows:
37
+ ```
38
+ powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
39
+ ```
40
+ and then:
41
+ ```
42
+ set Path=C:\Users\[username]\.local\bin;%Path%
43
+ ```
44
+
45
+ ⚠️ Do not proceed before installing UV
46
+
47
+ ### Claude for Desktop Integration
48
+
49
+ 1. Go to Claude > Settings > Developer > Edit Config > claude_desktop_config.json to include the following:
50
+
51
+ ```json
52
+ {
53
+ "mcpServers": {
54
+ "ableton": {
55
+ "command": "uvx",
56
+ "args": [
57
+ "ableton-mcp"
58
+ ]
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ ### Cursor Integration
65
+
66
+ Run ableton-mcp without installing it permanently through uvx. Go to Cursor Settings > MCP and paste this as a command:
67
+
68
+ ```
69
+ uvx ableton-mcp
70
+ ```
71
+
72
+ ⚠️ Only run one instance of the MCP server (either on Cursor or Claude Desktop), not both
73
+
74
+ ### Installing the Ableton Remote Script
75
+
76
+ 1. Download the `AbletonMCP` folder from this repo
77
+ 2. Copy the folder to Ableton's MIDI Remote Scripts directory:
78
+ - **macOS**: `/Applications/Ableton Live XX.app/Contents/App-Resources/MIDI Remote Scripts/`
79
+ - **Windows**: `C:\ProgramData\Ableton\Live XX\Resources\MIDI Remote Scripts\`
80
+ - (Replace XX with your Ableton version number)
81
+ 3. Launch Ableton Live
82
+ 4. Go to Preferences > Link/MIDI
83
+ 5. In the Control Surface dropdown, select "AbletonMCP"
84
+ 6. Set Input and Output to "None"
85
+
86
+ ## Usage
87
+
88
+ ### Starting the Connection
89
+
90
+ 1. Ensure the Ableton Remote Script is loaded in Ableton Live
91
+ 2. Make sure the MCP server is configured in Claude Desktop or Cursor
92
+ 3. The connection should be established automatically when you interact with Claude
93
+
94
+ ### Using with Claude
95
+
96
+ Once the config file has been set on Claude, and the remote script is running in Ableton, you will see a hammer icon with tools for the Ableton MCP.
97
+
98
+ ## Capabilities
99
+
100
+ - Get session and track information
101
+ - Create and modify MIDI and audio tracks
102
+ - Create, edit, and trigger clips
103
+ - Control playback
104
+ - Load instruments and effects from Ableton's browser
105
+ - Add notes to MIDI clips
106
+ - Change tempo and other session parameters
107
+
108
+ ## Example Commands
109
+
110
+ Here are some examples of what you can ask Claude to do:
111
+
112
+ - "Create an 80s synthwave track"
113
+ - "Create a Metro Boomin style hip-hop beat"
114
+ - "Create a new MIDI track with a synth bass instrument"
115
+ - "Add reverb to my drums"
116
+ - "Create a 4-bar MIDI clip with a simple melody"
117
+ - "Get information about the current Ableton session"
118
+ - "Load a 808 drum rack into the selected track"
119
+ - "Add a jazz chord progression to the clip in track 1"
120
+ - "Set the tempo to 120 BPM"
121
+ - "Play the clip in track 2"
122
+
123
+
124
+ ## Troubleshooting
125
+
126
+ - **Connection issues**: Make sure the Ableton Remote Script is loaded, and the MCP server is configured on Claude
127
+ - **Timeout errors**: Try simplifying your requests or breaking them into smaller steps
128
+ - **Have you tried turning it off and on again?**: If you're still having connection errors, try restarting both Claude and Ableton Live
129
+
130
+ ## Technical Details
131
+
132
+ ### Communication Protocol
133
+
134
+ The system uses a simple JSON-based protocol over TCP sockets:
135
+
136
+ - Commands are sent as JSON objects with a `type` and optional `params`
137
+ - Responses are JSON objects with a `status` and `result` or `message`
138
+
139
+ ### Limitations & Security Considerations
140
+
141
+ - Creating complex musical arrangements might need to be broken down into smaller steps
142
+ - The tool is designed to work with Ableton's default devices and browser items
143
+ - Always save your work before extensive experimentation
144
+
145
+ ## Contributing
146
+
147
+ Contributions are welcome! Please feel free to submit a Pull Request.
148
+
149
+ ## Disclaimer
150
+
151
+ This is a third-party integration and not made by Ableton.
@@ -0,0 +1,166 @@
1
+ Metadata-Version: 2.4
2
+ Name: ableton-mcp
3
+ Version: 0.1.0
4
+ Summary: Ableton Live integration through the Model Context Protocol
5
+ Author-email: Siddharth Ahuja <ahujasid@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ahujasid/ableton-mcp
8
+ Project-URL: Bug Tracker, https://github.com/ahujasid/ableton-mcp/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: mcp[cli]>=1.4.0
15
+
16
+ # AbletonMCP - Ableton Live Model Context Protocol Integration
17
+
18
+ AbletonMCP connects Ableton Live to Claude AI through the Model Context Protocol (MCP), allowing Claude to directly interact with and control Ableton Live. This integration enables prompt-assisted music production, track creation, and Live session manipulation.
19
+
20
+ ## Join the Community
21
+ Give feedback, get inspired, and build on top of the MCP: [Discord](https://discord.gg/claudeai)
22
+
23
+ ## Features
24
+
25
+ - **Two-way communication**: Connect Claude AI to Ableton Live through a socket-based server
26
+ - **Track manipulation**: Create, modify, and manipulate MIDI and audio tracks
27
+ - **Instrument and effect selection**: Claude can access and load the right instruments, effects and sounds from Ableton's library
28
+ - **Clip creation**: Create and edit MIDI clips with notes
29
+ - **Session control**: Start and stop playback, fire clips, and control transport
30
+
31
+ ## Components
32
+
33
+ The system consists of two main components:
34
+
35
+ 1. **Ableton Remote Script** (`AbletonMCP/__init__.py`): A MIDI Remote Script for Ableton Live that creates a socket server to receive and execute commands
36
+ 2. **MCP Server** (`server.py`): A Python server that implements the Model Context Protocol and connects to the Ableton Remote Script
37
+
38
+ ## Installation
39
+
40
+ ### Prerequisites
41
+
42
+ - Ableton Live 10 or newer
43
+ - Python 3.8 or newer
44
+ - [uv package manager](https://astral.sh/uv)
45
+
46
+ If you're on Mac, please install uv as:
47
+ ```
48
+ brew install uv
49
+ ```
50
+
51
+ On Windows:
52
+ ```
53
+ powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
54
+ ```
55
+ and then:
56
+ ```
57
+ set Path=C:\Users\[username]\.local\bin;%Path%
58
+ ```
59
+
60
+ ⚠️ Do not proceed before installing UV
61
+
62
+ ### Claude for Desktop Integration
63
+
64
+ 1. Go to Claude > Settings > Developer > Edit Config > claude_desktop_config.json to include the following:
65
+
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "ableton": {
70
+ "command": "uvx",
71
+ "args": [
72
+ "ableton-mcp"
73
+ ]
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ ### Cursor Integration
80
+
81
+ Run ableton-mcp without installing it permanently through uvx. Go to Cursor Settings > MCP and paste this as a command:
82
+
83
+ ```
84
+ uvx ableton-mcp
85
+ ```
86
+
87
+ ⚠️ Only run one instance of the MCP server (either on Cursor or Claude Desktop), not both
88
+
89
+ ### Installing the Ableton Remote Script
90
+
91
+ 1. Download the `AbletonMCP` folder from this repo
92
+ 2. Copy the folder to Ableton's MIDI Remote Scripts directory:
93
+ - **macOS**: `/Applications/Ableton Live XX.app/Contents/App-Resources/MIDI Remote Scripts/`
94
+ - **Windows**: `C:\ProgramData\Ableton\Live XX\Resources\MIDI Remote Scripts\`
95
+ - (Replace XX with your Ableton version number)
96
+ 3. Launch Ableton Live
97
+ 4. Go to Preferences > Link/MIDI
98
+ 5. In the Control Surface dropdown, select "AbletonMCP"
99
+ 6. Set Input and Output to "None"
100
+
101
+ ## Usage
102
+
103
+ ### Starting the Connection
104
+
105
+ 1. Ensure the Ableton Remote Script is loaded in Ableton Live
106
+ 2. Make sure the MCP server is configured in Claude Desktop or Cursor
107
+ 3. The connection should be established automatically when you interact with Claude
108
+
109
+ ### Using with Claude
110
+
111
+ Once the config file has been set on Claude, and the remote script is running in Ableton, you will see a hammer icon with tools for the Ableton MCP.
112
+
113
+ ## Capabilities
114
+
115
+ - Get session and track information
116
+ - Create and modify MIDI and audio tracks
117
+ - Create, edit, and trigger clips
118
+ - Control playback
119
+ - Load instruments and effects from Ableton's browser
120
+ - Add notes to MIDI clips
121
+ - Change tempo and other session parameters
122
+
123
+ ## Example Commands
124
+
125
+ Here are some examples of what you can ask Claude to do:
126
+
127
+ - "Create an 80s synthwave track"
128
+ - "Create a Metro Boomin style hip-hop beat"
129
+ - "Create a new MIDI track with a synth bass instrument"
130
+ - "Add reverb to my drums"
131
+ - "Create a 4-bar MIDI clip with a simple melody"
132
+ - "Get information about the current Ableton session"
133
+ - "Load a 808 drum rack into the selected track"
134
+ - "Add a jazz chord progression to the clip in track 1"
135
+ - "Set the tempo to 120 BPM"
136
+ - "Play the clip in track 2"
137
+
138
+
139
+ ## Troubleshooting
140
+
141
+ - **Connection issues**: Make sure the Ableton Remote Script is loaded, and the MCP server is configured on Claude
142
+ - **Timeout errors**: Try simplifying your requests or breaking them into smaller steps
143
+ - **Have you tried turning it off and on again?**: If you're still having connection errors, try restarting both Claude and Ableton Live
144
+
145
+ ## Technical Details
146
+
147
+ ### Communication Protocol
148
+
149
+ The system uses a simple JSON-based protocol over TCP sockets:
150
+
151
+ - Commands are sent as JSON objects with a `type` and optional `params`
152
+ - Responses are JSON objects with a `status` and `result` or `message`
153
+
154
+ ### Limitations & Security Considerations
155
+
156
+ - Creating complex musical arrangements might need to be broken down into smaller steps
157
+ - The tool is designed to work with Ableton's default devices and browser items
158
+ - Always save your work before extensive experimentation
159
+
160
+ ## Contributing
161
+
162
+ Contributions are welcome! Please feel free to submit a Pull Request.
163
+
164
+ ## Disclaimer
165
+
166
+ This is a third-party integration and not made by Ableton.
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ MCP_Server/__init__.py
4
+ MCP_Server/server.py
5
+ ableton_mcp.egg-info/PKG-INFO
6
+ ableton_mcp.egg-info/SOURCES.txt
7
+ ableton_mcp.egg-info/dependency_links.txt
8
+ ableton_mcp.egg-info/entry_points.txt
9
+ ableton_mcp.egg-info/requires.txt
10
+ ableton_mcp.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ableton-mcp = MCP_Server.server:main
@@ -0,0 +1 @@
1
+ mcp[cli]>=1.4.0
@@ -0,0 +1 @@
1
+ MCP_Server
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "ableton-mcp"
3
+ version = "0.1.0"
4
+ description = "Ableton Live integration through the Model Context Protocol"
5
+ readme = "README.md"
6
+ requires-python = ">=3.8"
7
+ authors = [
8
+ {name = "Siddharth Ahuja", email = "ahujasid@gmail.com"}
9
+ ]
10
+ license = {text = "MIT"}
11
+ classifiers = [
12
+ "Programming Language :: Python :: 3",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Operating System :: OS Independent",
15
+ ]
16
+ dependencies = [
17
+ "mcp[cli]>=1.4.0",
18
+ ]
19
+
20
+ [project.scripts]
21
+ ableton-mcp = "MCP_Server.server:main"
22
+
23
+ [build-system]
24
+ requires = ["setuptools>=61.0", "wheel"]
25
+ build-backend = "setuptools.build_meta"
26
+
27
+ [tool.setuptools]
28
+ packages = ["MCP_Server"]
29
+
30
+ [project.urls]
31
+ "Homepage" = "https://github.com/ahujasid/ableton-mcp"
32
+ "Bug Tracker" = "https://github.com/ahujasid/ableton-mcp/issues"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+