Skip to main content
Glama
InputValidator.ts10.6 kB
/** * InputValidator - Comprehensive parameter validation for Strudel MCP Server * * Provides robust validation for all user inputs to prevent: * - Invalid musical parameters (BPM, gain, etc.) * - Speaker damage from excessive gain values * - Mathematically invalid patterns (e.g., Euclidean rhythms) * - Security issues from malformed inputs * * All methods throw descriptive errors for invalid inputs. */ export class InputValidator { // Valid scale names from MusicTheory service private static readonly VALID_SCALES = [ 'major', 'minor', 'dorian', 'phrygian', 'lydian', 'mixolydian', 'aeolian', 'locrian', 'pentatonic', 'blues', 'chromatic', 'wholetone', 'harmonic_minor', 'melodic_minor' ]; // Valid chord progression styles from MusicTheory service private static readonly VALID_CHORD_STYLES = [ 'pop', 'jazz', 'blues', 'folk', 'rock', 'classical', 'modal', 'edm' ]; // Valid root notes (12-tone chromatic scale) private static readonly VALID_ROOT_NOTES = [ 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B' ]; /** * Validates BPM (Beats Per Minute) is within acceptable range * * @param bpm - Tempo in beats per minute * @throws {Error} When BPM is not a number, not finite, or outside 20-300 range * * @example * InputValidator.validateBPM(120); // ✓ Valid * InputValidator.validateBPM(19); // ✗ Throws: BPM must be between 20 and 300 */ static validateBPM(bpm: number): void { if (typeof bpm !== 'number') { throw new Error('BPM must be a number'); } if (isNaN(bpm) || !isFinite(bpm)) { throw new Error('BPM must be a valid number'); } if (bpm < 20 || bpm > 300) { throw new Error(`BPM must be between 20 and 300, got ${bpm}`); } } /** * Validates gain is within safe range to prevent speaker damage * * @param gain - Audio gain multiplier (0 = silence, 1 = unity, 2 = max safe) * @throws {Error} When gain is not a number, not finite, or outside 0-2.0 range * * @example * InputValidator.validateGain(1.0); // ✓ Valid (unity gain) * InputValidator.validateGain(10); // ✗ Throws: Gain must be between 0 and 2.0 */ static validateGain(gain: number): void { if (typeof gain !== 'number') { throw new Error('Gain must be a number'); } if (isNaN(gain) || !isFinite(gain)) { throw new Error('Gain must be a valid number'); } if (gain < 0 || gain > 2.0) { throw new Error(`Gain must be between 0 and 2.0, got ${gain}`); } } /** * Validates Euclidean rhythm parameters are mathematically valid * * Euclidean rhythms distribute hits evenly across steps. * Example: 3 hits in 8 steps = "X..X..X." pattern * * @param hits - Number of beats/hits (must be <= steps) * @param steps - Total number of steps in the pattern * @throws {Error} When hits > steps or parameters are not valid integers * * @example * InputValidator.validateEuclidean(3, 8); // ✓ Valid * InputValidator.validateEuclidean(10, 8); // ✗ Throws: Hits cannot exceed steps */ static validateEuclidean(hits: number, steps: number): void { // Validate hits if (typeof hits !== 'number') { throw new Error('Hits must be a number'); } if (isNaN(hits) || !isFinite(hits)) { throw new Error('Hits must be a valid number'); } if (!Number.isInteger(hits)) { throw new Error('Hits must be an integer'); } if (hits < 0) { throw new Error('Hits must be a non-negative integer'); } // Validate steps if (typeof steps !== 'number') { throw new Error('Steps must be a number'); } if (isNaN(steps) || !isFinite(steps)) { throw new Error('Steps must be a valid number'); } if (!Number.isInteger(steps)) { throw new Error('Steps must be an integer'); } if (steps <= 0) { throw new Error('Steps must be a positive integer'); } if (steps > 256) { throw new Error(`Steps cannot exceed 256, got ${steps}`); } // Validate relationship if (hits > steps) { throw new Error(`Hits (${hits}) cannot exceed steps (${steps})`); } } /** * Validates musical scale name is supported by MusicTheory service * * @param name - Scale name (must be lowercase) * @throws {Error} When scale name is invalid or not a string * * Supported scales: * - major, minor, pentatonic, blues * - Modes: dorian, phrygian, lydian, mixolydian, aeolian, locrian * - Special: chromatic, wholetone, harmonic_minor, melodic_minor * * @example * InputValidator.validateScaleName('major'); // ✓ Valid * InputValidator.validateScaleName('Major'); // ✗ Throws: Invalid scale (case-sensitive) */ static validateScaleName(name: string): void { if (typeof name !== 'string') { throw new Error('Scale name must be a string'); } const trimmed = name.trim(); if (trimmed.length === 0) { throw new Error('Scale name cannot be empty'); } if (trimmed.length > 100) { throw new Error('Scale name too long (max 100 characters)'); } if (!this.VALID_SCALES.includes(trimmed)) { throw new Error(`Invalid scale name: ${name}`); } } /** * Validates chord progression style is supported by MusicTheory service * * @param style - Chord style (must be lowercase) * @throws {Error} When chord style is invalid or not a string * * Supported styles: pop, jazz, blues, folk, rock, classical, modal, edm * * @example * InputValidator.validateChordStyle('jazz'); // ✓ Valid * InputValidator.validateChordStyle('Jazz'); // ✗ Throws: Invalid chord style */ static validateChordStyle(style: string): void { if (typeof style !== 'string') { throw new Error('Chord style must be a string'); } const trimmed = style.trim(); if (trimmed.length === 0) { throw new Error('Chord style cannot be empty'); } if (trimmed.length > 100) { throw new Error('Chord style too long (max 100 characters)'); } if (!this.VALID_CHORD_STYLES.includes(trimmed)) { throw new Error(`Invalid chord style: ${style}`); } } /** * Validates musical root note is a valid chromatic note * * @param note - Root note (A-G with optional #, case insensitive) * @throws {Error} When note is invalid or not a string * * Valid notes: C, C#, D, D#, E, F, F#, G, G#, A, A#, B * * @example * InputValidator.validateRootNote('C'); // ✓ Valid * InputValidator.validateRootNote('f#'); // ✓ Valid (normalized to F#) * InputValidator.validateRootNote('X'); // ✗ Throws: Invalid root note */ static validateRootNote(note: string): void { if (typeof note !== 'string') { throw new Error('Root note must be a string'); } const trimmed = note.trim(); if (trimmed.length === 0) { throw new Error('Root note cannot be empty'); } if (trimmed.length > 10) { throw new Error(`Root note too long (max 10 characters, got ${trimmed.length})`); } const normalized = trimmed.toUpperCase(); if (!this.VALID_ROOT_NOTES.includes(normalized)) { throw new Error(`Invalid root note: ${note}. Valid notes: ${this.VALID_ROOT_NOTES.join(', ')}`); } } /** * Validates string length to prevent resource exhaustion * * @param str - String to validate * @param fieldName - Field name for error messages * @param maxLength - Maximum allowed length (default: 1000) * @param allowEmpty - Whether to allow empty strings (default: false for identifiers, true for content) * @throws {Error} When string exceeds maximum length or is not a string * * @example * InputValidator.validateStringLength('hello', 'pattern', 100); // ✓ Valid * InputValidator.validateStringLength('x'.repeat(1001), 'pattern'); // ✗ Throws * InputValidator.validateStringLength('', 'pattern', 1000, true); // ✓ Valid (clearing pattern) * InputValidator.validateStringLength('', 'name', 1000, false); // ✗ Throws */ static validateStringLength(str: string, fieldName: string, maxLength: number = 1000, allowEmpty: boolean = true): void { if (typeof str !== 'string') { throw new Error(`${fieldName} must be a string`); } if (!allowEmpty && str.trim().length === 0) { throw new Error(`${fieldName} cannot be empty`); } if (str.length > maxLength) { throw new Error(`${fieldName} too long (max ${maxLength} characters, got ${str.length})`); } } /** * Validates a value is within 0-1 range (normalized/ratio value) * * Used for parameters like complexity, swing amount, humanize amount, etc. * that represent normalized ratios or percentages. * * @param value - Number to validate * @param fieldName - Field name for error messages * @throws {Error} When value is not in 0-1 range * * @example * InputValidator.validateNormalizedValue(0.5, 'complexity'); // ✓ Valid * InputValidator.validateNormalizedValue(1.5, 'complexity'); // ✗ Throws */ static validateNormalizedValue(value: number, fieldName: string): void { if (typeof value !== 'number') { throw new Error(`${fieldName} must be a number`); } if (isNaN(value) || !isFinite(value)) { throw new Error(`${fieldName} must be a valid number`); } if (value < 0 || value > 1.0) { throw new Error(`${fieldName} must be between 0 and 1.0, got ${value}`); } } /** * Validates a value is a positive integer (> 0) * * @param value - Number to validate * @param fieldName - Field name for error messages * @throws {Error} When value is not a positive integer * * @example * InputValidator.validatePositiveInteger(5, 'count'); // ✓ Valid * InputValidator.validatePositiveInteger(0, 'count'); // ✗ Throws: must be positive * InputValidator.validatePositiveInteger(1.5, 'count'); // ✗ Throws: must be integer */ static validatePositiveInteger(value: number, fieldName: string): void { if (typeof value !== 'number') { throw new Error(`${fieldName} must be a number`); } if (isNaN(value) || !isFinite(value)) { throw new Error(`${fieldName} must be a valid number`); } if (!Number.isInteger(value)) { throw new Error(`${fieldName} must be an integer`); } if (value <= 0) { throw new Error(`${fieldName} must be a positive integer`); } } }

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