livepilot 1.4.5 → 1.6.1

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.
@@ -1,2 +1,2 @@
1
1
  """LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
2
- __version__ = "1.0.0"
2
+ __version__ = "1.6.1"
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import json
6
6
  import os
7
7
  import socket
8
+ import threading
8
9
  import time
9
10
  import uuid
10
11
  from collections import deque
@@ -54,6 +55,7 @@ class AbletonConnection:
54
55
  self._socket: Optional[socket.socket] = None
55
56
  self._recv_buf: bytes = b""
56
57
  self._command_log: deque[dict] = deque(maxlen=self.MAX_LOG_ENTRIES)
58
+ self._lock = threading.Lock() # Serialize all TCP send/receive cycles
57
59
 
58
60
  # ------------------------------------------------------------------
59
61
  # Connection lifecycle
@@ -101,7 +103,8 @@ class AbletonConnection:
101
103
  def ping(self) -> bool:
102
104
  """Send a ping and return True if a pong is received."""
103
105
  try:
104
- resp = self._send_raw({"type": "ping"})
106
+ with self._lock:
107
+ resp = self._send_raw({"type": "ping"})
105
108
  return resp.get("result", {}).get("pong") is True
106
109
  except Exception:
107
110
  return False
@@ -109,25 +112,28 @@ class AbletonConnection:
109
112
  def send_command(self, command_type: str, params: Optional[dict] = None) -> dict:
110
113
  """Send a command to Ableton and return the result dict.
111
114
 
115
+ Thread-safe: a lock serializes all TCP send/receive cycles to
116
+ prevent socket corruption when multiple MCP tools fire concurrently.
112
117
  Retries once on socket errors with a fresh connection.
113
118
  """
114
- # Ensure we have a connection
115
- if not self.is_connected():
116
- self.connect()
119
+ with self._lock:
120
+ # Ensure we have a connection
121
+ if not self.is_connected():
122
+ self.connect()
117
123
 
118
- command: dict = {"type": command_type}
119
- if params:
120
- command["params"] = params
124
+ command: dict = {"type": command_type}
125
+ if params:
126
+ command["params"] = params
121
127
 
122
- try:
123
- response = self._send_raw(command)
124
- except (OSError, AbletonConnectionError):
125
- # Retry once with a fresh connection
126
- self.disconnect()
127
- self.connect()
128
- response = self._send_raw(command)
129
-
130
- # Log the command and response
128
+ try:
129
+ response = self._send_raw(command)
130
+ except (OSError, AbletonConnectionError):
131
+ # Retry once with a fresh connection
132
+ self.disconnect()
133
+ self.connect()
134
+ response = self._send_raw(command)
135
+
136
+ # Log and error handling outside the lock (no socket access needed)
131
137
  log_entry = {
132
138
  "command": command_type,
133
139
  "params": params,