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.
- ableton_mcp-0.1.0/MCP_Server/__init__.py +6 -0
- ableton_mcp-0.1.0/MCP_Server/server.py +661 -0
- ableton_mcp-0.1.0/PKG-INFO +166 -0
- ableton_mcp-0.1.0/README.md +151 -0
- ableton_mcp-0.1.0/ableton_mcp.egg-info/PKG-INFO +166 -0
- ableton_mcp-0.1.0/ableton_mcp.egg-info/SOURCES.txt +10 -0
- ableton_mcp-0.1.0/ableton_mcp.egg-info/dependency_links.txt +1 -0
- ableton_mcp-0.1.0/ableton_mcp.egg-info/entry_points.txt +2 -0
- ableton_mcp-0.1.0/ableton_mcp.egg-info/requires.txt +1 -0
- ableton_mcp-0.1.0/ableton_mcp.egg-info/top_level.txt +1 -0
- ableton_mcp-0.1.0/pyproject.toml +32 -0
- ableton_mcp-0.1.0/setup.cfg +4 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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"
|