@williamzujkowski/live-coding-music-mcp 2.0.0 → 3.0.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 +222 -231
- package/dist/AudioAnalyzer.js +0 -1
- package/dist/PatternStore.js +0 -1
- package/dist/index.js +0 -0
- package/dist/server/resources.js +175 -0
- package/dist/server/server.js +226 -348
- package/dist/server/tools/ai.js +78 -22
- package/dist/server/tools/analysis.js +266 -73
- package/dist/server/tools/capture.js +67 -20
- package/dist/server/tools/compose.js +159 -0
- package/dist/server/tools/diagnostics.js +112 -60
- package/dist/server/tools/editor.js +130 -66
- package/dist/server/tools/generate.js +190 -74
- package/dist/server/tools/history.js +149 -88
- package/dist/server/tools/playback.js +49 -18
- package/dist/server/tools/session.js +108 -70
- package/dist/server/tools/storage.js +86 -30
- package/dist/server/tools/transform.js +250 -116
- package/dist/server/tools/types.js +66 -1
- package/dist/services/AudioCaptureService.js +4 -3
- package/dist/services/GeminiService.js +1 -1
- package/dist/services/MIDIExportService.js +2 -4
- package/dist/services/SessionManager.js +4 -2
- package/dist/services/StrudelEngine.js +24 -126
- package/dist/services/StrudelEngineHelpers.js +151 -0
- package/package.json +1 -1
- package/dist/__tests__/MusicTheory.test.js +0 -245
- package/dist/__tests__/PatternGenerator.test.js +0 -337
- package/dist/__tests__/PatternStore.test.js +0 -153
- package/dist/__tests__/utils/MockPlaywright.js +0 -130
- package/dist/server/EnhancedMCPServer.js +0 -654
- package/dist/server/EnhancedMCPServerFixed.js +0 -2506
package/dist/server/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
4
|
import { StrudelController } from '../StrudelController.js';
|
|
5
5
|
import { PatternStore } from '../PatternStore.js';
|
|
6
6
|
import { MusicTheory } from '../services/MusicTheory.js';
|
|
@@ -12,7 +12,6 @@ import { SessionManager } from '../services/SessionManager.js';
|
|
|
12
12
|
import { readFileSync, existsSync } from 'fs';
|
|
13
13
|
import { Logger } from '../utils/Logger.js';
|
|
14
14
|
import { PerformanceMonitor } from '../utils/PerformanceMonitor.js';
|
|
15
|
-
import { InputValidator } from '../utils/InputValidator.js';
|
|
16
15
|
import { StrudelEngine } from '../services/StrudelEngine.js';
|
|
17
16
|
import { diagnosticsModule } from './tools/diagnostics.js';
|
|
18
17
|
import { playbackModule } from './tools/playback.js';
|
|
@@ -25,13 +24,14 @@ import { generateModule } from './tools/generate.js';
|
|
|
25
24
|
import { sessionModule } from './tools/session.js';
|
|
26
25
|
import { captureModule } from './tools/capture.js';
|
|
27
26
|
import { aiModule } from './tools/ai.js';
|
|
27
|
+
import { composeModule } from './tools/compose.js';
|
|
28
|
+
import { categorizeError, err, isEnvelope, ok } from './tools/types.js';
|
|
29
|
+
import { readResource, resources as mcpResources } from './resources.js';
|
|
30
|
+
import { join } from 'node:path';
|
|
28
31
|
const configPath = './config.json';
|
|
29
32
|
const config = existsSync(configPath)
|
|
30
33
|
? JSON.parse(readFileSync(configPath, 'utf-8'))
|
|
31
34
|
: { headless: false };
|
|
32
|
-
/** Energy level configuration for set_energy tool (#81) */
|
|
33
|
-
// MOOD_PROFILES + ENERGY_LEVELS moved to src/server/tools/transform.ts
|
|
34
|
-
// alongside the shift_mood / set_energy / refine handlers that use them.
|
|
35
35
|
export class StrudelMCPServer {
|
|
36
36
|
server;
|
|
37
37
|
controller;
|
|
@@ -39,29 +39,39 @@ export class StrudelMCPServer {
|
|
|
39
39
|
theory;
|
|
40
40
|
generator;
|
|
41
41
|
geminiService;
|
|
42
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Per-session AudioCaptureService instances (#180). Keyed by session id
|
|
44
|
+
* (or 'default' for the legacy/single-session path). Each session's
|
|
45
|
+
* recorder injects into its own page, so concurrent captures across
|
|
46
|
+
* sessions no longer conflict.
|
|
47
|
+
*/
|
|
48
|
+
audioCaptureServices = new Map();
|
|
43
49
|
midiExportService;
|
|
44
50
|
sessionManager;
|
|
45
51
|
logger;
|
|
46
52
|
perfMonitor;
|
|
47
53
|
strudelEngine;
|
|
48
54
|
sessionHistory = [];
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Per-session undo/redo/history bundles (#179). Keyed by session id;
|
|
57
|
+
* 'default' for the legacy/single-session path. Bundles are lazily
|
|
58
|
+
* created on first access via getHistory().
|
|
59
|
+
*/
|
|
60
|
+
historyBundles = new Map();
|
|
61
|
+
/** Server-wide history-entry counter; IDs unique across all sessions. */
|
|
53
62
|
historyIdCounter = 0;
|
|
54
|
-
/** Maximum history entries to prevent memory leaks */
|
|
63
|
+
/** Maximum history entries per session to prevent memory leaks */
|
|
55
64
|
MAX_HISTORY = 100;
|
|
56
65
|
isInitialized = false;
|
|
57
66
|
generatedPatterns = new Map();
|
|
58
67
|
constructor() {
|
|
59
68
|
this.server = new Server({
|
|
60
69
|
name: 'live-coding-music-mcp',
|
|
61
|
-
version: '
|
|
70
|
+
version: '3.0.0',
|
|
62
71
|
}, {
|
|
63
72
|
capabilities: {
|
|
64
73
|
tools: {},
|
|
74
|
+
resources: {},
|
|
65
75
|
},
|
|
66
76
|
});
|
|
67
77
|
this.controller = new StrudelController(config.headless);
|
|
@@ -77,70 +87,24 @@ export class StrudelMCPServer {
|
|
|
77
87
|
this.setupHandlers();
|
|
78
88
|
}
|
|
79
89
|
getTools() {
|
|
80
|
-
// Same tools as before - keeping the same structure
|
|
81
90
|
return [
|
|
82
|
-
// Core Control Tools (10)
|
|
83
91
|
{
|
|
84
92
|
name: 'init',
|
|
85
93
|
description: 'Initialize Strudel in browser',
|
|
86
|
-
inputSchema: { type: 'object', properties: {} }
|
|
94
|
+
inputSchema: { type: 'object', properties: {} },
|
|
87
95
|
},
|
|
88
|
-
// write, append, insert, replace, clear, get_pattern —
|
|
89
|
-
// extracted to src/server/tools/editor.ts (#104)
|
|
90
96
|
...editorModule.tools,
|
|
91
|
-
// play, pause, stop — extracted to src/server/tools/playback.ts (#104)
|
|
92
97
|
...playbackModule.tools,
|
|
93
|
-
// transform + effect + shape + set_tempo — extracted to src/server/tools/transform.ts (#104)
|
|
94
98
|
...transformModule.tools,
|
|
95
|
-
// generate_pattern, generate_drums, generate_bassline, generate_melody,
|
|
96
|
-
// generate_scale, generate_chord_progression, generate_euclidean,
|
|
97
|
-
// generate_polyrhythm, generate_fill — extracted to src/server/tools/generate.ts (#104)
|
|
98
99
|
...generateModule.tools,
|
|
99
|
-
// Audio Analysis + runtime validation — extracted to src/server/tools/analysis.ts (#104)
|
|
100
100
|
...analysisModule.tools,
|
|
101
|
-
// add_effect, remove_effect, set_tempo, add_swing, apply_scale,
|
|
102
|
-
// shift_mood — all handled by transformModule (see above).
|
|
103
|
-
// save, load, list — extracted to src/server/tools/storage.ts (#104)
|
|
104
101
|
...storageModule.tools,
|
|
105
|
-
// undo, redo, list_history, restore_history, compare_patterns
|
|
106
|
-
// — extracted to src/server/tools/history.ts (#104)
|
|
107
102
|
...historyModule.tools,
|
|
108
|
-
// generate_scale, generate_chord_progression, generate_euclidean,
|
|
109
|
-
// generate_polyrhythm, generate_fill — handled by generateModule (above).
|
|
110
|
-
// Performance, diagnostics, screenshots — extracted to src/server/tools/diagnostics.ts (#104)
|
|
111
103
|
...diagnosticsModule.tools,
|
|
112
|
-
|
|
113
|
-
{
|
|
114
|
-
name: 'show_browser',
|
|
115
|
-
description: 'Bring browser window to foreground for visual feedback',
|
|
116
|
-
inputSchema: { type: 'object', properties: {} }
|
|
117
|
-
},
|
|
118
|
-
// UX Tools - High-level Compose (#42, #73)
|
|
119
|
-
{
|
|
120
|
-
name: 'compose',
|
|
121
|
-
description: 'Generate, write, and play a complete pattern in one step. Auto-initializes browser if needed.',
|
|
122
|
-
inputSchema: {
|
|
123
|
-
type: 'object',
|
|
124
|
-
properties: {
|
|
125
|
-
style: { type: 'string', description: 'Genre: techno, house, dnb, ambient, trap, jungle, jazz, experimental' },
|
|
126
|
-
tempo: { type: 'number', description: 'BPM (default: genre-appropriate)' },
|
|
127
|
-
key: { type: 'string', description: 'Musical key (default: C)' },
|
|
128
|
-
auto_play: { type: 'boolean', description: 'Start playback immediately (default: true)' },
|
|
129
|
-
get_feedback: { type: 'boolean', description: 'Get AI feedback on the generated pattern (default: false)' }
|
|
130
|
-
},
|
|
131
|
-
required: ['style']
|
|
132
|
-
}
|
|
133
|
-
},
|
|
134
|
-
// get_pattern_feedback, suggest_pattern_from_audio, jam_with —
|
|
135
|
-
// extracted to src/server/tools/ai.ts (#104)
|
|
104
|
+
...composeModule.tools,
|
|
136
105
|
...aiModule.tools,
|
|
137
|
-
// start_audio_capture, stop_audio_capture, capture_audio_sample,
|
|
138
|
-
// export_midi — extracted to src/server/tools/capture.ts (#104)
|
|
139
106
|
...captureModule.tools,
|
|
140
|
-
// create_session, destroy_session, list_sessions, switch_session —
|
|
141
|
-
// extracted to src/server/tools/session.ts (#104)
|
|
142
107
|
...sessionModule.tools,
|
|
143
|
-
// refine, set_energy — handled by transformModule (see above).
|
|
144
108
|
];
|
|
145
109
|
}
|
|
146
110
|
setupHandlers() {
|
|
@@ -149,60 +113,115 @@ export class StrudelMCPServer {
|
|
|
149
113
|
}));
|
|
150
114
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
151
115
|
const { name, arguments: args } = request.params;
|
|
116
|
+
const envelope = await this.dispatchToolCall(name, args);
|
|
117
|
+
return {
|
|
118
|
+
content: [{ type: 'text', text: JSON.stringify(envelope, null, 2) }],
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
// MCP resources — read-only catalogs (#131). Resource handlers stay
|
|
122
|
+
// here in server.ts because they're protocol surface, not tool calls.
|
|
123
|
+
this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
124
|
+
resources: mcpResources,
|
|
125
|
+
}));
|
|
126
|
+
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
127
|
+
const uri = request.params.uri;
|
|
152
128
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
type: 'text',
|
|
159
|
-
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2)
|
|
160
|
-
}],
|
|
161
|
-
};
|
|
129
|
+
const content = await readResource(uri, {
|
|
130
|
+
store: this.store,
|
|
131
|
+
examplesDir: join(process.cwd(), 'patterns', 'examples'),
|
|
132
|
+
});
|
|
133
|
+
return { contents: [content] };
|
|
162
134
|
}
|
|
163
135
|
catch (error) {
|
|
164
136
|
const message = error instanceof Error ? error.message : String(error);
|
|
165
|
-
this.logger.error(`
|
|
166
|
-
|
|
167
|
-
content: [{
|
|
168
|
-
type: 'text',
|
|
169
|
-
text: `Error: ${message}`
|
|
170
|
-
}],
|
|
171
|
-
};
|
|
137
|
+
this.logger.error(`Resource read failed: ${uri}`, { error: message });
|
|
138
|
+
throw error;
|
|
172
139
|
}
|
|
173
140
|
});
|
|
174
141
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Wraps every tool call into the shared envelope (#130).
|
|
144
|
+
*
|
|
145
|
+
* Tools may return:
|
|
146
|
+
* - a raw value → auto-wrapped via `ok(value)`
|
|
147
|
+
* - a pre-built Envelope (via the `ok()`/`err()`/`empty()` helpers in
|
|
148
|
+
* `tools/types.ts`) → passed through unchanged
|
|
149
|
+
* - an "error-shaped" string (starts with "Error: " or "Browser not
|
|
150
|
+
* initialized") → converted to `err(...)` so legacy modules still
|
|
151
|
+
* surface as failures while their internal migration to envelope
|
|
152
|
+
* helpers proceeds incrementally
|
|
153
|
+
*
|
|
154
|
+
* Thrown errors are caught and converted to `err(...)` with a category
|
|
155
|
+
* inferred from the message (see `categorizeError`).
|
|
156
|
+
*/
|
|
157
|
+
async dispatchToolCall(name, args) {
|
|
158
|
+
try {
|
|
159
|
+
this.logger.info(`Executing tool: ${name}`, args);
|
|
160
|
+
const result = await this.perfMonitor.measureAsync(name, () => this.executeTool(name, args));
|
|
161
|
+
if (isEnvelope(result)) {
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
// Convert legacy "Error: ..." / "Browser not initialized" string returns
|
|
165
|
+
// into the envelope. Module-level migration is incremental — this branch
|
|
166
|
+
// shrinks as modules adopt the helpers natively.
|
|
167
|
+
if (typeof result === 'string') {
|
|
168
|
+
if (result.startsWith('Error: ')) {
|
|
169
|
+
return err('internal', result.slice('Error: '.length));
|
|
170
|
+
}
|
|
171
|
+
if (result.startsWith('Browser not initialized')) {
|
|
172
|
+
return err('business', result);
|
|
173
|
+
}
|
|
174
|
+
return ok(result);
|
|
175
|
+
}
|
|
176
|
+
return ok(result);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
180
|
+
this.logger.error(`Tool execution failed: ${name}`, { error: message });
|
|
181
|
+
return err(categorizeError(error), message);
|
|
182
|
+
}
|
|
189
183
|
}
|
|
190
|
-
|
|
184
|
+
// requiresInitialization() removed (#141 / #108): each module's
|
|
185
|
+
// execute() does its own session-aware init check. The hardcoded
|
|
186
|
+
// tool-name lists had drifted and were silently wrong for explicit
|
|
187
|
+
// session_id.
|
|
188
|
+
async getCurrentPatternSafe(sessionId) {
|
|
189
|
+
if (sessionId) {
|
|
190
|
+
// Explicit session — strict lookup, no pre-init stash. Named sessions
|
|
191
|
+
// must be created via `create_session` before they're useful.
|
|
192
|
+
const sessionController = this.sessionManager.getSession(sessionId);
|
|
193
|
+
if (!sessionController) {
|
|
194
|
+
throw new Error(`Session '${sessionId}' not found. Create it first with create_session.`);
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
return await sessionController.getCurrentPattern();
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return '';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
191
203
|
if (!this.isInitialized) {
|
|
192
|
-
//
|
|
204
|
+
// Default session: return the last pre-init generated pattern if any
|
|
193
205
|
const lastPattern = Array.from(this.generatedPatterns.values()).pop();
|
|
194
206
|
return lastPattern || '';
|
|
195
207
|
}
|
|
196
208
|
try {
|
|
197
209
|
return await this.controller.getCurrentPattern();
|
|
198
210
|
}
|
|
199
|
-
catch
|
|
211
|
+
catch {
|
|
200
212
|
return '';
|
|
201
213
|
}
|
|
202
214
|
}
|
|
203
|
-
async writePatternSafe(pattern) {
|
|
215
|
+
async writePatternSafe(pattern, sessionId) {
|
|
216
|
+
if (sessionId) {
|
|
217
|
+
const sessionController = this.sessionManager.getSession(sessionId);
|
|
218
|
+
if (!sessionController) {
|
|
219
|
+
throw new Error(`Session '${sessionId}' not found. Create it first with create_session.`);
|
|
220
|
+
}
|
|
221
|
+
return await sessionController.writePattern(pattern);
|
|
222
|
+
}
|
|
204
223
|
if (!this.isInitialized) {
|
|
205
|
-
//
|
|
224
|
+
// Default session: stash the pattern for the next init/auto-init
|
|
206
225
|
const id = `pattern_${Date.now()}`;
|
|
207
226
|
this.generatedPatterns.set(id, pattern);
|
|
208
227
|
return `Pattern generated (initialize Strudel to use it): ${pattern.substring(0, 50)}...`;
|
|
@@ -246,41 +265,40 @@ export class StrudelMCPServer {
|
|
|
246
265
|
return this.sessionManager.getDefaultSession() !== undefined || this.isInitialized;
|
|
247
266
|
}
|
|
248
267
|
async executeTool(name, args) {
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
268
|
+
// Pre-flight init check removed (#141): every module's execute() does
|
|
269
|
+
// its own session-aware init check via ctx.isInitialized() /
|
|
270
|
+
// ctx.getController(sid). The old hardcoded tool-name list drifted as
|
|
271
|
+
// tools moved between modules and #108 made it silently wrong for
|
|
272
|
+
// explicit session_id (it returned "Browser not initialized" instead
|
|
273
|
+
// of "Session 'X' not found").
|
|
274
|
+
// Save current state for undo and history (#41) before any edit
|
|
275
|
+
// (#179): route to the session-specific bundle. Default session is
|
|
276
|
+
// 'default'; named sessions get their own isolated stacks.
|
|
277
|
+
if (['write', 'append', 'insert', 'replace', 'clear'].includes(name)) {
|
|
278
|
+
const sid = args?.session_id;
|
|
279
|
+
const canRead = sid !== undefined || this.isInitialized;
|
|
280
|
+
if (canRead) {
|
|
281
|
+
try {
|
|
282
|
+
const controller = this.getControllerForSession(sid);
|
|
283
|
+
const current = await controller.getCurrentPattern();
|
|
284
|
+
const bundle = this.getHistoryBundle(sid ?? 'default');
|
|
285
|
+
bundle.undoStack.push(current);
|
|
286
|
+
this.historyIdCounter++;
|
|
287
|
+
bundle.historyStack.push({
|
|
288
|
+
id: this.historyIdCounter,
|
|
289
|
+
pattern: current,
|
|
290
|
+
timestamp: new Date(),
|
|
291
|
+
action: name,
|
|
292
|
+
});
|
|
293
|
+
if (bundle.undoStack.length > this.MAX_HISTORY)
|
|
294
|
+
bundle.undoStack.shift();
|
|
295
|
+
if (bundle.historyStack.length > this.MAX_HISTORY)
|
|
296
|
+
bundle.historyStack.shift();
|
|
297
|
+
bundle.redoStack.length = 0;
|
|
276
298
|
}
|
|
277
|
-
|
|
278
|
-
|
|
299
|
+
catch {
|
|
300
|
+
// Controller might not be initialized yet, or session is gone
|
|
279
301
|
}
|
|
280
|
-
this.redoStack = [];
|
|
281
|
-
}
|
|
282
|
-
catch (e) {
|
|
283
|
-
// Controller might not be initialized yet
|
|
284
302
|
}
|
|
285
303
|
}
|
|
286
304
|
// Delegate to extracted per-domain tool modules before the big switch.
|
|
@@ -296,17 +314,25 @@ export class StrudelMCPServer {
|
|
|
296
314
|
geminiService: this.geminiService,
|
|
297
315
|
strudelEngine: this.strudelEngine,
|
|
298
316
|
midiExportService: this.midiExportService,
|
|
299
|
-
getAudioCaptureService: () => this.getAudioCaptureService(),
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
317
|
+
getAudioCaptureService: (sessionId) => this.getAudioCaptureService(sessionId),
|
|
318
|
+
dropAudioCaptureService: (sessionId) => { this.audioCaptureServices.delete(sessionId); },
|
|
319
|
+
getHistory: (sessionId) => {
|
|
320
|
+
const bundle = this.getHistoryBundle(sessionId ?? 'default');
|
|
321
|
+
return {
|
|
322
|
+
undoStack: bundle.undoStack,
|
|
323
|
+
redoStack: bundle.redoStack,
|
|
324
|
+
historyStack: bundle.historyStack,
|
|
325
|
+
maxHistory: this.MAX_HISTORY,
|
|
326
|
+
};
|
|
305
327
|
},
|
|
328
|
+
historyEntryId: () => ++this.historyIdCounter,
|
|
329
|
+
dropHistory: (sessionId) => { this.historyBundles.delete(sessionId); },
|
|
306
330
|
logger: this.logger,
|
|
307
331
|
isInitialized: () => this.isInitialized,
|
|
308
|
-
|
|
309
|
-
|
|
332
|
+
ensureInitialized: () => this.ensureInitialized(),
|
|
333
|
+
getController: (sessionId) => this.getControllerForSession(sessionId),
|
|
334
|
+
getCurrentPatternSafe: (sessionId) => this.getCurrentPatternSafe(sessionId),
|
|
335
|
+
writePatternSafe: (p, sessionId) => this.writePatternSafe(p, sessionId),
|
|
310
336
|
};
|
|
311
337
|
if (diagnosticsModule.toolNames.has(name)) {
|
|
312
338
|
return await diagnosticsModule.execute(name, args, ctx);
|
|
@@ -341,230 +367,82 @@ export class StrudelMCPServer {
|
|
|
341
367
|
if (aiModule.toolNames.has(name)) {
|
|
342
368
|
return await aiModule.execute(name, args, ctx);
|
|
343
369
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
return initResult;
|
|
358
|
-
// write, append, insert, replace, clear, get_pattern
|
|
359
|
-
// — handled by editorModule above.
|
|
360
|
-
// play, pause, stop — handled by playbackModule above.
|
|
361
|
-
// Pattern + music-theory generation — all handled by
|
|
362
|
-
// generateModule above. See src/server/tools/generate.ts.
|
|
363
|
-
// Pattern manipulation + effects + tempo — all handled by
|
|
364
|
-
// transformModule above. See src/server/tools/transform.ts.
|
|
365
|
-
// analyze, analyze_spectrum, analyze_rhythm, detect_tempo, detect_key,
|
|
366
|
-
// validate_pattern_runtime — handled by analysisModule above.
|
|
367
|
-
// Local Pattern Tools (#83) - No browser required
|
|
368
|
-
case 'validate_pattern_local':
|
|
369
|
-
InputValidator.validateStringLength(args.pattern, 'pattern', 10000, false);
|
|
370
|
-
const localValidation = this.strudelEngine.validate(args.pattern);
|
|
371
|
-
return {
|
|
372
|
-
valid: localValidation.valid,
|
|
373
|
-
errors: localValidation.errors,
|
|
374
|
-
warnings: localValidation.warnings,
|
|
375
|
-
suggestions: localValidation.suggestions,
|
|
376
|
-
errorLocation: localValidation.errorLocation,
|
|
377
|
-
message: localValidation.valid
|
|
378
|
-
? '✅ Pattern is valid'
|
|
379
|
-
: `❌ Pattern has ${localValidation.errors.length} error(s)`
|
|
380
|
-
};
|
|
381
|
-
case 'analyze_pattern_local':
|
|
382
|
-
InputValidator.validateStringLength(args.pattern, 'pattern', 10000, false);
|
|
383
|
-
const patternMetadata = this.strudelEngine.analyzePattern(args.pattern);
|
|
384
|
-
return {
|
|
385
|
-
...patternMetadata,
|
|
386
|
-
message: `Pattern analysis: ${patternMetadata.eventsPerCycle} events/cycle, ` +
|
|
387
|
-
`complexity ${(patternMetadata.complexity * 100).toFixed(0)}%` +
|
|
388
|
-
(patternMetadata.bpm ? `, ${patternMetadata.bpm} BPM` : '')
|
|
389
|
-
};
|
|
390
|
-
case 'query_pattern_events':
|
|
391
|
-
InputValidator.validateStringLength(args.pattern, 'pattern', 10000, false);
|
|
392
|
-
const startCycle = args.start ?? 0;
|
|
393
|
-
const endCycle = args.end ?? 1;
|
|
394
|
-
if (startCycle >= endCycle) {
|
|
395
|
-
return { error: 'Start must be less than end' };
|
|
396
|
-
}
|
|
397
|
-
if (endCycle - startCycle > 16) {
|
|
398
|
-
return { error: 'Maximum range is 16 cycles to prevent excessive output' };
|
|
399
|
-
}
|
|
400
|
-
try {
|
|
401
|
-
const events = this.strudelEngine.queryEvents(args.pattern, startCycle, endCycle);
|
|
402
|
-
return {
|
|
403
|
-
count: events.length,
|
|
404
|
-
range: { start: startCycle, end: endCycle },
|
|
405
|
-
events: events.map((e) => ({
|
|
406
|
-
value: e.value,
|
|
407
|
-
start: e.start,
|
|
408
|
-
end: e.end,
|
|
409
|
-
duration: e.end - e.start
|
|
410
|
-
}))
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
catch (error) {
|
|
414
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
415
|
-
return {
|
|
416
|
-
error: message,
|
|
417
|
-
suggestion: 'Check pattern syntax with validate_pattern_local first'
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
case 'transpile_pattern':
|
|
421
|
-
InputValidator.validateStringLength(args.pattern, 'pattern', 10000, false);
|
|
422
|
-
const transpileResult = this.strudelEngine.transpile(args.pattern);
|
|
423
|
-
if (transpileResult.success) {
|
|
424
|
-
return {
|
|
425
|
-
success: true,
|
|
426
|
-
transpiledCode: transpileResult.transpiledCode,
|
|
427
|
-
message: 'Pattern transpiled successfully'
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
else {
|
|
431
|
-
return {
|
|
432
|
-
success: false,
|
|
433
|
-
error: transpileResult.error,
|
|
434
|
-
errorLocation: transpileResult.errorLocation,
|
|
435
|
-
message: 'Transpilation failed'
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
// shift_mood — handled by transformModule above.
|
|
439
|
-
// Session Management
|
|
440
|
-
// save, load, list — handled by storageModule above.
|
|
441
|
-
// undo, redo, list_history, restore_history, compare_patterns
|
|
442
|
-
// — handled by historyModule above.
|
|
443
|
-
// performance_report, memory_usage, screenshot, status, diagnostics,
|
|
444
|
-
// show_errors — all handled by diagnosticsModule before this switch.
|
|
445
|
-
// show_browser stays here until session.ts extraction lands.
|
|
446
|
-
// UX Tools - Browser Control (#37)
|
|
447
|
-
case 'show_browser':
|
|
448
|
-
if (!this.isInitialized) {
|
|
449
|
-
return 'Browser not initialized. Run init first.';
|
|
450
|
-
}
|
|
451
|
-
return await this.controller.showBrowser();
|
|
452
|
-
// UX Tools - High-level Compose (#42, #73)
|
|
453
|
-
case 'compose':
|
|
454
|
-
InputValidator.validateStringLength(args.style, 'style', 100, false);
|
|
455
|
-
if (args.key) {
|
|
456
|
-
InputValidator.validateRootNote(args.key);
|
|
457
|
-
}
|
|
458
|
-
if (args.tempo !== undefined) {
|
|
459
|
-
InputValidator.validateBPM(args.tempo);
|
|
460
|
-
}
|
|
461
|
-
// Auto-initialize if needed
|
|
462
|
-
if (!this.isInitialized) {
|
|
463
|
-
await this.controller.initialize();
|
|
464
|
-
this.isInitialized = true;
|
|
465
|
-
}
|
|
466
|
-
// Generate pattern
|
|
467
|
-
const composedPattern = this.generator.generateCompletePattern(args.style, args.key || 'C', args.tempo || this.getDefaultTempo(args.style));
|
|
468
|
-
// Write pattern
|
|
469
|
-
await this.controller.writePattern(composedPattern);
|
|
470
|
-
// Auto-play by default (unless explicitly set to false)
|
|
471
|
-
const shouldPlay = args.auto_play !== false;
|
|
472
|
-
if (shouldPlay) {
|
|
473
|
-
await this.controller.play();
|
|
474
|
-
}
|
|
475
|
-
// Build response
|
|
476
|
-
const composeResponse = {
|
|
477
|
-
success: true,
|
|
478
|
-
pattern: composedPattern.substring(0, 200) + (composedPattern.length > 200 ? '...' : ''),
|
|
479
|
-
metadata: {
|
|
480
|
-
style: args.style,
|
|
481
|
-
bpm: args.tempo || this.getDefaultTempo(args.style),
|
|
482
|
-
key: args.key || 'C'
|
|
483
|
-
},
|
|
484
|
-
status: shouldPlay ? 'playing' : 'ready',
|
|
485
|
-
message: `Created ${args.style} pattern in ${args.key || 'C'}${shouldPlay ? ' - now playing' : ''}`
|
|
486
|
-
};
|
|
487
|
-
// Get AI feedback if requested (#73)
|
|
488
|
-
if (args.get_feedback) {
|
|
489
|
-
if (this.geminiService.isAvailable()) {
|
|
490
|
-
try {
|
|
491
|
-
const feedback = await this.geminiService.getCreativeFeedback(composedPattern);
|
|
492
|
-
composeResponse.feedback = feedback;
|
|
493
|
-
composeResponse.message += ` (AI feedback: ${feedback.complexity} complexity, estimated ${feedback.estimatedStyle})`;
|
|
494
|
-
}
|
|
495
|
-
catch (error) {
|
|
496
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
497
|
-
this.logger.warn('Failed to get AI feedback for compose', { error: message });
|
|
498
|
-
// Don't fail the whole operation, just note the feedback failure
|
|
499
|
-
composeResponse.message += ' (AI feedback unavailable)';
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
else {
|
|
503
|
-
composeResponse.message += ' (AI feedback requires GEMINI_API_KEY)';
|
|
504
|
-
}
|
|
370
|
+
if (composeModule.toolNames.has(name)) {
|
|
371
|
+
return await composeModule.execute(name, args, ctx);
|
|
372
|
+
}
|
|
373
|
+
if (name === 'init') {
|
|
374
|
+
const initResult = await this.controller.initialize();
|
|
375
|
+
this.isInitialized = true;
|
|
376
|
+
// Replay any pattern generated before init landed.
|
|
377
|
+
if (this.generatedPatterns.size > 0) {
|
|
378
|
+
const lastPattern = Array.from(this.generatedPatterns.values()).pop();
|
|
379
|
+
if (lastPattern) {
|
|
380
|
+
await this.controller.writePattern(lastPattern);
|
|
381
|
+
return `${initResult}. Loaded generated pattern.`;
|
|
505
382
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
// handled by aiModule above.
|
|
509
|
-
// start_audio_capture, stop_audio_capture, capture_audio_sample,
|
|
510
|
-
// export_midi — handled by captureModule above.
|
|
511
|
-
// create_session, destroy_session, list_sessions, switch_session —
|
|
512
|
-
// handled by sessionModule above.
|
|
513
|
-
// jam_with — handled by aiModule above.
|
|
514
|
-
// refine, set_energy — handled by transformModule above.
|
|
515
|
-
default:
|
|
516
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
383
|
+
}
|
|
384
|
+
return initResult;
|
|
517
385
|
}
|
|
386
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
518
387
|
}
|
|
519
|
-
// refinePattern, setEnergyLevel, transposePattern moved to
|
|
520
|
-
// src/server/tools/transform.ts alongside the tools that used them.
|
|
521
388
|
/**
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
* @returns Default BPM for the style
|
|
389
|
+
* Lazily get or create the per-session history bundle (#179). Bundle is
|
|
390
|
+
* a mutable triple — callers push/shift directly on the arrays.
|
|
525
391
|
*/
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
'garage': 130,
|
|
541
|
-
'electro': 128,
|
|
542
|
-
'downtempo': 90,
|
|
543
|
-
'idm': 115
|
|
544
|
-
};
|
|
545
|
-
return tempoMap[style.toLowerCase()] || 120;
|
|
392
|
+
getHistoryBundle(sessionId) {
|
|
393
|
+
let bundle = this.historyBundles.get(sessionId);
|
|
394
|
+
if (!bundle) {
|
|
395
|
+
bundle = { undoStack: [], redoStack: [], historyStack: [] };
|
|
396
|
+
this.historyBundles.set(sessionId, bundle);
|
|
397
|
+
}
|
|
398
|
+
return bundle;
|
|
399
|
+
}
|
|
400
|
+
/** Idempotent browser bring-up. Used by tools that promise auto-init (e.g. `compose`). */
|
|
401
|
+
async ensureInitialized() {
|
|
402
|
+
if (this.isInitialized)
|
|
403
|
+
return;
|
|
404
|
+
await this.controller.initialize();
|
|
405
|
+
this.isInitialized = true;
|
|
546
406
|
}
|
|
547
|
-
// formatTimeAgo, generateDiff, summarizeDiff moved to
|
|
548
|
-
// src/server/tools/history.ts along with the tools that used them.
|
|
549
|
-
// getPatternFeedback, suggestPatternFromAudio, captureAudioSample,
|
|
550
|
-
// jamWith + six pattern-analysis helpers moved to src/server/tools/ai.ts.
|
|
551
407
|
/** Getter for page access in audio capture. */
|
|
552
408
|
get _page() {
|
|
553
409
|
return this.controller.page;
|
|
554
410
|
}
|
|
555
|
-
// Audio capture + MIDI export logic
|
|
556
|
-
// Server still owns
|
|
557
|
-
// the class
|
|
558
|
-
//
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
411
|
+
// Audio capture + MIDI export logic lives in src/server/tools/capture.ts.
|
|
412
|
+
// Server still owns AudioCaptureService lifecycles so tests can mock
|
|
413
|
+
// the class. Post-#180: per-session — each session's recorder injects
|
|
414
|
+
// into its own page; concurrent captures across sessions no longer
|
|
415
|
+
// share a singleton stream.
|
|
416
|
+
async getAudioCaptureService(sessionId) {
|
|
417
|
+
// Resolve the right page: explicit session via SessionManager, or
|
|
418
|
+
// legacy/default via this.controller.
|
|
419
|
+
let page;
|
|
420
|
+
let key;
|
|
421
|
+
if (sessionId) {
|
|
422
|
+
const sessionController = this.sessionManager.getSession(sessionId);
|
|
423
|
+
if (!sessionController) {
|
|
424
|
+
throw new Error(`Session '${sessionId}' not found. Create it first with create_session.`);
|
|
425
|
+
}
|
|
426
|
+
if (!sessionController.page) {
|
|
427
|
+
throw new Error(`Session '${sessionId}' has no active page yet.`);
|
|
428
|
+
}
|
|
429
|
+
page = sessionController.page;
|
|
430
|
+
key = sessionId;
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
if (!this.isInitialized || !this._page) {
|
|
434
|
+
throw new Error('Browser not initialized. Run init first.');
|
|
435
|
+
}
|
|
436
|
+
page = this._page;
|
|
437
|
+
key = 'default';
|
|
562
438
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
439
|
+
let service = this.audioCaptureServices.get(key);
|
|
440
|
+
if (!service) {
|
|
441
|
+
service = new AudioCaptureService();
|
|
442
|
+
await service.injectRecorder(page);
|
|
443
|
+
this.audioCaptureServices.set(key, service);
|
|
566
444
|
}
|
|
567
|
-
return
|
|
445
|
+
return service;
|
|
568
446
|
}
|
|
569
447
|
async run() {
|
|
570
448
|
const transport = new StdioServerTransport();
|