@williamzujkowski/live-coding-music-mcp 4.0.0 → 4.1.0
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/README.md +202 -705
- package/dist/AudioAnalyzer.js +1294 -91
- package/dist/PatternStore.js +276 -23
- package/dist/StrudelController.js +388 -56
- package/dist/server/resources.js +27 -20
- package/dist/server/server.js +384 -76
- package/dist/server/tools/ai.js +124 -91
- package/dist/server/tools/analysis.js +168 -59
- package/dist/server/tools/capture.js +135 -24
- package/dist/server/tools/compose.js +26 -26
- package/dist/server/tools/diagnostics.js +20 -3
- package/dist/server/tools/editor.js +60 -6
- package/dist/server/tools/generate.js +38 -15
- package/dist/server/tools/history.js +30 -8
- package/dist/server/tools/playback.js +2 -1
- package/dist/server/tools/session.js +38 -6
- package/dist/server/tools/storage.js +179 -8
- package/dist/server/tools/transform.js +127 -43
- package/dist/server/tools/types.js +209 -14
- package/dist/services/AudioCaptureService.js +302 -33
- package/dist/services/AudioExportService.js +318 -0
- package/dist/services/BrowserOnlyFunctions.js +149 -0
- package/dist/services/EventDensityProbe.js +324 -0
- package/dist/services/GeminiService.js +221 -36
- package/dist/services/IsolatedEngineRunner.js +308 -0
- package/dist/services/IsolatedStrudelEngine.js +135 -0
- package/dist/services/LocalPatternEngine.js +16 -0
- package/dist/services/MIDIExportService.js +765 -71
- package/dist/services/MIDIImportService.js +474 -0
- package/dist/services/MusicTheory.js +85 -13
- package/dist/services/PatternGenerator.js +168 -85
- package/dist/services/PatternSandbox.js +359 -0
- package/dist/services/SessionManager.js +338 -65
- package/dist/services/StrudelEngine.js +218 -12
- package/dist/services/StrudelEngineHelpers.js +24 -8
- package/dist/services/StyleRegistry.js +204 -0
- package/dist/services/Transferable.js +114 -0
- package/dist/services/ai/AiTransport.js +56 -0
- package/dist/services/ai/AudioMeasurements.js +155 -0
- package/dist/services/ai/CliTransport.js +256 -0
- package/dist/services/engineChild.js +83 -0
- package/dist/services/engineChildPath.js +28 -0
- package/dist/utils/CategorisedError.js +59 -0
- package/dist/utils/ErrorRecovery.js +130 -110
- package/dist/utils/InputValidator.js +55 -7
- package/dist/utils/PatternValidator.js +136 -17
- package/dist/utils/SafePath.js +112 -0
- package/dist/utils/ServerConfig.js +248 -0
- package/dist/utils/TableLookup.js +41 -0
- package/dist/utils/Tempo.js +143 -0
- package/package.json +17 -5
- package/dist/services/__mocks__/StrudelEngine.js +0 -157
- package/dist/types/index.js +0 -1
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* dispatcher. The server keeps the MCP protocol layer thin and delegates
|
|
7
7
|
* to these modules.
|
|
8
8
|
*/
|
|
9
|
+
import { AiAuthError, AiRateLimitError } from '../../services/ai/AiTransport.js';
|
|
10
|
+
import { BusinessError, TransientError, ValidationError } from '../../utils/CategorisedError.js';
|
|
9
11
|
/** Type guard for tools and dispatchers. */
|
|
10
12
|
export function isEnvelope(v) {
|
|
11
13
|
return (typeof v === 'object' &&
|
|
@@ -42,33 +44,226 @@ export function err(category, message, opts) {
|
|
|
42
44
|
* dispatcher when a tool throws without wrapping. Tools that know better
|
|
43
45
|
* should construct `err(category, message)` directly.
|
|
44
46
|
*/
|
|
47
|
+
/**
|
|
48
|
+
* Whether a tool returned a self-declared failure.
|
|
49
|
+
*
|
|
50
|
+
* Several modules report problems as `{ success: false, message }`
|
|
51
|
+
* rather than throwing or building an envelope. Wrapped with `ok()`
|
|
52
|
+
* those reach MCP clients as successes, with the real outcome hidden in
|
|
53
|
+
* a field the envelope contract says nothing about (#274).
|
|
54
|
+
*
|
|
55
|
+
* @param value - A tool's raw return value
|
|
56
|
+
* @returns True when it declares failure
|
|
57
|
+
*/
|
|
58
|
+
export function isFailureShaped(value) {
|
|
59
|
+
if (typeof value !== 'object' || value === null)
|
|
60
|
+
return false;
|
|
61
|
+
const record = value;
|
|
62
|
+
// An explicit success:false is unambiguous.
|
|
63
|
+
if ('success' in record && record.success === false)
|
|
64
|
+
return true;
|
|
65
|
+
// A bare `{ error: '...' }` with no success flag is equally a failure,
|
|
66
|
+
// and several modules return exactly that — ai.ts and analysis.ts both
|
|
67
|
+
// do. Requiring a `success` key let those through as ok:true, which is
|
|
68
|
+
// the same bug one shape over (#274).
|
|
69
|
+
//
|
|
70
|
+
// Guarded by `success !== true` so a result that genuinely succeeded
|
|
71
|
+
// while carrying a non-fatal `error` field is left alone.
|
|
72
|
+
return (record.success !== true &&
|
|
73
|
+
typeof record.error === 'string' &&
|
|
74
|
+
record.error.length > 0);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Prefix of the message `writePatternSafe` returns when it did *not*
|
|
78
|
+
* write — no browser session is up, so the pattern is stashed for the
|
|
79
|
+
* next init instead.
|
|
80
|
+
*
|
|
81
|
+
* Exported so `server.ts` builds the message from it and the helpers
|
|
82
|
+
* below test for it. One constant, so the producer and the consumers
|
|
83
|
+
* cannot drift apart.
|
|
84
|
+
*/
|
|
85
|
+
export const PATTERN_STASHED_PREFIX = 'Pattern generated (initialize Strudel to use it):';
|
|
86
|
+
/** Whether a `writePatternSafe` result means "stashed, not written". */
|
|
87
|
+
export function wasStashed(writeResult) {
|
|
88
|
+
return typeof writeResult === 'string' && writeResult.startsWith(PATTERN_STASHED_PREFIX);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Appends the "not in the editor yet" caveat to a tool's success
|
|
92
|
+
* message when the write was only a stash.
|
|
93
|
+
*
|
|
94
|
+
* Nineteen call sites across five modules used to `await
|
|
95
|
+
* ctx.writePatternSafe(...)` and throw the result away, then return
|
|
96
|
+
* their own message. Two modules could actually reach the stash path:
|
|
97
|
+
* `generate.ts` (all seven `generate_part` / `music_theory` /
|
|
98
|
+
* `generate_rhythm` tools) and `storage.ts` (`action=load`). Neither
|
|
99
|
+
* gates on `isInitialized`, so both reported the pattern as applied
|
|
100
|
+
* while it sat in `pendingPattern` waiting for an init the caller had
|
|
101
|
+
* no reason to run (#285).
|
|
102
|
+
*
|
|
103
|
+
* `transform.ts`, `editor.ts` and `ai.ts` refuse outright when the
|
|
104
|
+
* default session is down, so they never mis-reported. They thread the
|
|
105
|
+
* result anyway, so the rule holds uniformly and a relaxed gate cannot
|
|
106
|
+
* silently reintroduce the bug.
|
|
107
|
+
*
|
|
108
|
+
* @param message - What the tool would say on a normal write
|
|
109
|
+
* @param writeResult - Exactly what `writePatternSafe` returned
|
|
110
|
+
* @returns The message, with the caveat appended if it applies
|
|
111
|
+
*/
|
|
112
|
+
export function withStashNotice(message, writeResult) {
|
|
113
|
+
if (!wasStashed(writeResult))
|
|
114
|
+
return message;
|
|
115
|
+
// Trim a trailing period first: appending an em dash straight after
|
|
116
|
+
// one produced "…boom_bap. — but it is not in the editor yet" (#297).
|
|
117
|
+
return `${message.replace(/\.\s*$/, '')}${STASH_SUFFIX}`;
|
|
118
|
+
}
|
|
119
|
+
/** The caveat, as a suffix for prose results. */
|
|
120
|
+
const STASH_SUFFIX = ' — but it is not in the editor yet: no browser session is ' +
|
|
121
|
+
'running. Run `init` to open one and load it.';
|
|
122
|
+
/** The same caveat as a standalone sentence, for structured results. */
|
|
123
|
+
export const STASH_WARNING = 'The pattern is not in the editor yet: no browser ' +
|
|
124
|
+
'session is running. Run `init` to open one and load it.';
|
|
125
|
+
/**
|
|
126
|
+
* Structured-result counterpart to `withStashNotice`: adds a `warning`
|
|
127
|
+
* field when the write was only a stash, and leaves the result
|
|
128
|
+
* untouched otherwise.
|
|
129
|
+
*
|
|
130
|
+
* @param result - What the tool would return on a normal write
|
|
131
|
+
* @param writeResult - Exactly what `writePatternSafe` returned
|
|
132
|
+
*/
|
|
133
|
+
export function withStashField(result, writeResult) {
|
|
134
|
+
if (!wasStashed(writeResult))
|
|
135
|
+
return result;
|
|
136
|
+
// Spreading an array produces {"0":"a","1":"b"} — a silent shape
|
|
137
|
+
// change the `T & {...}` signature promised not to make. The type
|
|
138
|
+
// now excludes arrays; this is the runtime half of the same
|
|
139
|
+
// guarantee (#297).
|
|
140
|
+
if (Array.isArray(result)) {
|
|
141
|
+
throw new TypeError('withStashField expects an object result, not an array. ' +
|
|
142
|
+
'Spreading an array would change its shape.');
|
|
143
|
+
}
|
|
144
|
+
// Append rather than overwrite: a result that already carries a
|
|
145
|
+
// warning has something to say, and the stash notice is additional
|
|
146
|
+
// information, not a replacement for it.
|
|
147
|
+
const existing = result.warning;
|
|
148
|
+
const warning = typeof existing === 'string' && existing.length > 0
|
|
149
|
+
? `${existing} ${STASH_WARNING}`
|
|
150
|
+
: STASH_WARNING;
|
|
151
|
+
return { ...result, warning };
|
|
152
|
+
}
|
|
45
153
|
export function categorizeError(error) {
|
|
154
|
+
// A typed error first, so the categorisation does not depend on
|
|
155
|
+
// wording. The rate limiter used to throw a plain Error whose message
|
|
156
|
+
// happened to start "Rate limit exceeded", and everything downstream —
|
|
157
|
+
// three re-throw sites and the string match below — recognised it by
|
|
158
|
+
// that phrase. Rewording it would have quietly turned the single most
|
|
159
|
+
// retryable failure there is into `internal`, which is not retryable
|
|
160
|
+
// (#380).
|
|
161
|
+
if (error instanceof AiRateLimitError)
|
|
162
|
+
return 'transient';
|
|
163
|
+
// Same reasoning as the rate limit: the class exists, it is thrown,
|
|
164
|
+
// and recognising it by type means the categorisation does not depend
|
|
165
|
+
// on the message still saying "not authenticated".
|
|
166
|
+
if (error instanceof AiAuthError)
|
|
167
|
+
return 'permission';
|
|
168
|
+
// A thrower that knows what kind of failure it has beats a phrase
|
|
169
|
+
// match every time. "Steps cannot exceed 256" contains none of the
|
|
170
|
+
// words below, and fell through to `internal` — not retryable, and
|
|
171
|
+
// reading to an agent as "the server is broken" when the fix is to
|
|
172
|
+
// change an argument (#382).
|
|
173
|
+
if (error instanceof ValidationError)
|
|
174
|
+
return 'validation';
|
|
175
|
+
if (error instanceof BusinessError)
|
|
176
|
+
return 'business';
|
|
177
|
+
if (error instanceof TransientError)
|
|
178
|
+
return 'transient';
|
|
46
179
|
const message = error instanceof Error ? error.message : String(error);
|
|
47
180
|
const lower = message.toLowerCase();
|
|
181
|
+
// Transient first. It used to be checked last, behind `validation`,
|
|
182
|
+
// which meant a timeout mentioning an invalid state was filed as a
|
|
183
|
+
// caller mistake — and everything not matched fell through to
|
|
184
|
+
// `internal`, whose isRetryable is false.
|
|
185
|
+
//
|
|
186
|
+
// 'timed out' is the important entry: the codebase says that 13 times
|
|
187
|
+
// and 'timeout' twice, and 'timed out'.includes('timeout') is false. So
|
|
188
|
+
// essentially every real timeout was reported to agents as a permanent
|
|
189
|
+
// internal failure not worth retrying. Rate limits had the same
|
|
190
|
+
// problem across 14 throw sites, and a rate limit is the single most
|
|
191
|
+
// retryable error there is.
|
|
192
|
+
if (lower.includes('timed out') ||
|
|
193
|
+
lower.includes('timeout') ||
|
|
194
|
+
lower.includes('rate limit') ||
|
|
195
|
+
lower.includes('econnrefused') ||
|
|
196
|
+
lower.includes('econnreset') ||
|
|
197
|
+
lower.includes('etimedout') ||
|
|
198
|
+
lower.includes('network') ||
|
|
199
|
+
lower.includes('fetch failed') ||
|
|
200
|
+
lower.includes('did not become ready') ||
|
|
201
|
+
// Chromium spells its network errors net::ERR_*, which shares no
|
|
202
|
+
// substring with the POSIX names above — 'ERR_CONNECTION_REFUSED'
|
|
203
|
+
// does not contain 'econnrefused', and 'ERR_TIMED_OUT' does not
|
|
204
|
+
// contain 'timed out' (the underscore). So every navigation failure
|
|
205
|
+
// Playwright surfaced was landing in `internal`: a server bug the
|
|
206
|
+
// caller could do nothing about, when a refused connection is the
|
|
207
|
+
// most retryable error there is (#315).
|
|
208
|
+
lower.includes('net::err_')) {
|
|
209
|
+
return 'transient';
|
|
210
|
+
}
|
|
211
|
+
// Auth before business: a CLI that is installed but not logged in is a
|
|
212
|
+
// credential problem, and saying 'not found' about a login is not the
|
|
213
|
+
// same as saying it about a session.
|
|
214
|
+
//
|
|
215
|
+
// `gemini` used to be in this list, which made EVERY error mentioning
|
|
216
|
+
// the vendor a credentials problem — "Audio analysis failed: gemini
|
|
217
|
+
// returned 500" included. A vendor name is not an error condition.
|
|
218
|
+
// Auth failures from that path throw `AiAuthError`, which is matched
|
|
219
|
+
// by type above, or say "api key"/"not authenticated" in the message.
|
|
220
|
+
// Found because a test of mine passed with its fix removed: the
|
|
221
|
+
// fixture said "gemini rejected the request" (#382).
|
|
222
|
+
if (lower.includes('api key') ||
|
|
223
|
+
lower.includes('unauthorized') ||
|
|
224
|
+
lower.includes('unauthenticated') ||
|
|
225
|
+
lower.includes('not authenticated') ||
|
|
226
|
+
// 'authenticate' bare, as well as 'not authenticated': the
|
|
227
|
+
// no-transport message says "Install and authenticate one of".
|
|
228
|
+
// That is an auth word doing auth work, unlike the vendor name
|
|
229
|
+
// removed above.
|
|
230
|
+
lower.includes('authenticate') ||
|
|
231
|
+
lower.includes('forbidden') ||
|
|
232
|
+
lower.includes('permission denied') ||
|
|
233
|
+
lower.includes('credential')) {
|
|
234
|
+
return 'permission';
|
|
235
|
+
}
|
|
48
236
|
if (lower.includes('not initialized') ||
|
|
49
237
|
lower.includes("run 'init' first") ||
|
|
50
238
|
lower.includes('run init first') ||
|
|
51
239
|
lower.includes('not found') ||
|
|
52
|
-
lower.includes('
|
|
240
|
+
lower.includes('not installed') ||
|
|
241
|
+
lower.includes('no ai transport') ||
|
|
242
|
+
lower.includes('already exists') ||
|
|
243
|
+
lower.includes('already in progress') ||
|
|
244
|
+
lower.includes('not connected') ||
|
|
245
|
+
lower.includes('did not start') ||
|
|
246
|
+
lower.includes('no pattern')) {
|
|
53
247
|
return 'business';
|
|
54
248
|
}
|
|
55
249
|
if (lower.includes('invalid') ||
|
|
56
250
|
lower.includes('must be') ||
|
|
57
251
|
lower.includes('required') ||
|
|
58
|
-
lower.includes('out of range')
|
|
252
|
+
lower.includes('out of range') ||
|
|
253
|
+
lower.includes('too many') ||
|
|
254
|
+
lower.includes('too long') ||
|
|
255
|
+
lower.includes('non-empty') ||
|
|
256
|
+
lower.includes('cannot be empty') ||
|
|
257
|
+
// The MIDI parse message reached `validation` only because it
|
|
258
|
+
// happens to contain 'Invalid'. Reword it without that adjective
|
|
259
|
+
// and it silently became a non-retryable server fault again — the
|
|
260
|
+
// two halves of #280 were coupled by nothing but vocabulary (#297).
|
|
261
|
+
lower.includes('could not be parsed') ||
|
|
262
|
+
lower.includes('parse error') ||
|
|
263
|
+
lower.includes('failed to parse') ||
|
|
264
|
+
lower.includes('malformed') ||
|
|
265
|
+
lower.includes('corrupt')) {
|
|
59
266
|
return 'validation';
|
|
60
267
|
}
|
|
61
|
-
if (lower.includes('gemini') ||
|
|
62
|
-
lower.includes('api key') ||
|
|
63
|
-
lower.includes('unauthorized') ||
|
|
64
|
-
lower.includes('forbidden')) {
|
|
65
|
-
return 'permission';
|
|
66
|
-
}
|
|
67
|
-
if (lower.includes('timeout') ||
|
|
68
|
-
lower.includes('econnrefused') ||
|
|
69
|
-
lower.includes('network') ||
|
|
70
|
-
lower.includes('fetch failed')) {
|
|
71
|
-
return 'transient';
|
|
72
|
-
}
|
|
73
268
|
return 'internal';
|
|
74
269
|
}
|