@williamzujkowski/live-coding-music-mcp 1.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/LICENSE +21 -0
- package/README.md +1735 -0
- package/dist/AudioAnalyzer.js +678 -0
- package/dist/PatternStore.js +169 -0
- package/dist/StrudelController.js +676 -0
- package/dist/__tests__/MusicTheory.test.js +245 -0
- package/dist/__tests__/PatternGenerator.test.js +337 -0
- package/dist/__tests__/PatternStore.test.js +153 -0
- package/dist/__tests__/utils/MockPlaywright.js +130 -0
- package/dist/index.js +21 -0
- package/dist/server/EnhancedMCPServer.js +654 -0
- package/dist/server/EnhancedMCPServerFixed.js +2506 -0
- package/dist/server/server.js +580 -0
- package/dist/server/tools/ai.js +530 -0
- package/dist/server/tools/analysis.js +132 -0
- package/dist/server/tools/capture.js +184 -0
- package/dist/server/tools/diagnostics.js +96 -0
- package/dist/server/tools/editor.js +128 -0
- package/dist/server/tools/generate.js +218 -0
- package/dist/server/tools/history.js +196 -0
- package/dist/server/tools/playback.js +41 -0
- package/dist/server/tools/session.js +115 -0
- package/dist/server/tools/storage.js +77 -0
- package/dist/server/tools/transform.js +399 -0
- package/dist/server/tools/types.js +9 -0
- package/dist/services/AudioCaptureService.js +279 -0
- package/dist/services/GeminiService.js +790 -0
- package/dist/services/MIDIExportService.js +412 -0
- package/dist/services/MusicTheory.js +186 -0
- package/dist/services/PatternGenerator.js +647 -0
- package/dist/services/SessionManager.js +316 -0
- package/dist/services/StrudelEngine.js +374 -0
- package/dist/services/__mocks__/StrudelEngine.js +157 -0
- package/dist/types/AudioAnalysis.js +4 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/ErrorRecovery.js +248 -0
- package/dist/utils/InputValidator.js +281 -0
- package/dist/utils/Logger.js +19 -0
- package/dist/utils/PatternValidator.js +241 -0
- package/dist/utils/PerformanceMonitor.js +128 -0
- package/package.json +86 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { StrudelController } from '../StrudelController.js';
|
|
5
|
+
import { PatternStore } from '../PatternStore.js';
|
|
6
|
+
import { MusicTheory } from '../services/MusicTheory.js';
|
|
7
|
+
import { PatternGenerator } from '../services/PatternGenerator.js';
|
|
8
|
+
import { GeminiService } from '../services/GeminiService.js';
|
|
9
|
+
import { AudioCaptureService } from '../services/AudioCaptureService.js';
|
|
10
|
+
import { MIDIExportService } from '../services/MIDIExportService.js';
|
|
11
|
+
import { SessionManager } from '../services/SessionManager.js';
|
|
12
|
+
import { readFileSync, existsSync } from 'fs';
|
|
13
|
+
import { Logger } from '../utils/Logger.js';
|
|
14
|
+
import { PerformanceMonitor } from '../utils/PerformanceMonitor.js';
|
|
15
|
+
import { InputValidator } from '../utils/InputValidator.js';
|
|
16
|
+
import { StrudelEngine } from '../services/StrudelEngine.js';
|
|
17
|
+
import { diagnosticsModule } from './tools/diagnostics.js';
|
|
18
|
+
import { playbackModule } from './tools/playback.js';
|
|
19
|
+
import { storageModule } from './tools/storage.js';
|
|
20
|
+
import { historyModule } from './tools/history.js';
|
|
21
|
+
import { analysisModule } from './tools/analysis.js';
|
|
22
|
+
import { editorModule } from './tools/editor.js';
|
|
23
|
+
import { transformModule } from './tools/transform.js';
|
|
24
|
+
import { generateModule } from './tools/generate.js';
|
|
25
|
+
import { sessionModule } from './tools/session.js';
|
|
26
|
+
import { captureModule } from './tools/capture.js';
|
|
27
|
+
import { aiModule } from './tools/ai.js';
|
|
28
|
+
const configPath = './config.json';
|
|
29
|
+
const config = existsSync(configPath)
|
|
30
|
+
? JSON.parse(readFileSync(configPath, 'utf-8'))
|
|
31
|
+
: { 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
|
+
export class StrudelMCPServer {
|
|
36
|
+
server;
|
|
37
|
+
controller;
|
|
38
|
+
store;
|
|
39
|
+
theory;
|
|
40
|
+
generator;
|
|
41
|
+
geminiService;
|
|
42
|
+
audioCaptureService = null;
|
|
43
|
+
midiExportService;
|
|
44
|
+
sessionManager;
|
|
45
|
+
logger;
|
|
46
|
+
perfMonitor;
|
|
47
|
+
strudelEngine;
|
|
48
|
+
sessionHistory = [];
|
|
49
|
+
undoStack = [];
|
|
50
|
+
redoStack = [];
|
|
51
|
+
/** Pattern history with metadata for browsing (#41) */
|
|
52
|
+
historyStack = [];
|
|
53
|
+
historyIdCounter = 0;
|
|
54
|
+
/** Maximum history entries to prevent memory leaks */
|
|
55
|
+
MAX_HISTORY = 100;
|
|
56
|
+
isInitialized = false;
|
|
57
|
+
generatedPatterns = new Map();
|
|
58
|
+
constructor() {
|
|
59
|
+
this.server = new Server({
|
|
60
|
+
name: 'live-coding-music-mcp',
|
|
61
|
+
version: '2.0.1',
|
|
62
|
+
}, {
|
|
63
|
+
capabilities: {
|
|
64
|
+
tools: {},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
this.controller = new StrudelController(config.headless);
|
|
68
|
+
this.store = new PatternStore('./patterns');
|
|
69
|
+
this.theory = new MusicTheory();
|
|
70
|
+
this.generator = new PatternGenerator();
|
|
71
|
+
this.geminiService = new GeminiService();
|
|
72
|
+
this.midiExportService = new MIDIExportService();
|
|
73
|
+
this.sessionManager = new SessionManager(config.headless);
|
|
74
|
+
this.logger = new Logger();
|
|
75
|
+
this.perfMonitor = new PerformanceMonitor();
|
|
76
|
+
this.strudelEngine = new StrudelEngine();
|
|
77
|
+
this.setupHandlers();
|
|
78
|
+
}
|
|
79
|
+
getTools() {
|
|
80
|
+
// Same tools as before - keeping the same structure
|
|
81
|
+
return [
|
|
82
|
+
// Core Control Tools (10)
|
|
83
|
+
{
|
|
84
|
+
name: 'init',
|
|
85
|
+
description: 'Initialize Strudel in browser',
|
|
86
|
+
inputSchema: { type: 'object', properties: {} }
|
|
87
|
+
},
|
|
88
|
+
// write, append, insert, replace, clear, get_pattern —
|
|
89
|
+
// extracted to src/server/tools/editor.ts (#104)
|
|
90
|
+
...editorModule.tools,
|
|
91
|
+
// play, pause, stop — extracted to src/server/tools/playback.ts (#104)
|
|
92
|
+
...playbackModule.tools,
|
|
93
|
+
// transform + effect + shape + set_tempo — extracted to src/server/tools/transform.ts (#104)
|
|
94
|
+
...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
|
+
...generateModule.tools,
|
|
99
|
+
// Audio Analysis + runtime validation — extracted to src/server/tools/analysis.ts (#104)
|
|
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
|
+
...storageModule.tools,
|
|
105
|
+
// undo, redo, list_history, restore_history, compare_patterns
|
|
106
|
+
// — extracted to src/server/tools/history.ts (#104)
|
|
107
|
+
...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
|
+
...diagnosticsModule.tools,
|
|
112
|
+
// UX Tools - Browser Control (#37)
|
|
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)
|
|
136
|
+
...aiModule.tools,
|
|
137
|
+
// start_audio_capture, stop_audio_capture, capture_audio_sample,
|
|
138
|
+
// export_midi — extracted to src/server/tools/capture.ts (#104)
|
|
139
|
+
...captureModule.tools,
|
|
140
|
+
// create_session, destroy_session, list_sessions, switch_session —
|
|
141
|
+
// extracted to src/server/tools/session.ts (#104)
|
|
142
|
+
...sessionModule.tools,
|
|
143
|
+
// refine, set_energy — handled by transformModule (see above).
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
setupHandlers() {
|
|
147
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
148
|
+
tools: this.getTools()
|
|
149
|
+
}));
|
|
150
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
151
|
+
const { name, arguments: args } = request.params;
|
|
152
|
+
try {
|
|
153
|
+
this.logger.info(`Executing tool: ${name}`, args);
|
|
154
|
+
// Measure performance
|
|
155
|
+
const result = await this.perfMonitor.measureAsync(name, () => this.executeTool(name, args));
|
|
156
|
+
return {
|
|
157
|
+
content: [{
|
|
158
|
+
type: 'text',
|
|
159
|
+
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2)
|
|
160
|
+
}],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
165
|
+
this.logger.error(`Tool execution failed: ${name}`, { error: message });
|
|
166
|
+
return {
|
|
167
|
+
content: [{
|
|
168
|
+
type: 'text',
|
|
169
|
+
text: `Error: ${message}`
|
|
170
|
+
}],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
requiresInitialization(toolName) {
|
|
176
|
+
const toolsRequiringInit = [
|
|
177
|
+
'write', 'append', 'insert', 'replace', 'play', 'pause', 'stop',
|
|
178
|
+
'clear', 'get_pattern', 'analyze', 'analyze_spectrum', 'analyze_rhythm',
|
|
179
|
+
'transpose', 'reverse', 'stretch', 'humanize', 'generate_variation',
|
|
180
|
+
'add_effect', 'add_swing', 'set_tempo', 'save', 'undo', 'redo',
|
|
181
|
+
'validate_pattern_runtime'
|
|
182
|
+
];
|
|
183
|
+
const toolsRequiringWrite = [
|
|
184
|
+
'generate_pattern', 'generate_drums', 'generate_bassline', 'generate_melody',
|
|
185
|
+
'generate_chord_progression', 'generate_euclidean', 'generate_polyrhythm',
|
|
186
|
+
'generate_fill'
|
|
187
|
+
];
|
|
188
|
+
return toolsRequiringInit.includes(toolName) || toolsRequiringWrite.includes(toolName);
|
|
189
|
+
}
|
|
190
|
+
async getCurrentPatternSafe() {
|
|
191
|
+
if (!this.isInitialized) {
|
|
192
|
+
// Return the last generated pattern if available
|
|
193
|
+
const lastPattern = Array.from(this.generatedPatterns.values()).pop();
|
|
194
|
+
return lastPattern || '';
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
return await this.controller.getCurrentPattern();
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
return '';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async writePatternSafe(pattern) {
|
|
204
|
+
if (!this.isInitialized) {
|
|
205
|
+
// Store the pattern for later use
|
|
206
|
+
const id = `pattern_${Date.now()}`;
|
|
207
|
+
this.generatedPatterns.set(id, pattern);
|
|
208
|
+
return `Pattern generated (initialize Strudel to use it): ${pattern.substring(0, 50)}...`;
|
|
209
|
+
}
|
|
210
|
+
return await this.controller.writePattern(pattern);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Gets a StrudelController for the specified session, or the default session.
|
|
214
|
+
* Falls back to the legacy single controller if no sessions exist.
|
|
215
|
+
* @param sessionId - Optional session ID. Uses default session if not specified.
|
|
216
|
+
* @returns StrudelController for the session
|
|
217
|
+
* @throws {Error} If session doesn't exist
|
|
218
|
+
*/
|
|
219
|
+
getControllerForSession(sessionId) {
|
|
220
|
+
// If session_id is specified, use the SessionManager
|
|
221
|
+
if (sessionId) {
|
|
222
|
+
const controller = this.sessionManager.getSession(sessionId);
|
|
223
|
+
if (!controller) {
|
|
224
|
+
throw new Error(`Session '${sessionId}' not found. Create it first with create_session.`);
|
|
225
|
+
}
|
|
226
|
+
return controller;
|
|
227
|
+
}
|
|
228
|
+
// If sessions exist and there's a default, use it
|
|
229
|
+
const defaultController = this.sessionManager.getDefaultSession();
|
|
230
|
+
if (defaultController) {
|
|
231
|
+
return defaultController;
|
|
232
|
+
}
|
|
233
|
+
// Fall back to legacy single controller for backwards compatibility
|
|
234
|
+
return this.controller;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Checks if a session exists (or default/legacy controller is initialized)
|
|
238
|
+
* @param sessionId - Optional session ID
|
|
239
|
+
* @returns True if controller is available
|
|
240
|
+
*/
|
|
241
|
+
hasSession(sessionId) {
|
|
242
|
+
if (sessionId) {
|
|
243
|
+
return this.sessionManager.getSession(sessionId) !== undefined;
|
|
244
|
+
}
|
|
245
|
+
// Check if we have a default session or the legacy controller is initialized
|
|
246
|
+
return this.sessionManager.getDefaultSession() !== undefined || this.isInitialized;
|
|
247
|
+
}
|
|
248
|
+
async executeTool(name, args) {
|
|
249
|
+
// Check if tool needs initialization
|
|
250
|
+
if (this.requiresInitialization(name) && !this.isInitialized && name !== 'init') {
|
|
251
|
+
// For generation tools that don't require browser, handle them specially
|
|
252
|
+
const generationTools = [
|
|
253
|
+
'generate_pattern', 'generate_drums', 'generate_bassline', 'generate_melody',
|
|
254
|
+
'generate_chord_progression', 'generate_euclidean', 'generate_polyrhythm', 'generate_fill'
|
|
255
|
+
];
|
|
256
|
+
if (!generationTools.includes(name)) {
|
|
257
|
+
return `Browser not initialized. Run 'init' first to use ${name}.`;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Save current state for undo and history (#41) (only if initialized)
|
|
261
|
+
if (['write', 'append', 'insert', 'replace', 'clear'].includes(name) && this.isInitialized) {
|
|
262
|
+
try {
|
|
263
|
+
const current = await this.controller.getCurrentPattern();
|
|
264
|
+
this.undoStack.push(current);
|
|
265
|
+
// Add to history stack with metadata (#41)
|
|
266
|
+
this.historyIdCounter++;
|
|
267
|
+
this.historyStack.push({
|
|
268
|
+
id: this.historyIdCounter,
|
|
269
|
+
pattern: current,
|
|
270
|
+
timestamp: new Date(),
|
|
271
|
+
action: name
|
|
272
|
+
});
|
|
273
|
+
// Enforce bounds to prevent memory leaks
|
|
274
|
+
if (this.undoStack.length > this.MAX_HISTORY) {
|
|
275
|
+
this.undoStack.shift();
|
|
276
|
+
}
|
|
277
|
+
if (this.historyStack.length > this.MAX_HISTORY) {
|
|
278
|
+
this.historyStack.shift();
|
|
279
|
+
}
|
|
280
|
+
this.redoStack = [];
|
|
281
|
+
}
|
|
282
|
+
catch (e) {
|
|
283
|
+
// Controller might not be initialized yet
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Delegate to extracted per-domain tool modules before the big switch.
|
|
287
|
+
// Part of the #104 file split — each module owns its own definitions
|
|
288
|
+
// and handlers. server.ts keeps the protocol + state-tracking shell.
|
|
289
|
+
const ctx = {
|
|
290
|
+
controller: this.controller,
|
|
291
|
+
perfMonitor: this.perfMonitor,
|
|
292
|
+
store: this.store,
|
|
293
|
+
generator: this.generator,
|
|
294
|
+
theory: this.theory,
|
|
295
|
+
sessionManager: this.sessionManager,
|
|
296
|
+
geminiService: this.geminiService,
|
|
297
|
+
strudelEngine: this.strudelEngine,
|
|
298
|
+
midiExportService: this.midiExportService,
|
|
299
|
+
getAudioCaptureService: () => this.getAudioCaptureService(),
|
|
300
|
+
history: {
|
|
301
|
+
undoStack: this.undoStack,
|
|
302
|
+
redoStack: this.redoStack,
|
|
303
|
+
historyStack: this.historyStack,
|
|
304
|
+
maxHistory: this.MAX_HISTORY,
|
|
305
|
+
},
|
|
306
|
+
logger: this.logger,
|
|
307
|
+
isInitialized: () => this.isInitialized,
|
|
308
|
+
getCurrentPatternSafe: () => this.getCurrentPatternSafe(),
|
|
309
|
+
writePatternSafe: (p) => this.writePatternSafe(p),
|
|
310
|
+
};
|
|
311
|
+
if (diagnosticsModule.toolNames.has(name)) {
|
|
312
|
+
return await diagnosticsModule.execute(name, args, ctx);
|
|
313
|
+
}
|
|
314
|
+
if (playbackModule.toolNames.has(name)) {
|
|
315
|
+
return await playbackModule.execute(name, args, ctx);
|
|
316
|
+
}
|
|
317
|
+
if (storageModule.toolNames.has(name)) {
|
|
318
|
+
return await storageModule.execute(name, args, ctx);
|
|
319
|
+
}
|
|
320
|
+
if (historyModule.toolNames.has(name)) {
|
|
321
|
+
return await historyModule.execute(name, args, ctx);
|
|
322
|
+
}
|
|
323
|
+
if (analysisModule.toolNames.has(name)) {
|
|
324
|
+
return await analysisModule.execute(name, args, ctx);
|
|
325
|
+
}
|
|
326
|
+
if (editorModule.toolNames.has(name)) {
|
|
327
|
+
return await editorModule.execute(name, args, ctx);
|
|
328
|
+
}
|
|
329
|
+
if (transformModule.toolNames.has(name)) {
|
|
330
|
+
return await transformModule.execute(name, args, ctx);
|
|
331
|
+
}
|
|
332
|
+
if (generateModule.toolNames.has(name)) {
|
|
333
|
+
return await generateModule.execute(name, args, ctx);
|
|
334
|
+
}
|
|
335
|
+
if (sessionModule.toolNames.has(name)) {
|
|
336
|
+
return await sessionModule.execute(name, args, ctx);
|
|
337
|
+
}
|
|
338
|
+
if (captureModule.toolNames.has(name)) {
|
|
339
|
+
return await captureModule.execute(name, args, ctx);
|
|
340
|
+
}
|
|
341
|
+
if (aiModule.toolNames.has(name)) {
|
|
342
|
+
return await aiModule.execute(name, args, ctx);
|
|
343
|
+
}
|
|
344
|
+
switch (name) {
|
|
345
|
+
// Core Control
|
|
346
|
+
case 'init':
|
|
347
|
+
const initResult = await this.controller.initialize();
|
|
348
|
+
this.isInitialized = true;
|
|
349
|
+
// Write any pending patterns
|
|
350
|
+
if (this.generatedPatterns.size > 0) {
|
|
351
|
+
const lastPattern = Array.from(this.generatedPatterns.values()).pop();
|
|
352
|
+
if (lastPattern) {
|
|
353
|
+
await this.controller.writePattern(lastPattern);
|
|
354
|
+
return `${initResult}. Loaded generated pattern.`;
|
|
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
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return composeResponse;
|
|
507
|
+
// get_pattern_feedback, suggest_pattern_from_audio —
|
|
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}`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// refinePattern, setEnergyLevel, transposePattern moved to
|
|
520
|
+
// src/server/tools/transform.ts alongside the tools that used them.
|
|
521
|
+
/**
|
|
522
|
+
* Gets the default tempo for a given music style
|
|
523
|
+
* @param style - Music style/genre
|
|
524
|
+
* @returns Default BPM for the style
|
|
525
|
+
*/
|
|
526
|
+
getDefaultTempo(style) {
|
|
527
|
+
const tempoMap = {
|
|
528
|
+
'techno': 130,
|
|
529
|
+
'house': 125,
|
|
530
|
+
'dnb': 174,
|
|
531
|
+
'drum and bass': 174,
|
|
532
|
+
'ambient': 80,
|
|
533
|
+
'trap': 140,
|
|
534
|
+
'jungle': 160,
|
|
535
|
+
'jazz': 110,
|
|
536
|
+
'experimental': 120,
|
|
537
|
+
'dubstep': 140,
|
|
538
|
+
'trance': 138,
|
|
539
|
+
'breakbeat': 130,
|
|
540
|
+
'garage': 130,
|
|
541
|
+
'electro': 128,
|
|
542
|
+
'downtempo': 90,
|
|
543
|
+
'idm': 115
|
|
544
|
+
};
|
|
545
|
+
return tempoMap[style.toLowerCase()] || 120;
|
|
546
|
+
}
|
|
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
|
+
/** Getter for page access in audio capture. */
|
|
552
|
+
get _page() {
|
|
553
|
+
return this.controller.page;
|
|
554
|
+
}
|
|
555
|
+
// Audio capture + MIDI export logic moved to src/server/tools/capture.ts.
|
|
556
|
+
// Server still owns the AudioCaptureService lifecycle so tests can mock
|
|
557
|
+
// the class and the module fetches the (possibly mocked) instance via
|
|
558
|
+
// ctx.getAudioCaptureService() instead of caching its own.
|
|
559
|
+
async getAudioCaptureService() {
|
|
560
|
+
if (!this.isInitialized || !this._page) {
|
|
561
|
+
throw new Error('Browser not initialized. Run init first.');
|
|
562
|
+
}
|
|
563
|
+
if (!this.audioCaptureService) {
|
|
564
|
+
this.audioCaptureService = new AudioCaptureService();
|
|
565
|
+
await this.audioCaptureService.injectRecorder(this._page);
|
|
566
|
+
}
|
|
567
|
+
return this.audioCaptureService;
|
|
568
|
+
}
|
|
569
|
+
async run() {
|
|
570
|
+
const transport = new StdioServerTransport();
|
|
571
|
+
await this.server.connect(transport);
|
|
572
|
+
this.logger.info('Enhanced Strudel MCP server v2.0.1 running (fixed)');
|
|
573
|
+
process.on('SIGINT', async () => {
|
|
574
|
+
this.logger.info('Shutting down...');
|
|
575
|
+
await this.controller.cleanup();
|
|
576
|
+
await this.sessionManager.destroyAll();
|
|
577
|
+
process.exit(0);
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|