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.
|
|
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":
|
|
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) =>
|
|
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.
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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();
|