laive-mcp 0.1.2 → 0.1.4

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.4 - 2026-03-22
6
+
7
+ - Fixed MCP tool schema advertising so argument-bearing tools like `set_tempo`, `get_track_details`, `get_device_tree`, `create_clip`, and `set_parameter` now publish explicit JSON Schemas through `tools/list` instead of empty input objects, allowing Codex clients to send required parameters.
8
+
9
+ ## v0.1.3 - 2026-03-22
10
+
11
+ - Fixed MCP `tools/call` responses to return proper `CallToolResult` envelopes with `content`, `structuredContent`, and `isError`, so Codex clients accept the responses instead of rejecting them as an unexpected type.
12
+
5
13
  ## v0.1.2 - 2026-03-22
6
14
 
7
15
  - 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "laive-mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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",
@@ -1,5 +1,20 @@
1
1
  import { McpServerError } from "./errors.js";
2
2
 
3
+ const EMPTY_OBJECT_SCHEMA = {
4
+ type: "object",
5
+ properties: {},
6
+ additionalProperties: false
7
+ };
8
+
9
+ function createObjectSchema({ properties = {}, required = [] } = {}) {
10
+ return {
11
+ type: "object",
12
+ properties,
13
+ required,
14
+ additionalProperties: false
15
+ };
16
+ }
17
+
3
18
  function requireString(value, fieldName) {
4
19
  if (typeof value !== "string" || value.length === 0) {
5
20
  throw new McpServerError(
@@ -25,6 +40,7 @@ export function buildDefaultTools({ stateAdapter, bridgeAdapter, policyAdapter }
25
40
  {
26
41
  name: "get_project_summary",
27
42
  description: "Return a compact summary of the current Live set state.",
43
+ inputSchema: EMPTY_OBJECT_SCHEMA,
28
44
  async execute() {
29
45
  const summary = await stateAdapter.getProjectSummary();
30
46
  return {
@@ -41,6 +57,7 @@ export function buildDefaultTools({ stateAdapter, bridgeAdapter, policyAdapter }
41
57
  {
42
58
  name: "get_selected_context",
43
59
  description: "Return the selected track, scene, clip, and device context.",
60
+ inputSchema: EMPTY_OBJECT_SCHEMA,
44
61
  async execute() {
45
62
  const context = await stateAdapter.getSelectedContext();
46
63
  return {
@@ -59,6 +76,7 @@ export function buildDefaultTools({ stateAdapter, bridgeAdapter, policyAdapter }
59
76
  {
60
77
  name: "list_tracks",
61
78
  description: "List tracks in compact form.",
79
+ inputSchema: EMPTY_OBJECT_SCHEMA,
62
80
  async execute() {
63
81
  const tracks = await stateAdapter.listTracks();
64
82
  return {
@@ -75,6 +93,23 @@ export function buildDefaultTools({ stateAdapter, bridgeAdapter, policyAdapter }
75
93
  {
76
94
  name: "get_track_details",
77
95
  description: "Return detailed state for a track identified by ID, name, or index.",
96
+ inputSchema: createObjectSchema({
97
+ properties: {
98
+ id: {
99
+ type: "string",
100
+ description: "Track identifier, for example `track:7`."
101
+ },
102
+ name: {
103
+ type: "string",
104
+ description: "Exact track name."
105
+ },
106
+ index: {
107
+ type: "integer",
108
+ minimum: 0,
109
+ description: "Zero-based visible-track index."
110
+ }
111
+ }
112
+ }),
78
113
  async execute(args) {
79
114
  const target = args.id ?? args.name ?? args.index;
80
115
  if (target === undefined) {
@@ -99,6 +134,15 @@ export function buildDefaultTools({ stateAdapter, bridgeAdapter, policyAdapter }
99
134
  {
100
135
  name: "get_device_tree",
101
136
  description: "Return device state for a track.",
137
+ inputSchema: createObjectSchema({
138
+ properties: {
139
+ trackId: {
140
+ type: "string",
141
+ description: "Track identifier, for example `track:7`."
142
+ }
143
+ },
144
+ required: ["trackId"]
145
+ }),
102
146
  async execute(args) {
103
147
  const trackId = args.trackId ?? args.track ?? args.id;
104
148
  requireString(trackId, "trackId");
@@ -117,6 +161,20 @@ export function buildDefaultTools({ stateAdapter, bridgeAdapter, policyAdapter }
117
161
  {
118
162
  name: "set_tempo",
119
163
  description: "Update the current song tempo.",
164
+ inputSchema: createObjectSchema({
165
+ properties: {
166
+ tempo: {
167
+ type: "number",
168
+ exclusiveMinimum: 0,
169
+ description: "Target song tempo in BPM."
170
+ },
171
+ dryRun: {
172
+ type: "boolean",
173
+ description: "If true, preview the action without mutating Live."
174
+ }
175
+ },
176
+ required: ["tempo"]
177
+ }),
120
178
  async execute(args) {
121
179
  const nextTempo = Number(args.tempo);
122
180
  if (!Number.isFinite(nextTempo) || nextTempo <= 0) {
@@ -139,6 +197,19 @@ export function buildDefaultTools({ stateAdapter, bridgeAdapter, policyAdapter }
139
197
  {
140
198
  name: "create_track",
141
199
  description: "Create a new track.",
200
+ inputSchema: createObjectSchema({
201
+ properties: {
202
+ kind: {
203
+ type: "string",
204
+ enum: ["midi", "audio"],
205
+ description: "Track type to create."
206
+ },
207
+ dryRun: {
208
+ type: "boolean",
209
+ description: "If true, preview the action without mutating Live."
210
+ }
211
+ }
212
+ }),
142
213
  async execute(args) {
143
214
  const kind = args.kind ?? "midi";
144
215
  await policyAdapter.assertAllowed("create_track", args);
@@ -157,6 +228,33 @@ export function buildDefaultTools({ stateAdapter, bridgeAdapter, policyAdapter }
157
228
  {
158
229
  name: "create_clip",
159
230
  description: "Create a MIDI clip on a target track and slot.",
231
+ inputSchema: createObjectSchema({
232
+ properties: {
233
+ trackId: {
234
+ type: "string",
235
+ description: "Track identifier, for example `track:7`."
236
+ },
237
+ slotIndex: {
238
+ type: "integer",
239
+ minimum: 0,
240
+ description: "Zero-based session slot index on the target track."
241
+ },
242
+ lengthBeats: {
243
+ type: "number",
244
+ exclusiveMinimum: 0,
245
+ description: "Clip length in beats. Defaults to 4."
246
+ },
247
+ name: {
248
+ type: "string",
249
+ description: "Optional clip name."
250
+ },
251
+ dryRun: {
252
+ type: "boolean",
253
+ description: "If true, preview the action without mutating Live."
254
+ }
255
+ },
256
+ required: ["trackId", "slotIndex"]
257
+ }),
160
258
  async execute(args) {
161
259
  requireString(args.trackId, "trackId");
162
260
  if (!Number.isInteger(args.slotIndex) || args.slotIndex < 0) {
@@ -185,6 +283,31 @@ export function buildDefaultTools({ stateAdapter, bridgeAdapter, policyAdapter }
185
283
  {
186
284
  name: "set_parameter",
187
285
  description: "Set a device parameter by track/device/parameter identifiers.",
286
+ inputSchema: createObjectSchema({
287
+ properties: {
288
+ trackId: {
289
+ type: "string",
290
+ description: "Track identifier containing the target device."
291
+ },
292
+ deviceId: {
293
+ type: "string",
294
+ description: "Device identifier containing the target parameter."
295
+ },
296
+ parameterId: {
297
+ type: "string",
298
+ description: "Parameter identifier to update."
299
+ },
300
+ value: {
301
+ type: "number",
302
+ description: "Target numeric parameter value."
303
+ },
304
+ dryRun: {
305
+ type: "boolean",
306
+ description: "If true, preview the action without mutating Live."
307
+ }
308
+ },
309
+ required: ["trackId", "deviceId", "parameterId", "value"]
310
+ }),
188
311
  async execute(args) {
189
312
  requireString(args.trackId, "trackId");
190
313
  requireString(args.deviceId, "deviceId");
@@ -219,6 +342,14 @@ export function buildDefaultTools({ stateAdapter, bridgeAdapter, policyAdapter }
219
342
  {
220
343
  name: "refresh_state",
221
344
  description: "Force a state refresh for a target scope.",
345
+ inputSchema: createObjectSchema({
346
+ properties: {
347
+ target: {
348
+ type: "string",
349
+ description: "Refresh scope, for example `project`, `song`, or `track:7`."
350
+ }
351
+ }
352
+ }),
222
353
  async execute(args) {
223
354
  const target = args.target ?? "project";
224
355
  const refreshed = await stateAdapter.refreshState(target);
@@ -236,6 +367,7 @@ export function buildDefaultTools({ stateAdapter, bridgeAdapter, policyAdapter }
236
367
  {
237
368
  name: "get_capabilities",
238
369
  description: "Return bridge and server capabilities.",
370
+ inputSchema: EMPTY_OBJECT_SCHEMA,
239
371
  async execute() {
240
372
  const capabilities = await bridgeAdapter.getCapabilities();
241
373
  return {
@@ -86,15 +86,23 @@ export class LaiveMcpServer {
86
86
 
87
87
  if (message.method === "tools/call") {
88
88
  const params = message.params ?? {};
89
- const result = await this.invokeTool(params.name, params.arguments ?? {}, {
90
- requestId: message.id ?? null
91
- });
89
+ try {
90
+ const result = await this.invokeTool(params.name, params.arguments ?? {}, {
91
+ requestId: message.id ?? null
92
+ });
92
93
 
93
- return {
94
- jsonrpc: "2.0",
95
- id: message.id ?? null,
96
- result
97
- };
94
+ return {
95
+ jsonrpc: "2.0",
96
+ id: message.id ?? null,
97
+ result: toToolResult(result)
98
+ };
99
+ } catch (error) {
100
+ return {
101
+ jsonrpc: "2.0",
102
+ id: message.id ?? null,
103
+ result: toToolErrorResult(error)
104
+ };
105
+ }
98
106
  }
99
107
 
100
108
  throw new McpServerError("method_not_found", `Unsupported method: ${message.method}`);
@@ -113,6 +121,38 @@ export class LaiveMcpServer {
113
121
  }
114
122
  }
115
123
 
124
+ function toToolResult(result) {
125
+ return {
126
+ content: [
127
+ {
128
+ type: "text",
129
+ text:
130
+ typeof result?.summary === "string" && result.summary.length > 0
131
+ ? result.summary
132
+ : JSON.stringify(result, null, 2)
133
+ }
134
+ ],
135
+ structuredContent: result,
136
+ isError: false
137
+ };
138
+ }
139
+
140
+ function toToolErrorResult(error) {
141
+ const shape = toErrorShape(error);
142
+ return {
143
+ content: [
144
+ {
145
+ type: "text",
146
+ text: shape.message
147
+ }
148
+ ],
149
+ structuredContent: {
150
+ error: shape
151
+ },
152
+ isError: true
153
+ };
154
+ }
155
+
116
156
  function createUnsupportedAdapter(name) {
117
157
  return new Proxy(
118
158
  {},