laive-mcp 0.1.0 → 0.1.2

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v0.1.2 - 2026-03-22
6
+
7
+ - Fixed an MCP transport crash when the Live bridge socket is unreachable by preventing the bridge client from raising an unhandled `error` event during lazy connection attempts. Tool calls now return structured MCP errors instead of closing the server process.
8
+
9
+ ## v0.1.1 - 2026-03-22
10
+
11
+ - Fixed MCP startup compatibility by implementing the `initialize` handshake, ignoring `notifications/initialized`, and deferring Live bridge connection until the first real tool call so the server can start before Ableton is reachable.
12
+
5
13
  ## v0.1.0 - 2026-03-22
6
14
 
7
15
  - Renamed the published npm package from `laive` to `laive-mcp` because `laive` is already taken on npm. The Ableton-side control surface name remains `laive`.
@@ -11,3 +19,4 @@
11
19
  - Added shipping and staging for the prebuilt `laive-sidecar.amxd` device.
12
20
  - Added `laive mcp-config` for local and published MCP client configuration output.
13
21
  - Added publish and release tooling, including `AGENTS.md`, `scripts/release.mjs`, and `scripts/version-workspaces.mjs`.
22
+
package/bin/laive.mjs CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "laive-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Local MCP, install tooling, and helper assets for controlling Ableton Live.",
5
5
  "license": "GPL-3.0-only",
6
6
  "type": "module",
@@ -13,7 +13,9 @@
13
13
  "publishConfig": {
14
14
  "access": "public"
15
15
  },
