@mixofreality/live-mcp 1.0.0 → 1.1.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.
- package/dist/index.js +343 -25
- package/package.json +16 -7
- package/dist/__tests__/bridge-client.test.d.ts +0 -1
- package/dist/__tests__/bridge-client.test.js +0 -110
- package/dist/bridge-client.d.ts +0 -16
- package/dist/bridge-client.js +0 -91
- package/dist/index.d.ts +0 -2
- package/dist/tools/call-function.d.ts +0 -3
- package/dist/tools/call-function.js +0 -31
- package/dist/tools/get-children.d.ts +0 -3
- package/dist/tools/get-children.js +0 -21
- package/dist/tools/get-property.d.ts +0 -3
- package/dist/tools/get-property.js +0 -23
- package/dist/tools/index.d.ts +0 -3
- package/dist/tools/index.js +0 -14
- package/dist/tools/observe.d.ts +0 -3
- package/dist/tools/observe.js +0 -23
- package/dist/tools/set-property.d.ts +0 -3
- package/dist/tools/set-property.js +0 -26
- package/dist/tools/unobserve.d.ts +0 -3
- package/dist/tools/unobserve.js +0 -19
package/dist/index.js
CHANGED
|
@@ -1,32 +1,350 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
|
|
7
|
+
// src/bridge-client.ts
|
|
8
|
+
import * as net from "net";
|
|
9
|
+
import { randomUUID } from "crypto";
|
|
10
|
+
|
|
11
|
+
// ../../../shared/max4live-nodescript-ts/dist/bridge-protocol.js
|
|
12
|
+
function isBridgeNotification(msg) {
|
|
13
|
+
return "notification" in msg;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/bridge-client.ts
|
|
17
|
+
var BridgeClient = class {
|
|
18
|
+
constructor(host, port) {
|
|
19
|
+
this.host = host;
|
|
20
|
+
this.port = port;
|
|
21
|
+
}
|
|
22
|
+
socket = null;
|
|
23
|
+
buffer = "";
|
|
24
|
+
pending = /* @__PURE__ */ new Map();
|
|
25
|
+
notificationListeners = [];
|
|
26
|
+
async connect() {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const socket = net.createConnection({ host: this.host, port: this.port });
|
|
29
|
+
socket.once("connect", () => {
|
|
30
|
+
this.socket = socket;
|
|
31
|
+
resolve();
|
|
32
|
+
});
|
|
33
|
+
socket.once("error", reject);
|
|
34
|
+
socket.on("data", (data) => this.handleData(data.toString()));
|
|
35
|
+
socket.on("close", () => {
|
|
36
|
+
this.socket = null;
|
|
37
|
+
for (const [, { reject: rej }] of this.pending) {
|
|
38
|
+
rej(new Error("Bridge connection closed"));
|
|
39
|
+
}
|
|
40
|
+
this.pending.clear();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async disconnect() {
|
|
45
|
+
if (this.socket) {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
this.socket.end(() => resolve());
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
isConnected() {
|
|
52
|
+
return this.socket !== null && !this.socket.destroyed;
|
|
53
|
+
}
|
|
54
|
+
async request(method, params) {
|
|
55
|
+
if (!this.isConnected()) {
|
|
56
|
+
throw new Error("Not connected to bridge");
|
|
57
|
+
}
|
|
58
|
+
const id = randomUUID();
|
|
59
|
+
const request = JSON.stringify({ id, method, params });
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
this.pending.set(id, { resolve, reject });
|
|
62
|
+
this.socket.write(request + "\n");
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
onNotification(listener) {
|
|
66
|
+
this.notificationListeners.push(listener);
|
|
67
|
+
}
|
|
68
|
+
handleData(data) {
|
|
69
|
+
this.buffer += data;
|
|
70
|
+
const lines = this.buffer.split("\n");
|
|
71
|
+
this.buffer = lines.pop();
|
|
72
|
+
for (const line of lines) {
|
|
73
|
+
if (!line.trim()) continue;
|
|
74
|
+
let msg;
|
|
75
|
+
try {
|
|
76
|
+
msg = JSON.parse(line);
|
|
77
|
+
} catch {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (isBridgeNotification(msg)) {
|
|
81
|
+
for (const listener of this.notificationListeners) {
|
|
82
|
+
listener(msg);
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
const response = msg;
|
|
86
|
+
const pendingReq = this.pending.get(response.id);
|
|
87
|
+
if (pendingReq) {
|
|
88
|
+
this.pending.delete(response.id);
|
|
89
|
+
if (response.error) {
|
|
90
|
+
pendingReq.reject(new Error(response.error.message));
|
|
91
|
+
} else if (response.result) {
|
|
92
|
+
pendingReq.resolve(response.result);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// src/tools/get-property.ts
|
|
101
|
+
import { z } from "zod";
|
|
102
|
+
function registerGetProperty(server2, bridge2) {
|
|
103
|
+
server2.registerTool(
|
|
104
|
+
"get_property",
|
|
105
|
+
{
|
|
106
|
+
title: "Get LOM Property",
|
|
107
|
+
description: 'Read a property from a Live Object Model object. Use LOM paths like "live_set", "live_set tracks 0", "live_set tracks 0 devices 1 parameters 2".',
|
|
108
|
+
inputSchema: {
|
|
109
|
+
path: z.string().describe('LOM path (e.g. "live_set tracks 0")'),
|
|
110
|
+
property: z.string().describe('Property name (e.g. "tempo", "name")')
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
async ({ path, property }) => {
|
|
114
|
+
try {
|
|
115
|
+
const result = await bridge2.request("get_property", {
|
|
116
|
+
path,
|
|
117
|
+
property
|
|
118
|
+
});
|
|
119
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
120
|
+
} catch (error) {
|
|
121
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
122
|
+
return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/tools/set-property.ts
|
|
129
|
+
import { z as z2 } from "zod";
|
|
130
|
+
function registerSetProperty(server2, bridge2) {
|
|
131
|
+
server2.registerTool(
|
|
132
|
+
"set_property",
|
|
133
|
+
{
|
|
134
|
+
title: "Set LOM Property",
|
|
135
|
+
description: 'Set a property on a Live Object Model object. The value should be a JSON string that will be parsed (e.g. "120.0", "\\"hello\\"", "true").',
|
|
136
|
+
inputSchema: {
|
|
137
|
+
path: z2.string().describe('LOM path (e.g. "live_set tracks 0")'),
|
|
138
|
+
property: z2.string().describe('Property name (e.g. "tempo", "name")'),
|
|
139
|
+
value: z2.string().describe('Value as JSON string (e.g. "120.0", "\\"hello\\"", "true")')
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
async ({ path, property, value }) => {
|
|
143
|
+
try {
|
|
144
|
+
const parsed = JSON.parse(value);
|
|
145
|
+
const result = await bridge2.request("set_property", {
|
|
146
|
+
path,
|
|
147
|
+
property,
|
|
148
|
+
value: parsed
|
|
149
|
+
});
|
|
150
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
153
|
+
return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/tools/call-function.ts
|
|
160
|
+
import { z as z3 } from "zod";
|
|
161
|
+
function registerCallFunction(server2, bridge2) {
|
|
162
|
+
server2.registerTool(
|
|
163
|
+
"call_function",
|
|
164
|
+
{
|
|
165
|
+
title: "Call LOM Function",
|
|
166
|
+
description: 'Call a function on a Live Object Model object. Args is an optional JSON array string (e.g. "[1, 2]").',
|
|
167
|
+
inputSchema: {
|
|
168
|
+
path: z3.string().describe('LOM path (e.g. "live_set tracks 0")'),
|
|
169
|
+
function: z3.string().describe('Function name (e.g. "fire", "stop")'),
|
|
170
|
+
args: z3.string().optional().describe('Arguments as JSON array string (e.g. "[1, \\"hello\\"]")')
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
async ({ path, function: fn, args }) => {
|
|
174
|
+
try {
|
|
175
|
+
const parsedArgs = args ? JSON.parse(args) : void 0;
|
|
176
|
+
const result = await bridge2.request("call_function", {
|
|
177
|
+
path,
|
|
178
|
+
function: fn,
|
|
179
|
+
args: parsedArgs
|
|
180
|
+
});
|
|
181
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
182
|
+
} catch (error) {
|
|
183
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
184
|
+
return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/tools/get-children.ts
|
|
191
|
+
import { z as z4 } from "zod";
|
|
192
|
+
function registerGetChildren(server2, bridge2) {
|
|
193
|
+
server2.registerTool(
|
|
194
|
+
"get_children",
|
|
195
|
+
{
|
|
196
|
+
title: "Get LOM Children",
|
|
197
|
+
description: "List the children of a Live Object Model object. Returns child paths for navigation.",
|
|
198
|
+
inputSchema: {
|
|
199
|
+
path: z4.string().describe('LOM path (e.g. "live_set", "live_set tracks 0")')
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
async ({ path }) => {
|
|
203
|
+
try {
|
|
204
|
+
const result = await bridge2.request("get_children", {
|
|
205
|
+
path
|
|
206
|
+
});
|
|
207
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
208
|
+
} catch (error) {
|
|
209
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
210
|
+
return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/tools/observe.ts
|
|
217
|
+
import { z as z5 } from "zod";
|
|
218
|
+
function registerObserve(server2, bridge2) {
|
|
219
|
+
server2.registerTool(
|
|
220
|
+
"observe",
|
|
221
|
+
{
|
|
222
|
+
title: "Observe LOM Property",
|
|
223
|
+
description: "Subscribe to changes on a Live Object Model property. Returns a subscriptionId for later unobserve.",
|
|
224
|
+
inputSchema: {
|
|
225
|
+
path: z5.string().describe('LOM path (e.g. "live_set tracks 0")'),
|
|
226
|
+
property: z5.string().describe('Property name to observe (e.g. "tempo", "name")')
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
async ({ path, property }) => {
|
|
230
|
+
try {
|
|
231
|
+
const result = await bridge2.request("observe", {
|
|
232
|
+
path,
|
|
233
|
+
property
|
|
234
|
+
});
|
|
235
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
236
|
+
} catch (error) {
|
|
237
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
238
|
+
return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/tools/unobserve.ts
|
|
245
|
+
import { z as z6 } from "zod";
|
|
246
|
+
function registerUnobserve(server2, bridge2) {
|
|
247
|
+
server2.registerTool(
|
|
248
|
+
"unobserve",
|
|
249
|
+
{
|
|
250
|
+
title: "Unobserve LOM Property",
|
|
251
|
+
description: "Unsubscribe from a previously observed Live Object Model property using its subscriptionId.",
|
|
252
|
+
inputSchema: {
|
|
253
|
+
subscriptionId: z6.string().describe("Subscription ID returned from observe")
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
async ({ subscriptionId }) => {
|
|
257
|
+
try {
|
|
258
|
+
const result = await bridge2.request("unobserve", { subscriptionId });
|
|
259
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
260
|
+
} catch (error) {
|
|
261
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
262
|
+
return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/tools/index.ts
|
|
269
|
+
function registerAllTools(server2, bridge2) {
|
|
270
|
+
registerGetProperty(server2, bridge2);
|
|
271
|
+
registerSetProperty(server2, bridge2);
|
|
272
|
+
registerCallFunction(server2, bridge2);
|
|
273
|
+
registerGetChildren(server2, bridge2);
|
|
274
|
+
registerObserve(server2, bridge2);
|
|
275
|
+
registerUnobserve(server2, bridge2);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/index.ts
|
|
279
|
+
var BRIDGE_HOST = process.env["LIVE_MCP_BRIDGE_HOST"] ?? "127.0.0.1";
|
|
280
|
+
var BRIDGE_PORT = parseInt(process.env["LIVE_MCP_BRIDGE_PORT"] ?? "19740", 10);
|
|
281
|
+
var server = new McpServer(
|
|
282
|
+
{
|
|
283
|
+
name: "live-mcp",
|
|
284
|
+
version: "1.0.0"
|
|
285
|
+
},
|
|
286
|
+
{
|
|
13
287
|
capabilities: { logging: {} },
|
|
14
|
-
|
|
15
|
-
|
|
288
|
+
instructions: `Live MCP controls Ableton Live through the Live Object Model (LOM).
|
|
289
|
+
Full API reference: https://docs.cycling74.com/apiref/lom/
|
|
290
|
+
User guide: https://docs.cycling74.com/userguide/
|
|
291
|
+
|
|
292
|
+
## Path Syntax
|
|
293
|
+
|
|
294
|
+
Paths use space-separated navigation from root objects:
|
|
295
|
+
- \`live_set\` \u2014 the Song
|
|
296
|
+
- \`live_app\` \u2014 the Application
|
|
297
|
+
- \`live_set tracks 0\` \u2014 first track
|
|
298
|
+
- \`live_set tracks 0 clip_slots 2 clip\` \u2014 clip in slot 2 of track 0
|
|
299
|
+
- \`live_set tracks 0 devices 1 parameters 3\` \u2014 parameter 3 of device 1
|
|
300
|
+
- \`live_set master_track\` \u2014 master track
|
|
301
|
+
- \`live_set return_tracks 0\` \u2014 first return track
|
|
302
|
+
- \`live_set scenes 0\` \u2014 first scene
|
|
303
|
+
|
|
304
|
+
## Object Hierarchy
|
|
305
|
+
|
|
306
|
+
Song (live_set)
|
|
307
|
+
\u251C\u2500\u2500 tracks[] \u2192 Track
|
|
308
|
+
\u2502 \u251C\u2500\u2500 clip_slots[] \u2192 ClipSlot \u2192 clip \u2192 Clip
|
|
309
|
+
\u2502 \u251C\u2500\u2500 devices[] \u2192 Device (or RackDevice, SimplerDevice, PluginDevice, MaxDevice)
|
|
310
|
+
\u2502 \u2502 \u251C\u2500\u2500 parameters[] \u2192 DeviceParameter
|
|
311
|
+
\u2502 \u2502 \u251C\u2500\u2500 chains[] \u2192 Chain \u2192 devices[], mixer_device (racks only)
|
|
312
|
+
\u2502 \u2502 \u251C\u2500\u2500 drum_pads[] \u2192 DrumPad \u2192 chains[] \u2192 DrumChain (drum racks only)
|
|
313
|
+
\u2502 \u2502 \u2514\u2500\u2500 return_chains[] \u2192 Chain (racks only)
|
|
314
|
+
\u2502 \u2514\u2500\u2500 mixer_device \u2192 MixerDevice (volume, panning, sends)
|
|
315
|
+
\u251C\u2500\u2500 return_tracks[] \u2192 Track
|
|
316
|
+
\u251C\u2500\u2500 master_track \u2192 Track
|
|
317
|
+
\u251C\u2500\u2500 scenes[] \u2192 Scene
|
|
318
|
+
\u251C\u2500\u2500 cue_points[] \u2192 CuePoint
|
|
319
|
+
\u2514\u2500\u2500 groove_pool \u2192 GroovePool \u2192 grooves[]
|
|
320
|
+
Application (live_app)
|
|
321
|
+
\u251C\u2500\u2500 view \u2192 Application.View
|
|
322
|
+
\u2514\u2500\u2500 control_surfaces[]
|
|
323
|
+
|
|
324
|
+
## Workflow
|
|
325
|
+
|
|
326
|
+
1. Use get_children to discover what's available at any path
|
|
327
|
+
2. Use get_property / set_property to read/write values
|
|
328
|
+
3. Use call_function to invoke actions (fire, stop, create, delete, etc.)
|
|
329
|
+
4. Consult https://docs.cycling74.com/apiref/lom/<classname>/ for property and function details on specific classes`
|
|
330
|
+
}
|
|
331
|
+
);
|
|
332
|
+
var bridge = new BridgeClient(BRIDGE_HOST, BRIDGE_PORT);
|
|
16
333
|
registerAllTools(server, bridge);
|
|
17
334
|
async function main() {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
335
|
+
try {
|
|
336
|
+
await bridge.connect();
|
|
337
|
+
console.error("Connected to M4L bridge");
|
|
338
|
+
} catch {
|
|
339
|
+
console.error(
|
|
340
|
+
`Warning: Could not connect to M4L bridge at ${BRIDGE_HOST}:${BRIDGE_PORT}. Tools will fail until bridge is available.`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
const transport = new StdioServerTransport();
|
|
344
|
+
await server.connect(transport);
|
|
345
|
+
console.error("Live MCP Server running on stdio");
|
|
28
346
|
}
|
|
29
347
|
main().catch((error) => {
|
|
30
|
-
|
|
31
|
-
|
|
348
|
+
console.error("Fatal error:", error);
|
|
349
|
+
process.exit(1);
|
|
32
350
|
});
|
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mixofreality/live-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"live-mcp": "./dist/index.js"
|
|
8
8
|
},
|
|
9
|
-
"files": [
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
10
12
|
"scripts": {
|
|
11
|
-
"build": "
|
|
13
|
+
"build": "tsup",
|
|
12
14
|
"dev": "node --watch dist/index.js",
|
|
13
15
|
"typecheck": "tsc --noEmit",
|
|
14
16
|
"test": "vitest run",
|
|
@@ -16,7 +18,13 @@
|
|
|
16
18
|
"check": "npm run typecheck && npm run test",
|
|
17
19
|
"prepublishOnly": "npm run check && npm run build"
|
|
18
20
|
},
|
|
19
|
-
"keywords": [
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"ableton",
|
|
24
|
+
"live",
|
|
25
|
+
"max4live",
|
|
26
|
+
"lom"
|
|
27
|
+
],
|
|
20
28
|
"license": "MIT",
|
|
21
29
|
"description": "MCP server for controlling Ableton Live via the Live Object Model",
|
|
22
30
|
"dependencies": {
|
|
@@ -24,9 +32,10 @@
|
|
|
24
32
|
"zod": "^3.25.0"
|
|
25
33
|
},
|
|
26
34
|
"devDependencies": {
|
|
27
|
-
"
|
|
28
|
-
"vitest": "^4.0.16",
|
|
35
|
+
"@mixofreality/max4live-nodescript-ts": "file:../../../shared/max4live-nodescript-ts",
|
|
29
36
|
"@types/node": "^22.0.0",
|
|
30
|
-
"
|
|
37
|
+
"tsup": "^8.5.1",
|
|
38
|
+
"typescript": "~5.9.3",
|
|
39
|
+
"vitest": "^4.0.16"
|
|
31
40
|
}
|
|
32
41
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import * as net from 'node:net';
|
|
3
|
-
import { BridgeClient } from '../bridge-client.js';
|
|
4
|
-
describe('BridgeClient', () => {
|
|
5
|
-
let mockServer;
|
|
6
|
-
let serverPort;
|
|
7
|
-
beforeEach(async () => {
|
|
8
|
-
mockServer = net.createServer();
|
|
9
|
-
await new Promise((resolve) => {
|
|
10
|
-
mockServer.listen(0, '127.0.0.1', () => resolve());
|
|
11
|
-
});
|
|
12
|
-
const addr = mockServer.address();
|
|
13
|
-
serverPort = addr.port;
|
|
14
|
-
});
|
|
15
|
-
afterEach(async () => {
|
|
16
|
-
await new Promise((resolve) => {
|
|
17
|
-
mockServer.close(() => resolve());
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
it('should connect to the bridge server', async () => {
|
|
21
|
-
const serverGotConnection = new Promise((resolve) => {
|
|
22
|
-
mockServer.on('connection', () => resolve());
|
|
23
|
-
});
|
|
24
|
-
const client = new BridgeClient('127.0.0.1', serverPort);
|
|
25
|
-
await client.connect();
|
|
26
|
-
await serverGotConnection;
|
|
27
|
-
expect(client.isConnected()).toBe(true);
|
|
28
|
-
await client.disconnect();
|
|
29
|
-
});
|
|
30
|
-
it('should send a request and receive a response', async () => {
|
|
31
|
-
mockServer.on('connection', (socket) => {
|
|
32
|
-
let buffer = '';
|
|
33
|
-
socket.on('data', (data) => {
|
|
34
|
-
buffer += data.toString();
|
|
35
|
-
const lines = buffer.split('\n');
|
|
36
|
-
buffer = lines.pop();
|
|
37
|
-
for (const line of lines) {
|
|
38
|
-
if (line.trim()) {
|
|
39
|
-
const req = JSON.parse(line);
|
|
40
|
-
const response = JSON.stringify({ id: req.id, result: { value: 120 } });
|
|
41
|
-
socket.write(response + '\n');
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
const client = new BridgeClient('127.0.0.1', serverPort);
|
|
47
|
-
await client.connect();
|
|
48
|
-
const result = await client.request('get_property', {
|
|
49
|
-
path: 'live_set',
|
|
50
|
-
property: 'tempo',
|
|
51
|
-
});
|
|
52
|
-
expect(result).toEqual({ value: 120 });
|
|
53
|
-
await client.disconnect();
|
|
54
|
-
});
|
|
55
|
-
it('should handle bridge error responses', async () => {
|
|
56
|
-
mockServer.on('connection', (socket) => {
|
|
57
|
-
let buffer = '';
|
|
58
|
-
socket.on('data', (data) => {
|
|
59
|
-
buffer += data.toString();
|
|
60
|
-
const lines = buffer.split('\n');
|
|
61
|
-
buffer = lines.pop();
|
|
62
|
-
for (const line of lines) {
|
|
63
|
-
if (line.trim()) {
|
|
64
|
-
const req = JSON.parse(line);
|
|
65
|
-
const response = JSON.stringify({
|
|
66
|
-
id: req.id,
|
|
67
|
-
error: { code: -1, message: 'Object not found' },
|
|
68
|
-
});
|
|
69
|
-
socket.write(response + '\n');
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
const client = new BridgeClient('127.0.0.1', serverPort);
|
|
75
|
-
await client.connect();
|
|
76
|
-
await expect(client.request('get_property', { path: 'live_set tracks 99', property: 'name' })).rejects.toThrow('Object not found');
|
|
77
|
-
await client.disconnect();
|
|
78
|
-
});
|
|
79
|
-
it('should emit notifications for property changes', async () => {
|
|
80
|
-
mockServer.on('connection', (socket) => {
|
|
81
|
-
setTimeout(() => {
|
|
82
|
-
const notification = JSON.stringify({
|
|
83
|
-
notification: 'property_changed',
|
|
84
|
-
subscriptionId: 'sub-1',
|
|
85
|
-
path: 'live_set',
|
|
86
|
-
property: 'tempo',
|
|
87
|
-
value: 140,
|
|
88
|
-
});
|
|
89
|
-
socket.write(notification + '\n');
|
|
90
|
-
}, 50);
|
|
91
|
-
});
|
|
92
|
-
const client = new BridgeClient('127.0.0.1', serverPort);
|
|
93
|
-
await client.connect();
|
|
94
|
-
const notification = await new Promise((resolve) => {
|
|
95
|
-
client.onNotification((n) => resolve(n));
|
|
96
|
-
});
|
|
97
|
-
expect(notification).toEqual({
|
|
98
|
-
notification: 'property_changed',
|
|
99
|
-
subscriptionId: 'sub-1',
|
|
100
|
-
path: 'live_set',
|
|
101
|
-
property: 'tempo',
|
|
102
|
-
value: 140,
|
|
103
|
-
});
|
|
104
|
-
await client.disconnect();
|
|
105
|
-
});
|
|
106
|
-
it('should report disconnected when not connected', () => {
|
|
107
|
-
const client = new BridgeClient('127.0.0.1', 0);
|
|
108
|
-
expect(client.isConnected()).toBe(false);
|
|
109
|
-
});
|
|
110
|
-
});
|
package/dist/bridge-client.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { BridgeMethod, BridgeRequestParams, BridgeNotification, BridgeResult } from '@mixofreality/max4live-nodescript-ts';
|
|
2
|
-
export declare class BridgeClient {
|
|
3
|
-
private readonly host;
|
|
4
|
-
private readonly port;
|
|
5
|
-
private socket;
|
|
6
|
-
private buffer;
|
|
7
|
-
private pending;
|
|
8
|
-
private notificationListeners;
|
|
9
|
-
constructor(host: string, port: number);
|
|
10
|
-
connect(): Promise<void>;
|
|
11
|
-
disconnect(): Promise<void>;
|
|
12
|
-
isConnected(): boolean;
|
|
13
|
-
request(method: BridgeMethod, params: BridgeRequestParams): Promise<BridgeResult>;
|
|
14
|
-
onNotification(listener: (notification: BridgeNotification) => void): void;
|
|
15
|
-
private handleData;
|
|
16
|
-
}
|
package/dist/bridge-client.js
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import * as net from 'node:net';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { isBridgeNotification } from '@mixofreality/max4live-nodescript-ts';
|
|
4
|
-
export class BridgeClient {
|
|
5
|
-
host;
|
|
6
|
-
port;
|
|
7
|
-
socket = null;
|
|
8
|
-
buffer = '';
|
|
9
|
-
pending = new Map();
|
|
10
|
-
notificationListeners = [];
|
|
11
|
-
constructor(host, port) {
|
|
12
|
-
this.host = host;
|
|
13
|
-
this.port = port;
|
|
14
|
-
}
|
|
15
|
-
async connect() {
|
|
16
|
-
return new Promise((resolve, reject) => {
|
|
17
|
-
const socket = net.createConnection({ host: this.host, port: this.port });
|
|
18
|
-
socket.once('connect', () => {
|
|
19
|
-
this.socket = socket;
|
|
20
|
-
resolve();
|
|
21
|
-
});
|
|
22
|
-
socket.once('error', reject);
|
|
23
|
-
socket.on('data', (data) => this.handleData(data.toString()));
|
|
24
|
-
socket.on('close', () => {
|
|
25
|
-
this.socket = null;
|
|
26
|
-
for (const [, { reject: rej }] of this.pending) {
|
|
27
|
-
rej(new Error('Bridge connection closed'));
|
|
28
|
-
}
|
|
29
|
-
this.pending.clear();
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
async disconnect() {
|
|
34
|
-
if (this.socket) {
|
|
35
|
-
return new Promise((resolve) => {
|
|
36
|
-
this.socket.end(() => resolve());
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
isConnected() {
|
|
41
|
-
return this.socket !== null && !this.socket.destroyed;
|
|
42
|
-
}
|
|
43
|
-
async request(method, params) {
|
|
44
|
-
if (!this.isConnected()) {
|
|
45
|
-
throw new Error('Not connected to bridge');
|
|
46
|
-
}
|
|
47
|
-
const id = randomUUID();
|
|
48
|
-
const request = JSON.stringify({ id, method, params });
|
|
49
|
-
return new Promise((resolve, reject) => {
|
|
50
|
-
this.pending.set(id, { resolve, reject });
|
|
51
|
-
this.socket.write(request + '\n');
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
onNotification(listener) {
|
|
55
|
-
this.notificationListeners.push(listener);
|
|
56
|
-
}
|
|
57
|
-
handleData(data) {
|
|
58
|
-
this.buffer += data;
|
|
59
|
-
const lines = this.buffer.split('\n');
|
|
60
|
-
this.buffer = lines.pop();
|
|
61
|
-
for (const line of lines) {
|
|
62
|
-
if (!line.trim())
|
|
63
|
-
continue;
|
|
64
|
-
let msg;
|
|
65
|
-
try {
|
|
66
|
-
msg = JSON.parse(line);
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (isBridgeNotification(msg)) {
|
|
72
|
-
for (const listener of this.notificationListeners) {
|
|
73
|
-
listener(msg);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
const response = msg;
|
|
78
|
-
const pendingReq = this.pending.get(response.id);
|
|
79
|
-
if (pendingReq) {
|
|
80
|
-
this.pending.delete(response.id);
|
|
81
|
-
if (response.error) {
|
|
82
|
-
pendingReq.reject(new Error(response.error.message));
|
|
83
|
-
}
|
|
84
|
-
else if (response.result) {
|
|
85
|
-
pendingReq.resolve(response.result);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
package/dist/index.d.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
export function registerCallFunction(server, bridge) {
|
|
3
|
-
server.registerTool('call_function', {
|
|
4
|
-
title: 'Call LOM Function',
|
|
5
|
-
description: 'Call a function on a Live Object Model object. Args is an optional JSON array string (e.g. "[1, 2]").',
|
|
6
|
-
inputSchema: {
|
|
7
|
-
path: z.string().describe('LOM path (e.g. "live_set tracks 0")'),
|
|
8
|
-
function: z.string().describe('Function name (e.g. "fire", "stop")'),
|
|
9
|
-
args: z
|
|
10
|
-
.string()
|
|
11
|
-
.optional()
|
|
12
|
-
.describe('Arguments as JSON array string (e.g. "[1, \\"hello\\"]")'),
|
|
13
|
-
},
|
|
14
|
-
}, async ({ path, function: fn, args }) => {
|
|
15
|
-
try {
|
|
16
|
-
const parsedArgs = args
|
|
17
|
-
? JSON.parse(args)
|
|
18
|
-
: undefined;
|
|
19
|
-
const result = await bridge.request('call_function', {
|
|
20
|
-
path: path,
|
|
21
|
-
function: fn,
|
|
22
|
-
args: parsedArgs,
|
|
23
|
-
});
|
|
24
|
-
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
25
|
-
}
|
|
26
|
-
catch (error) {
|
|
27
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
28
|
-
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
export function registerGetChildren(server, bridge) {
|
|
3
|
-
server.registerTool('get_children', {
|
|
4
|
-
title: 'Get LOM Children',
|
|
5
|
-
description: 'List the children of a Live Object Model object. Returns child paths for navigation.',
|
|
6
|
-
inputSchema: {
|
|
7
|
-
path: z.string().describe('LOM path (e.g. "live_set", "live_set tracks 0")'),
|
|
8
|
-
},
|
|
9
|
-
}, async ({ path }) => {
|
|
10
|
-
try {
|
|
11
|
-
const result = await bridge.request('get_children', {
|
|
12
|
-
path: path,
|
|
13
|
-
});
|
|
14
|
-
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
15
|
-
}
|
|
16
|
-
catch (error) {
|
|
17
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
18
|
-
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
export function registerGetProperty(server, bridge) {
|
|
3
|
-
server.registerTool('get_property', {
|
|
4
|
-
title: 'Get LOM Property',
|
|
5
|
-
description: 'Read a property from a Live Object Model object. Use LOM paths like "live_set", "live_set tracks 0", "live_set tracks 0 devices 1 parameters 2".',
|
|
6
|
-
inputSchema: {
|
|
7
|
-
path: z.string().describe('LOM path (e.g. "live_set tracks 0")'),
|
|
8
|
-
property: z.string().describe('Property name (e.g. "tempo", "name")'),
|
|
9
|
-
},
|
|
10
|
-
}, async ({ path, property }) => {
|
|
11
|
-
try {
|
|
12
|
-
const result = await bridge.request('get_property', {
|
|
13
|
-
path: path,
|
|
14
|
-
property,
|
|
15
|
-
});
|
|
16
|
-
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
17
|
-
}
|
|
18
|
-
catch (error) {
|
|
19
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
-
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
}
|
package/dist/tools/index.d.ts
DELETED
package/dist/tools/index.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { registerGetProperty } from './get-property.js';
|
|
2
|
-
import { registerSetProperty } from './set-property.js';
|
|
3
|
-
import { registerCallFunction } from './call-function.js';
|
|
4
|
-
import { registerGetChildren } from './get-children.js';
|
|
5
|
-
import { registerObserve } from './observe.js';
|
|
6
|
-
import { registerUnobserve } from './unobserve.js';
|
|
7
|
-
export function registerAllTools(server, bridge) {
|
|
8
|
-
registerGetProperty(server, bridge);
|
|
9
|
-
registerSetProperty(server, bridge);
|
|
10
|
-
registerCallFunction(server, bridge);
|
|
11
|
-
registerGetChildren(server, bridge);
|
|
12
|
-
registerObserve(server, bridge);
|
|
13
|
-
registerUnobserve(server, bridge);
|
|
14
|
-
}
|
package/dist/tools/observe.d.ts
DELETED
package/dist/tools/observe.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
export function registerObserve(server, bridge) {
|
|
3
|
-
server.registerTool('observe', {
|
|
4
|
-
title: 'Observe LOM Property',
|
|
5
|
-
description: 'Subscribe to changes on a Live Object Model property. Returns a subscriptionId for later unobserve.',
|
|
6
|
-
inputSchema: {
|
|
7
|
-
path: z.string().describe('LOM path (e.g. "live_set tracks 0")'),
|
|
8
|
-
property: z.string().describe('Property name to observe (e.g. "tempo", "name")'),
|
|
9
|
-
},
|
|
10
|
-
}, async ({ path, property }) => {
|
|
11
|
-
try {
|
|
12
|
-
const result = await bridge.request('observe', {
|
|
13
|
-
path: path,
|
|
14
|
-
property,
|
|
15
|
-
});
|
|
16
|
-
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
17
|
-
}
|
|
18
|
-
catch (error) {
|
|
19
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
-
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
export function registerSetProperty(server, bridge) {
|
|
3
|
-
server.registerTool('set_property', {
|
|
4
|
-
title: 'Set LOM Property',
|
|
5
|
-
description: 'Set a property on a Live Object Model object. The value should be a JSON string that will be parsed (e.g. "120.0", "\\"hello\\"", "true").',
|
|
6
|
-
inputSchema: {
|
|
7
|
-
path: z.string().describe('LOM path (e.g. "live_set tracks 0")'),
|
|
8
|
-
property: z.string().describe('Property name (e.g. "tempo", "name")'),
|
|
9
|
-
value: z.string().describe('Value as JSON string (e.g. "120.0", "\\"hello\\"", "true")'),
|
|
10
|
-
},
|
|
11
|
-
}, async ({ path, property, value }) => {
|
|
12
|
-
try {
|
|
13
|
-
const parsed = JSON.parse(value);
|
|
14
|
-
const result = await bridge.request('set_property', {
|
|
15
|
-
path: path,
|
|
16
|
-
property,
|
|
17
|
-
value: parsed,
|
|
18
|
-
});
|
|
19
|
-
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
20
|
-
}
|
|
21
|
-
catch (error) {
|
|
22
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
23
|
-
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
}
|
package/dist/tools/unobserve.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
export function registerUnobserve(server, bridge) {
|
|
3
|
-
server.registerTool('unobserve', {
|
|
4
|
-
title: 'Unobserve LOM Property',
|
|
5
|
-
description: 'Unsubscribe from a previously observed Live Object Model property using its subscriptionId.',
|
|
6
|
-
inputSchema: {
|
|
7
|
-
subscriptionId: z.string().describe('Subscription ID returned from observe'),
|
|
8
|
-
},
|
|
9
|
-
}, async ({ subscriptionId }) => {
|
|
10
|
-
try {
|
|
11
|
-
const result = await bridge.request('unobserve', { subscriptionId });
|
|
12
|
-
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
13
|
-
}
|
|
14
|
-
catch (error) {
|
|
15
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
16
|
-
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
17
|
-
}
|
|
18
|
-
});
|
|
19
|
-
}
|