UNPKG

@williamzujkowski/strudel-mcp-server

Version:

Advanced MCP server for AI-powered music generation with Strudel.cc

740 lines (739 loc) 32.1 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { StrudelController } from '../StrudelController.js'; import { PatternStore } from '../PatternStore.js'; import { MusicTheory } from '../services/MusicTheory.js'; import { PatternGenerator } from '../services/PatternGenerator.js'; import { readFileSync, existsSync } from 'fs'; import { Logger } from '../utils/Logger.js'; const configPath = './config.json'; const config = existsSync(configPath) ? JSON.parse(readFileSync(configPath, 'utf-8')) : { headless: false }; export class EnhancedMCPServerFixed { server; controller; store; theory; generator; logger; sessionHistory = []; undoStack = []; redoStack = []; isInitialized = false; generatedPatterns = new Map(); constructor() { this.server = new Server({ name: 'strudel-mcp-enhanced', version: '2.0.1', }, { capabilities: { tools: {}, }, }); this.controller = new StrudelController(config.headless); this.store = new PatternStore('./patterns'); this.theory = new MusicTheory(); this.generator = new PatternGenerator(); this.logger = new Logger(); this.setupHandlers(); } getTools() { // Same tools as before - keeping the same structure return [ // Core Control Tools (10) { name: 'init', description: 'Initialize Strudel in browser', inputSchema: { type: 'object', properties: {} } }, { name: 'write', description: 'Write pattern to editor', inputSchema: { type: 'object', properties: { pattern: { type: 'string', description: 'Pattern code' } }, required: ['pattern'] } }, { name: 'append', description: 'Append code to current pattern', inputSchema: { type: 'object', properties: { code: { type: 'string', description: 'Code to append' } }, required: ['code'] } }, { name: 'insert', description: 'Insert code at specific line', inputSchema: { type: 'object', properties: { position: { type: 'number', description: 'Line number' }, code: { type: 'string', description: 'Code to insert' } }, required: ['position', 'code'] } }, { name: 'replace', description: 'Replace pattern section', inputSchema: { type: 'object', properties: { search: { type: 'string', description: 'Text to replace' }, replace: { type: 'string', description: 'Replacement text' } }, required: ['search', 'replace'] } }, { name: 'play', description: 'Start playing pattern', inputSchema: { type: 'object', properties: {} } }, { name: 'pause', description: 'Pause playback', inputSchema: { type: 'object', properties: {} } }, { name: 'stop', description: 'Stop playback', inputSchema: { type: 'object', properties: {} } }, { name: 'clear', description: 'Clear the editor', inputSchema: { type: 'object', properties: {} } }, { name: 'get_pattern', description: 'Get current pattern code', inputSchema: { type: 'object', properties: {} } }, // Pattern Manipulation (10) { name: 'transpose', description: 'Transpose notes by semitones', inputSchema: { type: 'object', properties: { semitones: { type: 'number', description: 'Semitones to transpose' } }, required: ['semitones'] } }, { name: 'reverse', description: 'Reverse pattern', inputSchema: { type: 'object', properties: {} } }, { name: 'stretch', description: 'Time stretch pattern', inputSchema: { type: 'object', properties: { factor: { type: 'number', description: 'Stretch factor' } }, required: ['factor'] } }, { name: 'quantize', description: 'Quantize to grid', inputSchema: { type: 'object', properties: { grid: { type: 'string', description: 'Grid size (e.g., "1/16")' } }, required: ['grid'] } }, { name: 'humanize', description: 'Add human timing variation', inputSchema: { type: 'object', properties: { amount: { type: 'number', description: 'Humanization amount (0-1)' } } } }, { name: 'generate_variation', description: 'Create pattern variations', inputSchema: { type: 'object', properties: { type: { type: 'string', description: 'Variation type (subtle/moderate/extreme/glitch/evolving)' } } } }, { name: 'generate_pattern', description: 'Generate complete pattern from style', inputSchema: { type: 'object', properties: { style: { type: 'string', description: 'Music style (techno/house/dnb/ambient/etc)' }, key: { type: 'string', description: 'Musical key' }, bpm: { type: 'number', description: 'Tempo in BPM' } }, required: ['style'] } }, { name: 'generate_drums', description: 'Generate drum pattern', inputSchema: { type: 'object', properties: { style: { type: 'string', description: 'Drum style' }, complexity: { type: 'number', description: 'Complexity (0-1)' } }, required: ['style'] } }, { name: 'generate_bassline', description: 'Generate bassline', inputSchema: { type: 'object', properties: { key: { type: 'string', description: 'Musical key' }, style: { type: 'string', description: 'Bass style' } }, required: ['key', 'style'] } }, { name: 'generate_melody', description: 'Generate melody from scale', inputSchema: { type: 'object', properties: { scale: { type: 'string', description: 'Scale name' }, root: { type: 'string', description: 'Root note' }, length: { type: 'number', description: 'Number of notes' } }, required: ['scale', 'root'] } }, // Audio Analysis (5) { name: 'analyze', description: 'Complete audio analysis', inputSchema: { type: 'object', properties: {} } }, { name: 'analyze_spectrum', description: 'FFT spectrum analysis', inputSchema: { type: 'object', properties: {} } }, { name: 'analyze_rhythm', description: 'Rhythm analysis', inputSchema: { type: 'object', properties: {} } }, { name: 'detect_tempo', description: 'BPM detection', inputSchema: { type: 'object', properties: {} } }, { name: 'detect_key', description: 'Key detection', inputSchema: { type: 'object', properties: {} } }, // Effects & Processing (5) { name: 'add_effect', description: 'Add effect to pattern', inputSchema: { type: 'object', properties: { effect: { type: 'string', description: 'Effect name' }, params: { type: 'string', description: 'Effect parameters' } }, required: ['effect'] } }, { name: 'remove_effect', description: 'Remove effect', inputSchema: { type: 'object', properties: { effect: { type: 'string', description: 'Effect to remove' } }, required: ['effect'] } }, { name: 'set_tempo', description: 'Set BPM', inputSchema: { type: 'object', properties: { bpm: { type: 'number', description: 'Tempo in BPM' } }, required: ['bpm'] } }, { name: 'add_swing', description: 'Add swing to pattern', inputSchema: { type: 'object', properties: { amount: { type: 'number', description: 'Swing amount (0-1)' } }, required: ['amount'] } }, { name: 'apply_scale', description: 'Apply scale to notes', inputSchema: { type: 'object', properties: { scale: { type: 'string', description: 'Scale name' }, root: { type: 'string', description: 'Root note' } }, required: ['scale', 'root'] } }, // Session Management (5) { name: 'save', description: 'Save pattern with metadata', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Pattern name' }, tags: { type: 'array', items: { type: 'string' } } }, required: ['name'] } }, { name: 'load', description: 'Load saved pattern', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Pattern name' } }, required: ['name'] } }, { name: 'list', description: 'List saved patterns', inputSchema: { type: 'object', properties: { tag: { type: 'string', description: 'Filter by tag' } } } }, { name: 'undo', description: 'Undo last action', inputSchema: { type: 'object', properties: {} } }, { name: 'redo', description: 'Redo action', inputSchema: { type: 'object', properties: {} } }, // Additional Music Theory Tools (5) { name: 'generate_scale', description: 'Generate scale notes', inputSchema: { type: 'object', properties: { root: { type: 'string', description: 'Root note' }, scale: { type: 'string', description: 'Scale type' } }, required: ['root', 'scale'] } }, { name: 'generate_chord_progression', description: 'Generate chord progression', inputSchema: { type: 'object', properties: { key: { type: 'string', description: 'Key' }, style: { type: 'string', description: 'Style (pop/jazz/blues/etc)' } }, required: ['key', 'style'] } }, { name: 'generate_euclidean', description: 'Generate Euclidean rhythm', inputSchema: { type: 'object', properties: { hits: { type: 'number', description: 'Number of hits' }, steps: { type: 'number', description: 'Total steps' }, sound: { type: 'string', description: 'Sound to use' } }, required: ['hits', 'steps'] } }, { name: 'generate_polyrhythm', description: 'Generate polyrhythm', inputSchema: { type: 'object', properties: { sounds: { type: 'array', items: { type: 'string' }, description: 'Sounds to use' }, patterns: { type: 'array', items: { type: 'number' }, description: 'Pattern numbers' } }, required: ['sounds', 'patterns'] } }, { name: 'generate_fill', description: 'Generate drum fill', inputSchema: { type: 'object', properties: { style: { type: 'string', description: 'Fill style' }, bars: { type: 'number', description: 'Number of bars' } }, required: ['style'] } } ]; } setupHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: this.getTools() })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { this.logger.info(`Executing tool: ${name}`, args); let result = await this.executeTool(name, args); return { content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }], }; } catch (error) { this.logger.error(`Tool execution failed: ${name}`, error); return { content: [{ type: 'text', text: `Error: ${error.message}` }], }; } }); } requiresInitialization(toolName) { const toolsRequiringInit = [ 'write', 'append', 'insert', 'replace', 'play', 'pause', 'stop', 'clear', 'get_pattern', 'analyze', 'analyze_spectrum', 'analyze_rhythm', 'transpose', 'reverse', 'stretch', 'humanize', 'generate_variation', 'add_effect', 'add_swing', 'set_tempo', 'save', 'undo', 'redo' ]; const toolsRequiringWrite = [ 'generate_pattern', 'generate_drums', 'generate_bassline', 'generate_melody', 'generate_chord_progression', 'generate_euclidean', 'generate_polyrhythm', 'generate_fill' ]; return toolsRequiringInit.includes(toolName) || toolsRequiringWrite.includes(toolName); } async getCurrentPatternSafe() { if (!this.isInitialized) { // Return the last generated pattern if available const lastPattern = Array.from(this.generatedPatterns.values()).pop(); return lastPattern || ''; } try { return await this.controller.getCurrentPattern(); } catch (e) { return ''; } } async writePatternSafe(pattern) { if (!this.isInitialized) { // Store the pattern for later use const id = `pattern_${Date.now()}`; this.generatedPatterns.set(id, pattern); return `Pattern generated (initialize Strudel to use it): ${pattern.substring(0, 50)}...`; } return await this.controller.writePattern(pattern); } async executeTool(name, args) { // Check if tool needs initialization if (this.requiresInitialization(name) && !this.isInitialized && name !== 'init') { // For generation tools that don't require browser, handle them specially const generationTools = [ 'generate_pattern', 'generate_drums', 'generate_bassline', 'generate_melody', 'generate_chord_progression', 'generate_euclidean', 'generate_polyrhythm', 'generate_fill' ]; if (!generationTools.includes(name)) { return `Browser not initialized. Run 'init' first to use ${name}.`; } } // Save current state for undo (only if initialized) if (['write', 'append', 'insert', 'replace', 'clear'].includes(name) && this.isInitialized) { try { const current = await this.controller.getCurrentPattern(); this.undoStack.push(current); this.redoStack = []; } catch (e) { // Controller might not be initialized yet } } switch (name) { // Core Control case 'init': const initResult = await this.controller.initialize(); this.isInitialized = true; // Write any pending patterns if (this.generatedPatterns.size > 0) { const lastPattern = Array.from(this.generatedPatterns.values()).pop(); if (lastPattern) { await this.controller.writePattern(lastPattern); return `${initResult}. Loaded generated pattern.`; } } return initResult; case 'write': return await this.writePatternSafe(args.pattern); case 'append': const current = await this.getCurrentPatternSafe(); return await this.writePatternSafe(current + '\n' + args.code); case 'insert': const lines = (await this.getCurrentPatternSafe()).split('\n'); lines.splice(args.position, 0, args.code); return await this.writePatternSafe(lines.join('\n')); case 'replace': const pattern = await this.getCurrentPatternSafe(); const replaced = pattern.replace(args.search, args.replace); return await this.writePatternSafe(replaced); case 'play': return await this.controller.play(); case 'pause': case 'stop': return await this.controller.stop(); case 'clear': return await this.writePatternSafe(''); case 'get_pattern': return await this.getCurrentPatternSafe(); // Pattern Generation - These can work without browser case 'generate_pattern': const generated = this.generator.generateCompletePattern(args.style, args.key || 'C', args.bpm || 120); await this.writePatternSafe(generated); return `Generated ${args.style} pattern`; case 'generate_drums': const drums = this.generator.generateDrumPattern(args.style, args.complexity || 0.5); const currentDrum = await this.getCurrentPatternSafe(); const newDrumPattern = currentDrum ? currentDrum + '\n' + drums : drums; await this.writePatternSafe(newDrumPattern); return `Generated ${args.style} drums`; case 'generate_bassline': const bass = this.generator.generateBassline(args.key, args.style); const currentBass = await this.getCurrentPatternSafe(); const newBassPattern = currentBass ? currentBass + '\n' + bass : bass; await this.writePatternSafe(newBassPattern); return `Generated ${args.style} bassline in ${args.key}`; case 'generate_melody': const scale = this.theory.generateScale(args.root, args.scale); const melody = this.generator.generateMelody(scale, args.length || 8); const currentMelody = await this.getCurrentPatternSafe(); const newMelodyPattern = currentMelody ? currentMelody + '\n' + melody : melody; await this.writePatternSafe(newMelodyPattern); return `Generated melody in ${args.root} ${args.scale}`; // Music Theory - These don't require browser case 'generate_scale': const scaleNotes = this.theory.generateScale(args.root, args.scale); return `${args.root} ${args.scale} scale: ${scaleNotes.join(', ')}`; case 'generate_chord_progression': const progression = this.theory.generateChordProgression(args.key, args.style); const chordPattern = this.generator.generateChords(progression); const currentChords = await this.getCurrentPatternSafe(); const newChordPattern = currentChords ? currentChords + '\n' + chordPattern : chordPattern; await this.writePatternSafe(newChordPattern); return `Generated ${args.style} progression in ${args.key}: ${progression}`; case 'generate_euclidean': const euclidean = this.generator.generateEuclideanPattern(args.hits, args.steps, args.sound || 'bd'); const currentEuc = await this.getCurrentPatternSafe(); const newEucPattern = currentEuc ? currentEuc + '\n' + euclidean : euclidean; await this.writePatternSafe(newEucPattern); return `Generated Euclidean rhythm (${args.hits}/${args.steps})`; case 'generate_polyrhythm': const poly = this.generator.generatePolyrhythm(args.sounds, args.patterns); const currentPoly = await this.getCurrentPatternSafe(); const newPolyPattern = currentPoly ? currentPoly + '\n' + poly : poly; await this.writePatternSafe(newPolyPattern); return `Generated polyrhythm`; case 'generate_fill': const fill = this.generator.generateFill(args.style, args.bars || 1); const currentFill = await this.getCurrentPatternSafe(); const newFillPattern = currentFill ? currentFill + '\n' + fill : fill; await this.writePatternSafe(newFillPattern); return `Generated ${args.bars || 1} bar fill`; // Pattern Manipulation - These require browser case 'transpose': const toTranspose = await this.getCurrentPatternSafe(); const transposed = this.transposePattern(toTranspose, args.semitones); await this.writePatternSafe(transposed); return `Transposed ${args.semitones} semitones`; case 'reverse': const toReverse = await this.getCurrentPatternSafe(); const reversed = toReverse + '.rev'; await this.writePatternSafe(reversed); return 'Pattern reversed'; case 'stretch': const toStretch = await this.getCurrentPatternSafe(); const stretched = toStretch + `.slow(${args.factor})`; await this.writePatternSafe(stretched); return `Stretched by factor of ${args.factor}`; case 'humanize': const toHumanize = await this.getCurrentPatternSafe(); const humanized = toHumanize + `.nudge(rand.range(-${args.amount || 0.01}, ${args.amount || 0.01}))`; await this.writePatternSafe(humanized); return 'Added human timing'; case 'generate_variation': const toVary = await this.getCurrentPatternSafe(); const varied = this.generator.generateVariation(toVary, args.type || 'subtle'); await this.writePatternSafe(varied); return `Added ${args.type || 'subtle'} variation`; // Effects - These require browser case 'add_effect': const currentEffect = await this.getCurrentPatternSafe(); const withEffect = args.params ? currentEffect + `.${args.effect}(${args.params})` : currentEffect + `.${args.effect}()`; await this.writePatternSafe(withEffect); return `Added ${args.effect} effect`; case 'add_swing': const currentSwing = await this.getCurrentPatternSafe(); const withSwing = currentSwing + `.swing(${args.amount})`; await this.writePatternSafe(withSwing); return `Added swing: ${args.amount}`; case 'set_tempo': const currentTempo = await this.getCurrentPatternSafe(); const withTempo = `setcpm(${args.bpm})\n${currentTempo}`; await this.writePatternSafe(withTempo); return `Set tempo to ${args.bpm} BPM`; // Audio Analysis - Requires browser case 'analyze': if (!this.isInitialized) { return 'Browser not initialized. Run init first.'; } return await this.controller.analyzeAudio(); case 'analyze_spectrum': if (!this.isInitialized) { return 'Browser not initialized. Run init first.'; } const spectrum = await this.controller.analyzeAudio(); return spectrum.features || spectrum; case 'analyze_rhythm': if (!this.isInitialized) { return 'Browser not initialized. Run init first.'; } const analysis = await this.controller.analyzeAudio(); return { isPlaying: analysis.features?.isPlaying, tempo: 'Analysis pending implementation', pattern: 'Rhythm pattern analysis' }; case 'detect_tempo': return 'Tempo detection: Coming soon'; case 'detect_key': return 'Key detection: Coming soon'; // Session Management case 'save': const toSave = await this.getCurrentPatternSafe(); if (!toSave) { return 'No pattern to save'; } await this.store.save(args.name, toSave, args.tags || []); return `Pattern saved as "${args.name}"`; case 'load': const saved = await this.store.load(args.name); if (saved) { await this.writePatternSafe(saved.content); return `Loaded pattern "${args.name}"`; } return `Pattern "${args.name}" not found`; case 'list': const patterns = await this.store.list(args?.tag); return patterns.map(p => `• ${p.name} [${p.tags.join(', ')}] - ${p.timestamp}`).join('\n') || 'No patterns found'; case 'undo': if (!this.isInitialized) { return 'Browser not initialized. Run init first.'; } if (this.undoStack.length > 0) { const currentUndo = await this.controller.getCurrentPattern(); this.redoStack.push(currentUndo); const previous = this.undoStack.pop(); await this.controller.writePattern(previous); return 'Undone'; } return 'Nothing to undo'; case 'redo': if (!this.isInitialized) { return 'Browser not initialized. Run init first.'; } if (this.redoStack.length > 0) { const currentRedo = await this.controller.getCurrentPattern(); this.undoStack.push(currentRedo); const next = this.redoStack.pop(); await this.controller.writePattern(next); return 'Redone'; } return 'Nothing to redo'; default: throw new Error(`Unknown tool: ${name}`); } } transposePattern(pattern, semitones) { // Simple transpose implementation - would need more sophisticated parsing return pattern.replace(/([a-g][#b]?)(\d)/gi, (match, note, octave) => { const noteMap = { 'c': 0, 'c#': 1, 'd': 2, 'd#': 3, 'e': 4, 'f': 5, 'f#': 6, 'g': 7, 'g#': 8, 'a': 9, 'a#': 10, 'b': 11 }; const currentNote = note.toLowerCase(); const noteValue = noteMap[currentNote] || 0; const newNoteValue = (noteValue + semitones + 12) % 12; const noteNames = ['c', 'c#', 'd', 'd#', 'e', 'f', 'f#', 'g', 'g#', 'a', 'a#', 'b']; const newOctave = parseInt(octave) + Math.floor((noteValue + semitones) / 12); return noteNames[newNoteValue] + newOctave; }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); this.logger.info('Enhanced Strudel MCP server v2.0.1 running (fixed)'); process.on('SIGINT', async () => { this.logger.info('Shutting down...'); await this.controller.cleanup(); process.exit(0); }); } }