16
- "bin": "./bin/laive.mjs",
16
+ "bin": {
17
+ "laive-mcp": "bin/laive.mjs"
18
+ },
17
19
  "files": [
18
20
  "bin/",
19
21
  "README.md",
@@ -55,7 +55,11 @@ export class BridgeClient extends EventEmitter {
55
55
 
56
56
  this.socket.on("data", (chunk) => parser.push(Buffer.from(chunk)));
57
57
  this.socket.on("end", () => parser.end());
58
- this.socket.on("error", (error) => this.emit("error", error));
58
+ this.socket.on("error", (error) => {
59
+ if (this.listenerCount("error") > 0) {
60
+ this.emit("error", error);
61
+ }
62
+ });
59
63
  this.socket.on("close", () => {
60
64
  this.socket = null;
61
65
  this.emit("close");
@@ -44,7 +44,7 @@ async function createSession(options) {
44
44
  return LaiveFixtureSession.create();
45
45
  }
46
46
 
47
- return LaiveBridgeSession.connect({
47
+ return LaiveBridgeSession.createLazy({
48
48
  host: options.host,
49
49
  port: options.port
50
50
  });
@@ -55,7 +55,7 @@ async function main() {
55
55
  const session = await createSession(options);
56
56
  const server = new LaiveMcpServer({
57
57
  stateAdapter: createStateAdapter(session),
58
- bridgeAdapter: createBridgeAdapter(session.bridgeClient),
58
+ bridgeAdapter: createBridgeAdapter(session),
59
59
  policyAdapter: createAllowAllPolicyAdapter()
60
60
  });
61
61
  let lineReader = null;
@@ -101,7 +101,9 @@ async function main() {
101
101
  }
102
102
 
103
103
  const response = await server.safeHandleRpcMessage(message);
104
- process.stdout.write(`${JSON.stringify(response)}\n`);
104
+ if (response) {
105
+ process.stdout.write(`${JSON.stringify(response)}\n`);
106
+ }
105
107
  }
106
108
 
107
109
  await session.close();
@@ -1,3 +1,4 @@
1
+ import rootPackage from "../../../package.json" with { type: "json" };
1
2
  import { ToolRegistry } from "./tool-registry.js";
2
3
  import { buildDefaultTools } from "./default-tools.js";
3
4
  import { McpServerError, toErrorShape } from "./errors.js";
@@ -6,7 +7,7 @@ export class LaiveMcpServer {
6
7
  constructor({ stateAdapter, bridgeAdapter, policyAdapter, serverInfo } = {}) {
7
8
  this.serverInfo = serverInfo ?? {
8
9
  name: "laive-mcp",
9
- version: "0.1.0"
10
+ version: rootPackage.version
10
11
  };
11
12
  this.stateAdapter = stateAdapter ?? createUnsupportedAdapter("state");
12
13
  this.bridgeAdapter = bridgeAdapter ?? createUnsupportedAdapter("bridge");
@@ -39,6 +40,39 @@ export class LaiveMcpServer {
39
40
  throw new McpServerError("invalid_request", "Message must be an object");
40
41
  }
41
42
 
43
+ if (message.method === "initialize") {
44
+ const requestedProtocolVersion =
45
+ typeof message.params?.protocolVersion === "string"
46
+ ? message.params.protocolVersion
47
+ : null;
48
+
49
+ return {
50
+ jsonrpc: "2.0",
51
+ id: message.id ?? null,
52
+ result: {
53
+ protocolVersion: requestedProtocolVersion ?? "2024-11-05",
54
+ capabilities: {
55
+ tools: {
56
+ listChanged: false
57
+ }
58
+ },
59
+ serverInfo: this.serverInfo
60
+ }
61
+ };
62
+ }
63
+
64
+ if (message.method === "ping") {
65
+ return {
66
+ jsonrpc: "2.0",
67
+ id: message.id ?? null,
68
+ result: {}
69
+ };
70
+ }
71
+
72
+ if (typeof message.method === "string" && message.method.startsWith("notifications/")) {
73
+ return null;
74
+ }
75
+
42
76
  if (message.method === "tools/list") {
43
77
  return {
44
78
  jsonrpc: "2.0",
@@ -1,3 +1,4 @@
1
+ import rootPackage from "../../../package.json" with { type: "json" };
1
2
  import {
2
3
  BridgeClient,
3
4
  BridgeServer,
@@ -60,7 +61,7 @@ function normalizeTrack(track) {
60
61
  function toRuntimeSnapshot({ liveVersion, capabilities, song, scenes, tracks }) {
61
62
  return {
62
63
  observed_at: new Date().toISOString(),
63
- bridge_version: "0.1.0",
64
+ bridge_version: rootPackage.version,
64
65
  live_version: liveVersion,
65
66
  application: parseLiveVersion(liveVersion),
66
67
  song: {
@@ -157,12 +158,23 @@ export function createAllowAllPolicyAdapter() {
157
158
  };
158
159
  }
159
160
 
160
- export function createBridgeAdapter(bridgeClient) {
161
+ async function resolveBridgeClient(target) {
162
+ if (target && typeof target.ensureConnected === "function") {
163
+ await target.ensureConnected();
164
+ return target.bridgeClient;
165
+ }
166
+
167
+ return target;
168
+ }
169
+
170
+ export function createBridgeAdapter(target) {
161
171
  return {
162
172
  async getCapabilities() {
173
+ const bridgeClient = await resolveBridgeClient(target);
163
174
  return (await bridgeClient.request("capabilities")).result;
164
175
  },
165
176
  async setTempo(tempo, options = {}) {
177
+ const bridgeClient = await resolveBridgeClient(target);
166
178
  return (
167
179
  await bridgeClient.request(
168
180
  "set",
@@ -173,6 +185,7 @@ export function createBridgeAdapter(bridgeClient) {
173
185
  ).result;
174
186
  },
175
187
  async createTrack(kind, options = {}) {
188
+ const bridgeClient = await resolveBridgeClient(target);
176
189
  const result = (
177
190
  await bridgeClient.request(
178
191
  "call",
@@ -188,6 +201,7 @@ export function createBridgeAdapter(bridgeClient) {
188
201
  };
189
202
  },
190
203
  async createClip(payload) {
204
+ const bridgeClient = await resolveBridgeClient(target);
191
205
  const result = (
192
206
  await bridgeClient.request(
193
207
  "call",
@@ -208,6 +222,7 @@ export function createBridgeAdapter(bridgeClient) {
208
222
  };
209
223
  },
210
224
  async setParameter(payload, options = {}) {
225
+ const bridgeClient = await resolveBridgeClient(target);
211
226
  const result = (
212
227
  await bridgeClient.request(
213
228
  "set",
@@ -246,6 +261,9 @@ export function createStateAdapter(session) {
246
261
 
247
262
  return {
248
263
  async getProjectSummary() {
264
+ if (typeof session.ensureConnected === "function") {
265
+ await session.ensureConnected();
266
+ }
249
267
  const summary = session.stateEngine.query.summarizeProject();
250
268
  return {
251
269
  ...summary,
@@ -254,6 +272,9 @@ export function createStateAdapter(session) {
254
272
  };
255
273
  },
256
274
  async getSelectedContext() {
275
+ if (typeof session.ensureConnected === "function") {
276
+ await session.ensureConnected();
277
+ }
257
278
  const context = session.stateEngine.query.getSelectedContext() ?? {};
258
279
  return {
259
280
  stateVersion: stateVersion(),
@@ -261,9 +282,15 @@ export function createStateAdapter(session) {
261
282
  };
262
283
  },
263
284
  async listTracks() {
285
+ if (typeof session.ensureConnected === "function") {
286
+ await session.ensureConnected();
287
+ }
264
288
  return trackList();
265
289
  },
266
290
  async getTrackDetails(target) {
291
+ if (typeof session.ensureConnected === "function") {
292
+ await session.ensureConnected();
293
+ }
267
294
  const track =
268
295
  session.stateEngine.getState().tracks[target] ?? session.stateEngine.query.findTrack(target);
269
296
 
@@ -280,6 +307,9 @@ export function createStateAdapter(session) {
280
307
  };
281
308
  },
282
309
  async getDeviceTree(trackId) {
310
+ if (typeof session.ensureConnected === "function") {
311
+ await session.ensureConnected();
312
+ }
283
313
  const details = session.stateEngine.query.getTrackDetails(trackId);
284
314
  if (!details) {
285
315
  throw new Error(`Track not found: ${trackId}`);
@@ -292,6 +322,9 @@ export function createStateAdapter(session) {
292
322
  };
293
323
  },
294
324
  async refreshState(target) {
325
+ if (typeof session.ensureConnected === "function") {
326
+ await session.ensureConnected();
327
+ }
295
328
  const previousStateVersion = stateVersion();
296
329
  await session.syncSnapshot();
297
330
  return {
@@ -338,6 +371,10 @@ export class LaiveBridgeSession {
338
371
  return session;
339
372
  }
340
373
 
374
+ static createLazy(options = {}) {
375
+ return new LaiveLazySession(options);
376
+ }
377
+
341
378
  async start() {
342
379
  this.bridgeClient.on("event", this.boundEventHandler);
343
380
  await Promise.all([
@@ -394,6 +431,58 @@ export class LaiveFixtureSession extends LaiveBridgeSession {
394
431
  }
395
432
  }
396
433
 
434
+ export class LaiveLazySession {
435
+ constructor({
436
+ host = process.env.LAIVE_BRIDGE_HOST ?? "127.0.0.1",
437
+ port = Number.parseInt(process.env.LAIVE_BRIDGE_PORT ?? "7612", 10),
438
+ clientId = process.env.LAIVE_BRIDGE_CLIENT_ID ?? "laive-mcp-session",
439
+ socketFactory = null
440
+ } = {}) {
441
+ this.connectionOptions = {
442
+ host,
443
+ port,
444
+ clientId,
445
+ socketFactory
446
+ };
447
+ this.stateEngine = createStateEngine();
448
+ this.bridgeClient = null;
449
+ this.activeSession = null;
450
+ this.connectingPromise = null;
451
+ }
452
+
453
+ async ensureConnected() {
454
+ if (this.activeSession) {
455
+ return this.activeSession;
456
+ }
457
+
458
+ if (!this.connectingPromise) {
459
+ this.connectingPromise = LaiveBridgeSession.connect(this.connectionOptions)
460
+ .then((session) => {
461
+ this.activeSession = session;
462
+ this.bridgeClient = session.bridgeClient;
463
+ this.stateEngine = session.stateEngine;
464
+ return session;
465
+ })
466
+ .finally(() => {
467
+ this.connectingPromise = null;
468
+ });
469
+ }
470
+
471
+ return await this.connectingPromise;
472
+ }
473
+
474
+ async syncSnapshot() {
475
+ const session = await this.ensureConnected();
476
+ return await session.syncSnapshot();
477
+ }
478
+
479
+ async close() {
480
+ if (this.activeSession) {
481
+ await this.activeSession.close();
482
+ }
483
+ }
484
+ }
485
+
397
486
  function createLoopbackSocketPair() {
398
487
  const serverSocket = new FakeSocket();
399
488
  const clientSocket = new FakeSocket();