Skip to main content
Glama
EnhancedMCPServerFixed.ts49.5 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } 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'; import { PerformanceMonitor } from '../utils/PerformanceMonitor.js'; import { InputValidator } from '../utils/InputValidator.js'; const configPath = './config.json'; const config = existsSync(configPath) ? JSON.parse(readFileSync(configPath, 'utf-8')) : { headless: false }; /** History entry with metadata for pattern browsing */ interface HistoryEntry { id: number; pattern: string; timestamp: Date; action: string; } export class EnhancedMCPServerFixed { private server: Server; private controller: StrudelController; private store: PatternStore; private theory: MusicTheory; private generator: PatternGenerator; private logger: Logger; private perfMonitor: PerformanceMonitor; private sessionHistory: string[] = []; private undoStack: string[] = []; private redoStack: string[] = []; /** Pattern history with metadata for browsing (#41) */ private historyStack: HistoryEntry[] = []; private historyIdCounter: number = 0; /** Maximum history entries to prevent memory leaks */ private readonly MAX_HISTORY = 100; private isInitialized: boolean = false; private generatedPatterns: Map<string, string> = 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.perfMonitor = new PerformanceMonitor(); this.setupHandlers(); } private getTools(): Tool[] { // 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 with optional auto-play and validation', inputSchema: { type: 'object', properties: { pattern: { type: 'string', description: 'Pattern code' }, auto_play: { type: 'boolean', description: 'Start playback immediately after writing (default: false)' }, validate: { type: 'boolean', description: 'Validate pattern before writing (default: true)' } }, 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 with optional auto-play', 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' }, auto_play: { type: 'boolean', description: 'Start playback immediately (default: false)' } }, 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: {} } }, { name: 'validate_pattern_runtime', description: 'Validate pattern with runtime error checking (monitors Strudel console for errors)', inputSchema: { type: 'object', properties: { pattern: { type: 'string', description: 'Pattern code to validate' }, waitMs: { type: 'number', description: 'How long to wait for errors (default 500ms)' } }, required: ['pattern'] } }, // 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: {} } }, // Pattern History Tools (#41) { name: 'list_history', description: 'List recent pattern history with timestamps and previews', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Maximum entries to return (default: 10)' } } } }, { name: 'restore_history', description: 'Restore a previous pattern from history by ID', inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'History entry ID to restore' } }, required: ['id'] } }, { name: 'compare_patterns', description: 'Compare two patterns from history showing differences', inputSchema: { type: 'object', properties: { id1: { type: 'number', description: 'First pattern ID' }, id2: { type: 'number', description: 'Second pattern ID (default: current pattern)' } }, required: ['id1'] } }, // 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'] } }, // Performance Monitoring (2) { name: 'performance_report', description: 'Get performance metrics and bottlenecks', inputSchema: { type: 'object', properties: {} } }, { name: 'memory_usage', description: 'Get current memory usage statistics', inputSchema: { type: 'object', properties: {} } }, // UX Tools - Browser Control (#37) { name: 'show_browser', description: 'Bring browser window to foreground for visual feedback', inputSchema: { type: 'object', properties: {} } }, { name: 'screenshot', description: 'Take a screenshot of the current Strudel editor state', inputSchema: { type: 'object', properties: { filename: { type: 'string', description: 'Optional filename for screenshot' } } } }, // UX Tools - Status & Diagnostics (#39) { name: 'status', description: 'Get current browser and playback status (quick state check)', inputSchema: { type: 'object', properties: {} } }, { name: 'diagnostics', description: 'Get detailed browser diagnostics including cache, errors, and performance', inputSchema: { type: 'object', properties: {} } }, { name: 'show_errors', description: 'Display captured console errors and warnings from Strudel', inputSchema: { type: 'object', properties: {} } }, // UX Tools - High-level Compose (#42) { name: 'compose', description: 'Generate, write, and play a complete pattern in one step. Auto-initializes browser if needed.', inputSchema: { type: 'object', properties: { style: { type: 'string', description: 'Genre: techno, house, dnb, ambient, trap, jungle, jazz, experimental' }, tempo: { type: 'number', description: 'BPM (default: genre-appropriate)' }, key: { type: 'string', description: 'Musical key (default: C)' }, auto_play: { type: 'boolean', description: 'Start playback immediately (default: true)' } }, required: ['style'] } } ]; } private 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); // Measure performance const result = await this.perfMonitor.measureAsync( name, () => this.executeTool(name, args) ); return { content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }], }; } catch (error: any) { this.logger.error(`Tool execution failed: ${name}`, error); return { content: [{ type: 'text', text: `Error: ${error.message}` }], }; } }); } private requiresInitialization(toolName: string): boolean { 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', 'validate_pattern_runtime' ]; 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); } private async getCurrentPatternSafe(): Promise<string> { 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 ''; } } private async writePatternSafe(pattern: string): Promise<string> { 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); } private async executeTool(name: string, args: any): Promise<any> { // 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 and history (#41) (only if initialized) if (['write', 'append', 'insert', 'replace', 'clear'].includes(name) && this.isInitialized) { try { const current = await this.controller.getCurrentPattern(); this.undoStack.push(current); // Add to history stack with metadata (#41) this.historyIdCounter++; this.historyStack.push({ id: this.historyIdCounter, pattern: current, timestamp: new Date(), action: name }); // Enforce bounds to prevent memory leaks if (this.undoStack.length > this.MAX_HISTORY) { this.undoStack.shift(); } if (this.historyStack.length > this.MAX_HISTORY) { this.historyStack.shift(); } 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': InputValidator.validateStringLength(args.pattern, 'pattern', 10000, true); // Validate pattern if requested (default: true) - Issue #40 if (args.validate !== false && this.isInitialized && typeof this.controller.validatePattern === 'function') { try { const validation = await this.controller.validatePattern(args.pattern); if (validation && !validation.valid) { return { success: false, errors: validation.errors, warnings: validation.warnings, suggestions: validation.suggestions, message: `Pattern validation failed: ${validation.errors.join('; ')}` }; } } catch (e) { // Validation failed, but we can still try to write this.logger.warn('Pattern validation threw error, continuing with write'); } } const writeResult = await this.writePatternSafe(args.pattern); // Auto-play if requested - Issue #38 if (args.auto_play && this.isInitialized) { await this.controller.play(); return `${writeResult}. Playing.`; } return writeResult; case 'append': InputValidator.validateStringLength(args.code, 'code', 10000, true); const current = await this.getCurrentPatternSafe(); return await this.writePatternSafe(current + '\n' + args.code); case 'insert': InputValidator.validatePositiveInteger(args.position, 'position'); InputValidator.validateStringLength(args.code, 'code', 10000, true); const lines = (await this.getCurrentPatternSafe()).split('\n'); lines.splice(args.position, 0, args.code); return await this.writePatternSafe(lines.join('\n')); case 'replace': InputValidator.validateStringLength(args.search, 'search', 1000, true); InputValidator.validateStringLength(args.replace, 'replace', 10000, true); 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': InputValidator.validateStringLength(args.style, 'style', 100, false); if (args.key) { InputValidator.validateRootNote(args.key); } if (args.bpm !== undefined) { InputValidator.validateBPM(args.bpm); } const generated = this.generator.generateCompletePattern( args.style, args.key || 'C', args.bpm || 120 ); await this.writePatternSafe(generated); // Auto-play if requested - Issue #38 if (args.auto_play && this.isInitialized) { await this.controller.play(); return `Generated ${args.style} pattern. Playing.`; } return `Generated ${args.style} pattern`; case 'generate_drums': InputValidator.validateStringLength(args.style, 'style', 100, false); if (args.complexity !== undefined) { InputValidator.validateNormalizedValue(args.complexity, 'complexity'); } 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': InputValidator.validateRootNote(args.key); InputValidator.validateStringLength(args.style, 'style', 100, false); 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': InputValidator.validateRootNote(args.root); InputValidator.validateScaleName(args.scale); if (args.length !== undefined) { InputValidator.validatePositiveInteger(args.length, 'length'); } 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': InputValidator.validateRootNote(args.root); InputValidator.validateScaleName(args.scale); const scaleNotes = this.theory.generateScale(args.root, args.scale); return `${args.root} ${args.scale} scale: ${scaleNotes.join(', ')}`; case 'generate_chord_progression': InputValidator.validateRootNote(args.key); InputValidator.validateChordStyle(args.style); 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': InputValidator.validateEuclidean(args.hits, args.steps); if (args.sound) { InputValidator.validateStringLength(args.sound, 'sound', 100, false); } 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': args.sounds.forEach((sound: string) => { InputValidator.validateStringLength(sound, 'sound', 100, false); }); args.patterns.forEach((pattern: number) => { InputValidator.validatePositiveInteger(pattern, 'pattern'); }); 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': InputValidator.validateStringLength(args.style, 'style', 100, false); if (args.bars !== undefined) { InputValidator.validatePositiveInteger(args.bars, 'bars'); } 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': // Semitones can be positive or negative, just validate it's a number if (typeof args.semitones !== 'number' || !Number.isInteger(args.semitones)) { throw new Error('Semitones must be an integer'); } 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': InputValidator.validateGain(args.factor); // Positive number, use gain validator for simplicity const toStretch = await this.getCurrentPatternSafe(); const stretched = toStretch + `.slow(${args.factor})`; await this.writePatternSafe(stretched); return `Stretched by factor of ${args.factor}`; case 'humanize': if (args.amount !== undefined) { InputValidator.validateNormalizedValue(args.amount, 'amount'); } 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': InputValidator.validateStringLength(args.effect, 'effect', 100, false); if (args.params) { InputValidator.validateStringLength(args.params, 'params', 1000, true); } 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': InputValidator.validateNormalizedValue(args.amount, 'amount'); const currentSwing = await this.getCurrentPatternSafe(); const withSwing = currentSwing + `.swing(${args.amount})`; await this.writePatternSafe(withSwing); return `Added swing: ${args.amount}`; case 'set_tempo': InputValidator.validateBPM(args.bpm); 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': if (!this.isInitialized) { return 'Browser not initialized. Run init first.'; } try { const tempoAnalysis = await this.controller.detectTempo(); if (!tempoAnalysis || tempoAnalysis.bpm === 0) { return { bpm: 0, confidence: 0, message: 'No tempo detected. Ensure audio is playing and has a clear rhythmic pattern.' }; } return { bpm: tempoAnalysis.bpm, confidence: Math.round(tempoAnalysis.confidence * 100) / 100, method: tempoAnalysis.method, message: `Detected ${tempoAnalysis.bpm} BPM with ${Math.round(tempoAnalysis.confidence * 100)}% confidence` }; } catch (error: any) { return { bpm: 0, confidence: 0, error: error.message || 'Tempo detection failed' }; } case 'detect_key': if (!this.isInitialized) { return 'Browser not initialized. Run init first.'; } try { const keyAnalysis = await this.controller.detectKey(); if (!keyAnalysis || keyAnalysis.confidence < 0.1) { return { key: 'Unknown', scale: 'unknown', confidence: 0, message: 'No clear key detected. Ensure audio is playing and has tonal content.' }; } const result: any = { key: keyAnalysis.key, scale: keyAnalysis.scale, confidence: Math.round(keyAnalysis.confidence * 100) / 100, message: `Detected ${keyAnalysis.key} ${keyAnalysis.scale} with ${Math.round(keyAnalysis.confidence * 100)}% confidence` }; // Include alternatives if available and confidence is moderate if (keyAnalysis.alternatives && keyAnalysis.alternatives.length > 0) { result.alternatives = keyAnalysis.alternatives.map((alt: any) => ({ key: alt.key, scale: alt.scale, confidence: Math.round(alt.confidence * 100) / 100 })); } return result; } catch (error: any) { return { key: 'Unknown', scale: 'unknown', confidence: 0, error: error.message || 'Key detection failed' }; } case 'validate_pattern_runtime': if (!this.isInitialized) { return 'Browser not initialized. Run init first.'; } InputValidator.validateStringLength(args.pattern, 'pattern', 10000, false); const validation = await this.controller.validatePatternRuntime( args.pattern, args.waitMs || 500 ); if (validation.valid) { return `✅ Pattern valid - no runtime errors detected`; } else { return `❌ Pattern has runtime errors:\n${validation.errors.join('\n')}\n` + (validation.warnings.length > 0 ? `\nWarnings:\n${validation.warnings.join('\n')}` : ''); } // Session Management case 'save': InputValidator.validateStringLength(args.name, 'name', 255, false); 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': InputValidator.validateStringLength(args.name, 'name', 255, false); 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': if (args?.tag) { InputValidator.validateStringLength(args.tag, 'tag', 100, false); } 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); // Enforce bounds to prevent memory leaks if (this.redoStack.length > this.MAX_HISTORY) { this.redoStack.shift(); } 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); // Enforce bounds to prevent memory leaks if (this.undoStack.length > this.MAX_HISTORY) { this.undoStack.shift(); } const next = this.redoStack.pop()!; await this.controller.writePattern(next); return 'Redone'; } return 'Nothing to redo'; // Pattern History (#41) case 'list_history': if (this.historyStack.length === 0) { return 'No pattern history yet. Make some edits to build history.'; } const limit = args?.limit || 10; const recentHistory = this.historyStack.slice(-limit).reverse(); return { count: this.historyStack.length, showing: recentHistory.length, entries: recentHistory.map(entry => ({ id: entry.id, preview: entry.pattern.substring(0, 60) + (entry.pattern.length > 60 ? '...' : ''), chars: entry.pattern.length, action: entry.action, timestamp: this.formatTimeAgo(entry.timestamp) })) }; case 'restore_history': if (!this.isInitialized) { return 'Browser not initialized. Run init first.'; } const entryToRestore = this.historyStack.find(e => e.id === args.id); if (!entryToRestore) { return `History entry #${args.id} not found. Use list_history to see available entries.`; } // Save current state before restoring const currentBeforeRestore = await this.controller.getCurrentPattern(); this.undoStack.push(currentBeforeRestore); if (this.undoStack.length > this.MAX_HISTORY) { this.undoStack.shift(); } await this.controller.writePattern(entryToRestore.pattern); return `Restored pattern from history #${args.id} (${this.formatTimeAgo(entryToRestore.timestamp)})`; case 'compare_patterns': const entry1 = this.historyStack.find(e => e.id === args.id1); if (!entry1) { return `History entry #${args.id1} not found.`; } let pattern2: string; let label2: string; if (args.id2) { const entry2 = this.historyStack.find(e => e.id === args.id2); if (!entry2) { return `History entry #${args.id2} not found.`; } pattern2 = entry2.pattern; label2 = `#${args.id2}`; } else { pattern2 = await this.getCurrentPatternSafe(); label2 = 'current'; } const diff = this.generateDiff(entry1.pattern, pattern2); return { pattern1: { id: args.id1, chars: entry1.pattern.length }, pattern2: { id: label2, chars: pattern2.length }, diff: diff, summary: this.summarizeDiff(entry1.pattern, pattern2) }; // Performance Monitoring case 'performance_report': const report = this.perfMonitor.getReport(); const bottlenecks = this.perfMonitor.getBottlenecks(5); return `${report}\n\nTop 5 Bottlenecks:\n${JSON.stringify(bottlenecks, null, 2)}`; case 'memory_usage': const memory = this.perfMonitor.getMemoryUsage(); return memory ? JSON.stringify(memory, null, 2) : 'Memory usage not available'; // UX Tools - Browser Control (#37) case 'show_browser': if (!this.isInitialized) { return 'Browser not initialized. Run init first.'; } return await this.controller.showBrowser(); case 'screenshot': if (!this.isInitialized) { return 'Browser not initialized. Run init first.'; } return await this.controller.takeScreenshot(args?.filename); // UX Tools - Status & Diagnostics (#39) case 'status': return this.controller.getStatus(); case 'diagnostics': if (!this.isInitialized) { return { initialized: false, message: 'Browser not initialized. Run init first for full diagnostics.' }; } return await this.controller.getDiagnostics(); case 'show_errors': const errors = this.controller.getConsoleErrors(); const warnings = this.controller.getConsoleWarnings(); if (errors.length === 0 && warnings.length === 0) { return 'No errors or warnings captured.'; } let result = ''; if (errors.length > 0) { result += `❌ Errors (${errors.length}):\n${errors.map(e => ` • ${e}`).join('\n')}\n`; } if (warnings.length > 0) { result += `⚠️ Warnings (${warnings.length}):\n${warnings.map(w => ` • ${w}`).join('\n')}`; } return result.trim(); // UX Tools - High-level Compose (#42) case 'compose': InputValidator.validateStringLength(args.style, 'style', 100, false); if (args.key) { InputValidator.validateRootNote(args.key); } if (args.tempo !== undefined) { InputValidator.validateBPM(args.tempo); } // Auto-initialize if needed if (!this.isInitialized) { await this.controller.initialize(); this.isInitialized = true; } // Generate pattern const composedPattern = this.generator.generateCompletePattern( args.style, args.key || 'C', args.tempo || this.getDefaultTempo(args.style) ); // Write pattern await this.controller.writePattern(composedPattern); // Auto-play by default (unless explicitly set to false) const shouldPlay = args.auto_play !== false; if (shouldPlay) { await this.controller.play(); } return { success: true, pattern: composedPattern.substring(0, 200) + (composedPattern.length > 200 ? '...' : ''), metadata: { style: args.style, bpm: args.tempo || this.getDefaultTempo(args.style), key: args.key || 'C' }, status: shouldPlay ? 'playing' : 'ready', message: `Created ${args.style} pattern in ${args.key || 'C'}${shouldPlay ? ' - now playing' : ''}` }; default: throw new Error(`Unknown tool: ${name}`); } } private transposePattern(pattern: string, semitones: number): string { // Simple transpose implementation - would need more sophisticated parsing return pattern.replace(/([a-g][#b]?)(\d)/gi, (match, note, octave) => { const noteMap: Record<string, number> = { '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; }); } /** * Gets the default tempo for a given music style * @param style - Music style/genre * @returns Default BPM for the style */ private getDefaultTempo(style: string): number { const tempoMap: Record<string, number> = { 'techno': 130, 'house': 125, 'dnb': 174, 'drum and bass': 174, 'ambient': 80, 'trap': 140, 'jungle': 160, 'jazz': 110, 'experimental': 120, 'dubstep': 140, 'trance': 138, 'breakbeat': 130, 'garage': 130, 'electro': 128, 'downtempo': 90, 'idm': 115 }; return tempoMap[style.toLowerCase()] || 120; } /** * Formats a timestamp as human-readable "time ago" string * @param date - Date to format * @returns Human-readable string like "2m ago" or "1h ago" */ private formatTimeAgo(date: Date): string { const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000); if (seconds < 60) return `${seconds}s ago`; if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; return `${Math.floor(seconds / 86400)}d ago`; } /** * Generates a simple line-by-line diff between two patterns * @param pattern1 - First pattern * @param pattern2 - Second pattern * @returns Diff output showing additions and removals */ private generateDiff(pattern1: string, pattern2: string): string[] { const lines1 = pattern1.split('\n'); const lines2 = pattern2.split('\n'); const diff: string[] = []; const maxLines = Math.max(lines1.length, lines2.length); for (let i = 0; i < maxLines; i++) { const line1 = lines1[i] || ''; const line2 = lines2[i] || ''; if (line1 === line2) { diff.push(` ${line1}`); } else { if (line1) diff.push(`- ${line1}`); if (line2) diff.push(`+ ${line2}`); } } return diff; } /** * Summarizes differences between two patterns * @param pattern1 - First pattern * @param pattern2 - Second pattern * @returns Summary of differences */ private summarizeDiff(pattern1: string, pattern2: string): { linesAdded: number; linesRemoved: number; linesChanged: number; charsDiff: number; } { const lines1 = pattern1.split('\n'); const lines2 = pattern2.split('\n'); let linesAdded = 0; let linesRemoved = 0; let linesChanged = 0; const maxLines = Math.max(lines1.length, lines2.length); for (let i = 0; i < maxLines; i++) { const line1 = lines1[i]; const line2 = lines2[i]; if (line1 === undefined && line2 !== undefined) { linesAdded++; } else if (line1 !== undefined && line2 === undefined) { linesRemoved++; } else if (line1 !== line2) { linesChanged++; } } return { linesAdded, linesRemoved, linesChanged, charsDiff: pattern2.length - pattern1.length }; } 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); }); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/williamzujkowski/strudel-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server