u-he-preset-randomizer
Version:
Create random u-he synth presets through randomization and merging of your existing presets.
1,201 lines (1,195 loc) • 51.1 kB
JavaScript
#!/usr/bin/env node
/**
* @file Model Context Protocol (MCP) server for u-he preset randomizer.
* Provides AI assistants with tools to browse, search, analyze, and generate u-he synth presets.
*/
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join, normalize, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
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 chalk from 'chalk';
import { getDefaultConfig } from './config.js';
import { generatePresets } from './generatePresets.js';
import { narrowDownByAuthor, narrowDownByCategory, narrowDownByFavoritesFile, } from './libraryFilters.js';
import { isValidPreset, parseUhePreset } from './parser.js';
import { loadPresetLibrary } from './presetLibrary.js';
import { detectPresetLibraryLocations, uheSynthNames, } from './utils/detector.js';
// Load package.json
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageJsonPath = join(__dirname, '../package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
const DEFAULT_PRESET_AMOUNT = 16;
const SKILLS_DIR = join(__dirname, '..', 'src', 'skills');
const synthContextFileOverrides = {
ZebraHZ: 'Zebra2',
};
function resolveContextFilePath(synthName) {
const contextFileName = synthContextFileOverrides[synthName] ?? synthName;
return join(SKILLS_DIR, `${contextFileName}.md`);
}
const state = {
availableSynths: [],
currentSynth: null,
presetLibrary: null,
config: getDefaultConfig(),
detectionWarningLogged: false,
};
function ensureSynthDetection() {
if (state.availableSynths.length > 0) {
return;
}
try {
state.availableSynths = detectPresetLibraryLocations(state.config);
}
catch (error) {
if (!state.detectionWarningLogged) {
console.error(chalk.yellow('Warning: Synth detection failed when listing tools.'), error);
state.detectionWarningLogged = true;
}
}
}
/**
* Load synth-specific context documentation from the skills directory.
* @param synthName The name of the synth to load context for
* @returns The context content as a string, or null if not available
*/
function loadSynthContext(synthName) {
try {
const contextFilePath = resolveContextFilePath(synthName);
if (!existsSync(contextFilePath)) {
return null;
}
return readFileSync(contextFilePath, 'utf-8');
}
catch (error) {
console.error(`Failed to load context file for ${synthName}:`, error);
return null;
}
}
/**
* Check if context documentation is available for a synth.
* @param synthName The name of the synth to check
* @returns True if context documentation exists
*/
function hasContextAvailable(synthName) {
const contextFilePath = resolveContextFilePath(synthName);
return existsSync(contextFilePath);
}
function getSynthOptions() {
ensureSynthDetection();
if (state.availableSynths.length > 0) {
const uniqueSynths = Array.from(new Set(state.availableSynths.map((s) => s.synthName))).sort((a, b) => a.localeCompare(b));
return uniqueSynths;
}
return [...uheSynthNames];
}
function resolveAmount(requestedAmount) {
if (typeof requestedAmount === 'number' &&
Number.isFinite(requestedAmount) &&
requestedAmount > 0) {
return Math.floor(requestedAmount);
}
return DEFAULT_PRESET_AMOUNT;
}
const serverInstructions = [
'1. Use list_synths to discover installed u-he synths.',
'2. Call select_synth to load a library (supported: ' +
Array.from(uheSynthNames).join(', ') +
').',
' TIP: For large libraries (3000+ presets), use pattern parameter for faster loading.',
' Example: select_synth(synth="Hive", pattern="Bass/**/*") loads only bass presets.',
' Omit pattern to load all presets (slower but gives full library access).',
'3. Explore presets with search_presets, filter_presets, explain_preset.',
'4. Generate new sounds (all generated presets are automatically added to the loaded library):',
' - generate_random_presets: Fully random patches (supports author/category/pattern/favorites filters)',
' - randomize_presets: Variations of existing presets (supports author/category/pattern filters)',
' - merge_presets: Hybrid blends of multiple presets (supports author/category filters + wildcards)',
' NOTE: Generation filters work on loaded presets only. If library was loaded with pattern,',
' only those presets are available for filtering/generation.',
'5. Use get_synth_context for technical documentation (Diva, Hive, Repro-1, Repro-5).',
'',
'COMMON WORKFLOWS:',
'- Load and generate: select_synth(synth="Hive") → generate_random_presets(category="Bass", amount=16)',
'- Fast focused workflow: select_synth(synth="Hive", pattern="Bass/**/*") → generate_random_presets(amount=16)',
'- Preset variations: randomize_presets(preset_names=["My Favorite"], amount=4, randomness=30)',
'- Expand filtered library: select_synth(synth="Hive") [omit pattern to reload all]',
'',
'IMPORTANT: In randomize_presets, "amount" is per source preset:',
'- 1 preset + amount=4 → 4 files total',
'- 10 presets (via author filter) + amount=2 → 20 files total',
].join('\n');
// Initialize MCP server
const server = new Server({
name: 'u-he-preset-randomizer',
version: packageJson.version,
}, {
capabilities: {
tools: {},
},
instructions: serverInstructions,
});
// Define all available tools
const tools = [
{
name: 'list_synths',
description: 'List all available u-he synths detected on this system. Call this to see which synths are installed and can be selected.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'select_synth',
description: 'Select a synth and load its preset library. MUST be called before other preset tools. Optional pattern filters which presets are loaded (for faster loading with large libraries). Trade-off: filtered loading is faster but limits subsequent operations to only those presets until reloaded. Omit pattern for full library access (recommended unless library is very large).',
inputSchema: {
type: 'object',
properties: {
synth: {
type: 'string',
description: 'Name of the synth to select (e.g., "Diva", "Zebra3", "Hive")',
},
pattern: {
type: 'string',
description: 'Optional glob pattern to filter loaded presets (e.g., "Bass/**/*", "**/*Pad*"). Use for faster loading with large libraries (3000+ presets). Omit to load all presets (default, recommended). To switch between filtered/full, call again with/without pattern.',
},
},
required: ['synth'],
},
},
{
name: 'get_current_synth',
description: 'Get information about the currently selected synth, including its name and the number of loaded presets.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'list_presets',
description: 'List all loaded presets for the current synth with their names, categories, and authors. Use this for browsing available presets. Requires a synth to be selected first.',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Maximum number of presets to return. Defaults to 50. Use higher values to see more presets.',
},
offset: {
type: 'number',
description: 'Number of presets to skip before returning results. Use for pagination.',
},
},
required: [],
},
},
{
name: 'search_presets',
description: 'Search presets by name, category, or author using fuzzy matching. Returns matching presets with relevance scoring. Requires a synth to be selected first.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query to match against preset names, categories, and authors (e.g., "bass", "pad", "fat")',
},
limit: {
type: 'number',
description: 'Maximum number of results to return. Defaults to 20.',
},
},
required: ['query'],
},
},
{
name: 'filter_presets',
description: 'Filter presets by specific criteria including category (hierarchical), author, favorites file, or glob pattern. Can combine multiple filters. Requires a synth to be selected first.',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Filter by category prefix (e.g., "Bass", "Bass:Sub", "Lead:Mono"). Uses hierarchical matching.',
},
author: {
type: 'string',
description: 'Filter by preset author/creator name (exact match required)',
},
favorites: {
type: 'string',
description: 'Filter by favorites file name (e.g., "MyFavorites.uhe-fav")',
},
pattern: {
type: 'string',
description: 'Glob pattern to filter preset paths (e.g., "Bass/**/*", "*Pad*")',
},
},
required: [],
},
},
{
name: 'explain_preset',
description: 'Get detailed information about a specific preset including all metadata, parameters, and values. Use this to understand what makes a preset unique. Requires a synth to be selected first.',
inputSchema: {
type: 'object',
properties: {
preset_name: {
type: 'string',
description: 'Name of the preset to explain (without .h2p extension)',
},
include_parameters: {
type: 'boolean',
description: 'Include full parameter list in the explanation. Defaults to false (only shows metadata).',
},
},
required: ['preset_name'],
},
},
{
name: 'get_categories',
description: 'Get a list of all unique preset categories available in the currently loaded presets. Useful for discovering what types of sounds are available. Requires a synth to be selected first.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'get_authors',
description: 'Get a list of all unique preset authors/creators in the currently loaded presets. Useful for filtering by favorite sound designers. Requires a synth to be selected first.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'get_favorites_files',
description: 'Get a list of all .uhe-fav favorites files found for the current synth. These can be used for filtering presets. Requires a synth to be selected first.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'get_synth_context',
description: 'Get detailed technical documentation about how the currently selected u-he synth works, including its preset format, parameters, modules, and architecture. This is extremely useful for understanding synth-specific settings before analyzing or creating presets. Requires a synth to be selected first.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'generate_random_presets',
description: `Generate fully random presets based on parameter statistics from the loaded preset library. Optionally filter the statistical basis by author, category, pattern, or favorites to generate more focused/coherent sounds (e.g., random bass presets). Generated presets are automatically added to the library for immediate use with search_presets and explain_preset. Defaults to ${DEFAULT_PRESET_AMOUNT} presets if amount is omitted.`,
inputSchema: {
type: 'object',
properties: {
amount: {
type: 'number',
description: `Number of random presets to generate. Defaults to ${DEFAULT_PRESET_AMOUNT}.`,
default: DEFAULT_PRESET_AMOUNT,
},
pattern: {
type: 'string',
description: 'Optional glob pattern to filter which presets to use as statistical basis (e.g., "Bass/**/*", "**/*Pad*").',
},
author: {
type: 'string',
description: 'Filter statistical basis by author name (exact match). Use presets from specific author as inspiration.',
},
category: {
type: 'string',
description: 'Filter statistical basis by category prefix (e.g., "Bass", "Bass:Sub"). Generate random sounds in specific category style.',
},
favorites: {
type: 'string',
description: 'Filter statistical basis by favorites file name (e.g., "MyFavorites.uhe-fav"). Use only favorited presets as inspiration.',
},
stable: {
type: 'boolean',
description: 'Use stable randomization (randomizes per-section to maintain coherence). Defaults to true.',
},
dictionary: {
type: 'boolean',
description: 'Use dictionary of meaningful names from preset library. Defaults to true.',
},
},
required: [],
},
},
{
name: 'randomize_presets',
description: `Create variations of existing presets by randomly modifying their parameters (0-100% randomness). Select source presets by preset_names, pattern, author, or category. IMPORTANT: 'amount' is per source preset (1 preset + amount=4 → 4 files; 10 presets + amount=2 → 20 files). Generated variations are automatically added to the library. Defaults to ${DEFAULT_PRESET_AMOUNT} variations per preset and 50% randomness.`,
inputSchema: {
type: 'object',
properties: {
preset_names: {
type: 'array',
items: { type: 'string' },
description: 'Names of presets to randomize (without .h2p extension). Can specify multiple.',
},
pattern: {
type: 'string',
description: 'Path substring to filter presets (e.g., "Bass/**/*"). Alternative to preset_names.',
},
author: {
type: 'string',
description: 'Filter source presets by author name (exact match). Alternative to preset_names/pattern.',
},
category: {
type: 'string',
description: 'Filter source presets by category prefix (e.g., "Bass", "Bass:Sub"). Alternative to preset_names/pattern.',
},
amount: {
type: 'number',
description: `Number of variations to generate per base preset. Defaults to ${DEFAULT_PRESET_AMOUNT}.`,
default: DEFAULT_PRESET_AMOUNT,
},
randomness: {
type: 'number',
description: 'Percentage of randomness to apply (0-100). 0 = no change, 100 = completely random. Defaults to 50.',
},
stable: {
type: 'boolean',
description: 'Use stable randomization (randomizes per-section). Defaults to true.',
},
},
required: [],
},
},
{
name: 'merge_presets',
description: `Merge multiple presets to create hybrid sounds using weighted random ratios. Select presets via preset_names (supports wildcards: "*" for random from all, "?" for random from list), or filter by author/category/pattern. Generated merges are automatically added to the library. Defaults to ${DEFAULT_PRESET_AMOUNT} merged presets with 0% additional randomness.`,
inputSchema: {
type: 'object',
properties: {
preset_names: {
type: 'array',
items: { type: 'string' },
description: 'Names of presets to merge (without .h2p). Use "*" for random from all presets, "?" for random from specified list.',
},
pattern: {
type: 'string',
description: 'Path substring to select presets to merge from (e.g., "Bass/**/*"). Alternative to preset_names.',
},
author: {
type: 'string',
description: 'Filter source presets by author name. Random presets from this author will be merged. Alternative to preset_names/pattern.',
},
category: {
type: 'string',
description: 'Filter source presets by category prefix. Random presets from this category will be merged. Alternative to preset_names/pattern.',
},
amount: {
type: 'number',
description: `Number of merged presets to generate. Defaults to ${DEFAULT_PRESET_AMOUNT}.`,
default: DEFAULT_PRESET_AMOUNT,
},
randomness: {
type: 'number',
description: 'Additional randomness to apply after merging (0-100). Defaults to 0.',
},
stable: {
type: 'boolean',
description: 'Use stable randomization for any additional randomness. Defaults to true.',
},
},
required: [],
},
},
];
// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
const synthOptions = getSynthOptions();
const toolsWithHints = tools.map((tool) => {
if (tool.name === 'select_synth') {
const properties = {
...(tool.inputSchema?.properties ?? {}),
};
const synthProperty = {
...(properties.synth ?? {}),
};
return {
...tool,
description: `${tool.description} Known synth options include: ${synthOptions.join(', ')}.`,
inputSchema: {
...tool.inputSchema,
properties: {
...properties,
synth: {
...synthProperty,
enum: synthOptions,
},
},
},
};
}
return tool;
});
return { tools: toolsWithHints };
});
// Call tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'list_synths':
return handleListSynths();
case 'select_synth':
return handleSelectSynth(args);
case 'get_current_synth':
return handleGetCurrentSynth();
case 'list_presets':
return handleListPresets(args);
case 'search_presets':
return handleSearchPresets(args);
case 'filter_presets':
return handleFilterPresets(args);
case 'explain_preset':
return handleExplainPreset(args);
case 'get_categories':
return handleGetCategories();
case 'get_authors':
return handleGetAuthors();
case 'get_favorites_files':
return handleGetFavoritesFiles();
case 'get_synth_context':
return handleGetSynthContext();
case 'generate_random_presets':
return handleGenerateRandomPresets(args);
case 'randomize_presets':
return handleRandomizePresets(args);
case 'merge_presets':
return handleMergePresets(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: 'text',
text: `Error: ${errorMessage}`,
},
],
};
}
});
// Tool implementation functions
/**
* Load and add generated presets to the current preset library.
* This allows generated presets to be immediately searchable and explainable.
*/
function addGeneratedPresetsToLibrary(writtenFiles, userPresetsFolder) {
if (!state.presetLibrary) {
return 0;
}
let addedCount = 0;
const binary = state.config.binary ?? false;
for (const absolutePath of writtenFiles) {
try {
// Convert absolute path to relative path from userPresetsFolder
const normalizedAbsolute = normalize(absolutePath);
const normalizedUserFolder = normalize(userPresetsFolder);
const relativePath = relative(normalizedUserFolder, normalizedAbsolute);
// Create a virtual path in the format expected by the preset library
const virtualPath = `/User/${relativePath.split('\\').join('/')}`;
// Read and parse the preset file
const presetString = readFileSync(normalizedAbsolute, 'utf-8');
const parsedPreset = parseUhePreset(presetString, virtualPath, binary);
if (isValidPreset(parsedPreset)) {
state.presetLibrary.presets.push(parsedPreset);
addedCount++;
}
}
catch (error) {
console.error(chalk.yellow(`Warning: Could not load generated preset: ${absolutePath}`), error);
}
}
return addedCount;
}
function handleListSynths() {
ensureSynthDetection();
if (state.availableSynths.length === 0) {
return {
content: [
{
type: 'text',
text: `No u-he synths detected on this system. Please ensure u-he synths are installed. Known u-he products include: ${Array.from(uheSynthNames).join(', ')}.`,
},
],
};
}
const synthsList = state.availableSynths
.map((synth) => {
const current = synth.synthName === state.currentSynth ? ' (current)' : '';
return `- ${synth.synthName}${current}\n Location: ${synth.root}`;
})
.join('\n\n');
return {
content: [
{
type: 'text',
text: `Found ${state.availableSynths.length} u-he synth(s):\n\n${synthsList}\n\nUse select_synth to choose one and load its presets, then call generate_random_presets to create new sounds (defaults to ${DEFAULT_PRESET_AMOUNT} presets when amount is omitted).`,
},
],
};
}
function handleSelectSynth(args) {
const { synth, pattern } = args;
ensureSynthDetection();
// Find the synth
const synthInfo = state.availableSynths.find((s) => s.synthName.toLowerCase() === synth.toLowerCase());
if (!synthInfo) {
const synthOptions = getSynthOptions();
return {
content: [
{
type: 'text',
text: `Synth "${synth}" not found. Available synths: ${synthOptions.join(', ')}`,
},
],
};
}
// Check if this synth is already loaded with the same pattern
if (state.currentSynth === synthInfo.synthName &&
state.presetLibrary !== null) {
// Normalize pattern comparison (undefined/null should match default '**/*')
const normalizePattern = (p) => p ?? '**/*';
const currentPattern = normalizePattern(state.config.pattern);
const requestedPattern = normalizePattern(pattern);
if (currentPattern === requestedPattern) {
const patternInfo = pattern && pattern !== '**/*' ? ` (filtered by: ${pattern})` : '';
return {
content: [
{
type: 'text',
text: `${synthInfo.synthName} is already loaded with ${state.presetLibrary.presets.length} presets${patternInfo}. No need to reload.`,
},
],
};
}
// Pattern changed - inform user why we're reloading
const oldPatternInfo = currentPattern !== '**/*' ? ` (was: ${currentPattern})` : '';
const newPatternInfo = requestedPattern !== '**/*' ? ` (now: ${requestedPattern})` : '';
console.error(chalk.yellow(`Pattern changed${oldPatternInfo}${newPatternInfo}. Reloading preset library...`));
}
// Update config - ALWAYS set pattern (even if undefined) to clear old value
state.config.synth = synthInfo.synthName;
state.config.pattern = pattern;
// Load preset library
try {
const startTime = Date.now();
state.presetLibrary = loadPresetLibrary(synthInfo.synthName, state.config);
state.currentSynth = synthInfo.synthName;
const loadTime = Date.now() - startTime;
// Check if context documentation is available
const contextAvailable = hasContextAvailable(synthInfo.synthName);
const contextNote = contextAvailable
? `\n\nDetailed technical documentation is available for ${synthInfo.synthName}. Use get_synth_context to learn about its architecture, parameters, and preset format.`
: '';
const patternInfo = pattern
? `\nPattern filter: ${pattern} (call select_synth again without pattern to load full library)`
: '';
return {
content: [
{
type: 'text',
text: `Successfully selected ${synthInfo.synthName} and loaded ${state.presetLibrary.presets.length} presets in ${loadTime}ms.${patternInfo}\n\nYou can now use other tools to browse, search, filter, and generate presets. Call generate_random_presets without arguments to create ${DEFAULT_PRESET_AMOUNT} fresh patches immediately.${contextNote}`,
},
],
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: 'text',
text: `Failed to load presets for ${synthInfo.synthName}: ${errorMessage}`,
},
],
};
}
}
function handleGetCurrentSynth() {
if (!state.currentSynth || !state.presetLibrary) {
return {
content: [
{
type: 'text',
text: 'No synth is currently selected. Use select_synth to choose a synth and load its presets.',
},
],
};
}
return {
content: [
{
type: 'text',
text: `Current synth: ${state.currentSynth}\nLoaded presets: ${state.presetLibrary.presets.length}\nRoot folder: ${state.presetLibrary.rootFolder}\nUser presets folder: ${state.presetLibrary.userPresetsFolder}`,
},
],
};
}
function requireSynth() {
if (!state.currentSynth || !state.presetLibrary) {
throw new Error('No synth is currently selected. Use select_synth first.');
}
return state.presetLibrary;
}
function requireSynthWithName() {
if (!state.currentSynth || !state.presetLibrary) {
throw new Error('No synth is currently selected. Use select_synth first.');
}
return {
library: state.presetLibrary,
synthName: state.currentSynth,
};
}
function handleListPresets(args) {
const library = requireSynth();
const limit = args.limit ?? 50;
const offset = args.offset ?? 0;
const presets = library.presets.slice(offset, offset + limit);
if (presets.length === 0) {
return {
content: [
{
type: 'text',
text: `No presets found in the specified range. Total presets: ${library.presets.length}`,
},
],
};
}
const presetsList = presets
.map((preset, index) => {
const categories = preset.categories.join(', ') || 'No category';
const author = preset.meta.find((m) => m.key === 'Author')?.value || 'Unknown';
return `${offset + index + 1}. ${preset.presetName}\n Categories: ${categories}\n Author: ${author}\n Path: ${preset.filePath}`;
})
.join('\n\n');
return {
content: [
{
type: 'text',
text: `Showing presets ${offset + 1}-${offset + presets.length} of ${library.presets.length}:\n\n${presetsList}`,
},
],
};
}
function handleSearchPresets(args) {
const library = requireSynth();
const query = args.query.toLowerCase();
const limit = args.limit ?? 20;
// Simple relevance scoring
const matches = library.presets
.map((preset) => {
let score = 0;
const presetNameLower = preset.presetName.toLowerCase();
const categoriesLower = preset.categories.join(' ').toLowerCase();
const author = preset.meta
.find((m) => m.key === 'Author')
?.value?.toString()
.toLowerCase() || '';
// Exact name match
if (presetNameLower === query)
score += 100;
// Name starts with query
else if (presetNameLower.startsWith(query))
score += 50;
// Name contains query
else if (presetNameLower.includes(query))
score += 25;
// Category match
if (categoriesLower.includes(query))
score += 15;
// Author match
if (author.includes(query))
score += 10;
return { preset, score };
})
.filter((match) => match.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, limit);
if (matches.length === 0) {
return {
content: [
{
type: 'text',
text: `No presets found matching "${args.query}". Try a different search term or use filter_presets for more specific filtering.`,
},
],
};
}
const matchesList = matches
.map((match, index) => {
const { preset, score } = match;
const categories = preset.categories.join(', ') || 'No category';
const author = preset.meta.find((m) => m.key === 'Author')?.value || 'Unknown';
return `${index + 1}. ${preset.presetName} (relevance: ${score})\n Categories: ${categories}\n Author: ${author}`;
})
.join('\n\n');
return {
content: [
{
type: 'text',
text: `Found ${matches.length} preset(s) matching "${args.query}":\n\n${matchesList}`,
},
],
};
}
function handleFilterPresets(args) {
const library = requireSynth();
let filteredPresets = [...library.presets];
const appliedFilters = [];
if (args.category) {
filteredPresets = narrowDownByCategory({ ...library, presets: filteredPresets }, args.category);
appliedFilters.push(`category: "${args.category}"`);
}
if (args.author) {
filteredPresets = narrowDownByAuthor({ ...library, presets: filteredPresets }, args.author);
appliedFilters.push(`author: "${args.author}"`);
}
if (args.favorites) {
filteredPresets = narrowDownByFavoritesFile({ ...library, presets: filteredPresets }, args.favorites);
appliedFilters.push(`favorites: "${args.favorites}"`);
}
if (args.pattern) {
const patternLower = args.pattern.toLowerCase();
filteredPresets = filteredPresets.filter((preset) => preset.filePath.toLowerCase().includes(patternLower));
appliedFilters.push(`pattern: "${args.pattern}"`);
}
if (filteredPresets.length === 0) {
return {
content: [
{
type: 'text',
text: `No presets found matching filters: ${appliedFilters.join(', ')}`,
},
],
};
}
const limit = 30;
const presetsList = filteredPresets.slice(0, limit).map((preset, index) => {
const categories = preset.categories.join(', ') || 'No category';
const author = preset.meta.find((m) => m.key === 'Author')?.value || 'Unknown';
return `${index + 1}. ${preset.presetName}\n Categories: ${categories}\n Author: ${author}`;
});
const moreText = filteredPresets.length > limit
? `\n\n... and ${filteredPresets.length - limit} more presets.`
: '';
return {
content: [
{
type: 'text',
text: `Found ${filteredPresets.length} preset(s) matching filters: ${appliedFilters.join(', ')}\n\n${presetsList.join('\n\n')}${moreText}`,
},
],
};
}
function handleExplainPreset(args) {
const library = requireSynth();
const preset = library.presets.find((p) => p.presetName.toLowerCase() === args.preset_name.toLowerCase());
if (!preset) {
return {
content: [
{
type: 'text',
text: `Preset "${args.preset_name}" not found. Use list_presets or search_presets to find available presets.`,
},
],
};
}
// Try to load context file for the synth
const contextContent = state.currentSynth
? loadSynthContext(state.currentSynth)
: null;
let explanation = `# ${preset.presetName}\n\n`;
explanation += `**Path:** ${preset.filePath}\n\n`;
// Metadata
explanation += '## Metadata\n\n';
for (const meta of preset.meta) {
const value = Array.isArray(meta.value)
? meta.value.join(', ')
: meta.value;
explanation += `- **${meta.key}:** ${value}\n`;
}
// Categories
if (preset.categories.length > 0) {
explanation += `\n**Categories:** ${preset.categories.join(', ')}\n`;
}
// Parameters
if (args.include_parameters) {
explanation += `\n## Parameters (${preset.params.length} total)\n\n`;
// Group by section
const paramsBySection = new Map();
for (const param of preset.params) {
if (!paramsBySection.has(param.section)) {
paramsBySection.set(param.section, []);
}
paramsBySection.get(param.section)?.push(param);
}
for (const [section, params] of paramsBySection) {
explanation += `### Section: ${section}\n\n`;
for (const param of params.slice(0, 20)) {
// Limit to first 20 per section
explanation += `- ${param.key} = ${param.value} (${param.type})\n`;
}
if (params.length > 20) {
explanation += ` ... and ${params.length - 20} more parameters\n`;
}
explanation += '\n';
}
}
else {
explanation += `\n**Total Parameters:** ${preset.params.length}\n`;
explanation +=
'\nUse include_parameters: true to see the full parameter list.\n';
}
// Build response with context if available
const content = [
{
type: 'text',
text: explanation,
},
];
if (contextContent) {
content.push({
type: 'text',
text: `\n---\n\n## Context: ${state.currentSynth} Preset Format Reference\n\n${contextContent}`,
});
}
return {
content,
};
}
function handleGetCategories() {
const library = requireSynth();
const categories = new Set();
for (const preset of library.presets) {
for (const category of preset.categories) {
categories.add(category);
}
}
const sortedCategories = Array.from(categories).sort();
if (sortedCategories.length === 0) {
return {
content: [
{
type: 'text',
text: 'No categories found in loaded presets.',
},
],
};
}
return {
content: [
{
type: 'text',
text: `Found ${sortedCategories.length} unique categories:\n\n${sortedCategories.join('\n')}`,
},
],
};
}
function handleGetAuthors() {
const library = requireSynth();
const authors = new Set();
for (const preset of library.presets) {
const author = preset.meta.find((m) => m.key === 'Author')?.value;
if (author) {
const authorStr = Array.isArray(author) ? author.join(', ') : author;
authors.add(authorStr);
}
}
const sortedAuthors = Array.from(authors).sort();
if (sortedAuthors.length === 0) {
return {
content: [
{
type: 'text',
text: 'No authors found in loaded presets.',
},
],
};
}
return {
content: [
{
type: 'text',
text: `Found ${sortedAuthors.length} unique author(s):\n\n${sortedAuthors.join('\n')}`,
},
],
};
}
function handleGetFavoritesFiles() {
const library = requireSynth();
if (library.favorites.length === 0) {
return {
content: [
{
type: 'text',
text: 'No favorites files (.uhe-fav) found for this synth.',
},
],
};
}
const favoritesList = library.favorites
.map((fav) => `- ${fav.fileName} (${fav.presets.length} presets)`)
.join('\n');
return {
content: [
{
type: 'text',
text: `Found ${library.favorites.length} favorites file(s):\n\n${favoritesList}`,
},
],
};
}
function handleGetSynthContext() {
if (!state.currentSynth) {
return {
content: [
{
type: 'text',
text: 'No synth is currently selected. Use select_synth to choose a synth first.',
},
],
};
}
const contextContent = loadSynthContext(state.currentSynth);
if (!contextContent) {
return {
content: [
{
type: 'text',
text: `No technical documentation is currently available for ${state.currentSynth}. Documentation is available for: Diva, Hive, Repro-1, and Repro-5.`,
},
],
};
}
return {
content: [
{
type: 'text',
text: `# ${state.currentSynth} Technical Documentation\n\n${contextContent}`,
},
],
};
}
function handleGenerateRandomPresets(args) {
const { library, synthName } = requireSynthWithName();
// Apply filters to create a filtered library for statistical basis
let filteredLibrary = library;
const appliedFilters = [];
if (args.pattern || args.author || args.category || args.favorites) {
let filteredPresets = [...library.presets];
if (args.category) {
filteredPresets = narrowDownByCategory({ ...library, presets: filteredPresets }, args.category);
appliedFilters.push(`category: "${args.category}"`);
}
if (args.author) {
filteredPresets = narrowDownByAuthor({ ...library, presets: filteredPresets }, args.author);
appliedFilters.push(`author: "${args.author}"`);
}
if (args.favorites) {
filteredPresets = narrowDownByFavoritesFile({ ...library, presets: filteredPresets }, args.favorites);
appliedFilters.push(`favorites: "${args.favorites}"`);
}
if (args.pattern) {
const patternLower = args.pattern.toLowerCase();
filteredPresets = filteredPresets.filter((preset) => preset.filePath.toLowerCase().includes(patternLower));
appliedFilters.push(`pattern: "${args.pattern}"`);
}
if (filteredPresets.length === 0) {
throw new Error(`No presets found matching filters: ${appliedFilters.join(', ')}`);
}
filteredLibrary = { ...library, presets: filteredPresets };
}
const config = {
...state.config,
synth: synthName,
amount: resolveAmount(args.amount),
stable: args.stable ?? true,
dictionary: args.dictionary ?? true,
};
try {
const result = generatePresets(config, filteredLibrary);
// Add generated presets to the library so they can be searched/explained immediately
const addedCount = addGeneratedPresetsToLibrary(result.writtenFiles, library.userPresetsFolder);
const filterInfo = appliedFilters.length > 0
? `\nStatistical basis: ${filteredLibrary.presets.length} presets matching ${appliedFilters.join(', ')}\n`
: '';
const libraryInfo = addedCount > 0
? `\n✓ Added ${addedCount} preset(s) to loaded library (now searchable via search_presets and explain_preset)\n`
: '';
return {
content: [
{
type: 'text',
text: `Successfully generated ${result.presetCount} random preset(s)!${filterInfo}${libraryInfo}\nSaved to: ${result.outputFolder}/RANDOM/Fully Random/\n\nGenerated files:\n${result.writtenFiles.map((f) => `- ${f}`).join('\n')}`,
},
],
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to generate random presets: ${errorMessage}`);
}
}
function handleRandomizePresets(args) {
const { library, synthName } = requireSynthWithName();
// Determine which preset(s) to randomize
let preset;
let sourceDescription;
if (args.preset_names && args.preset_names.length > 0) {
preset = args.preset_names;
sourceDescription = `${preset.length} specified preset(s)`;
}
else if (args.author) {
const filteredPresets = narrowDownByAuthor(library, args.author);
if (filteredPresets.length === 0) {
throw new Error(`No presets found by author: ${args.author}`);
}
preset = filteredPresets.map((p) => p.presetName);
sourceDescription = `${preset.length} preset(s) by ${args.author}`;
}
else if (args.category) {
const filteredPresets = narrowDownByCategory(library, args.category);
if (filteredPresets.length === 0) {
throw new Error(`No presets found in category: ${args.category}`);
}
preset = filteredPresets.map((p) => p.presetName);
sourceDescription = `${preset.length} preset(s) in category ${args.category}`;
}
else if (args.pattern) {
// Find presets matching the pattern
const pattern = args.pattern;
const matchingPresets = library.presets.filter((p) => p.filePath.toLowerCase().includes(pattern.toLowerCase()));
if (matchingPresets.length === 0) {
throw new Error(`No presets found matching pattern: ${pattern}`);
}
preset = matchingPresets.map((p) => p.presetName);
sourceDescription = `${preset.length} preset(s) matching pattern "${pattern}"`;
}
else {
throw new Error('Either preset_names, pattern, author, or category must be specified for randomization');
}
const amountPerPreset = resolveAmount(args.amount);
const sourceCount = Array.isArray(preset) ? preset.length : 1;
const expectedTotal = sourceCount * amountPerPreset;
const config = {
...state.config,
synth: synthName,
preset: preset,
amount: amountPerPreset,
randomness: args.randomness ?? 50,
stable: args.stable ?? true,
};
try {
const result = generatePresets(config, library);
// Add generated presets to the library so they can be searched/explained immediately
const addedCount = addGeneratedPresetsToLibrary(result.writtenFiles, library.userPresetsFolder);
const libraryInfo = addedCount > 0
? `\n✓ Added ${addedCount} preset(s) to loaded library (now searchable via search_presets and explain_preset)\n`
: '';
return {
content: [
{
type: 'text',
text: `Successfully generated ${result.presetCount} randomized preset variation(s)!
Source: ${sourceDescription}
Generated: ${amountPerPreset} variation(s) per source preset
Total files: ${result.presetCount} (expected: ${expectedTotal})
Randomness: ${config.randomness}%${libraryInfo}
Saved to: ${result.outputFolder}/RANDOM/Randomized Preset/
Generated files:
${result.writtenFiles.map((f) => `- ${f}`).join('\n')}`,
},
],
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to randomize presets: ${errorMessage}`);
}
}
function handleMergePresets(args) {
const { library, synthName } = requireSynthWithName();
// Determine which preset(s) to merge
let merge;
let sourceDescription;
let libraryToUse = library;
if (args.preset_names && args.preset_names.length > 0) {
merge = args.preset_names;
sourceDescription = `Specified presets with wildcards: ${merge.join(', ')}`;
}
else if (args.author) {
const filteredPresets = narrowDownByAuthor(library, args.author);
if (filteredPresets.length === 0) {
throw new Error(`No presets found by author: ${args.author}`);
}
// Use wildcards to randomly select from author's presets for each merge
merge = ['*', '*', '*', '*']; // Random selection from filtered set
sourceDescription = `Random presets from ${filteredPresets.length} preset(s) by ${args.author}`;
// Create a filtered library
libraryToUse = { ...library, presets: filteredPresets };
state.presetLibrary = libraryToUse;
}
else if (args.category) {
const filteredPresets = narrowDownByCategory(library, args.category);
if (filteredPresets.length === 0) {
throw new Error(`No presets found in category: ${args.category}`);
}
merge = ['*', '*', '*', '*'];
sourceDescription = `Random presets from ${filteredPresets.length} preset(s) in category ${args.category}`;
libraryToUse = { ...library, presets: filteredPresets };
state.presetLibrary = libraryToUse;
}
else if (args.pattern) {
// Use pattern as merge selector
merge = args.pattern;
sourceDescription = `Pattern: ${args.pattern}`;
}
else {
throw new Error('Either preset_names, pattern, author, or category must be specified for merging');
}
const config = {
...state.config,
synth: synthName,
merge: merge,
amount: resolveAmount(args.amount),
randomness: args.randomness ?? 0,
stable: args.stable ?? true,
};
try {
const result = generatePresets(config, libraryToUse);
// Restore original library if we filtered it
if (args.author || args.category) {
state.presetLibrary = library;
}
// Add generated presets to the library so they can be searched/explained immediately
const addedCount = addGeneratedPresetsToLibrary(result.writtenFiles, library.userPresetsFolder);
const libraryInfo = addedCount > 0
? `\n✓ Added ${addedCount} preset(s) to loaded library (now searchable via search_presets and explain_preset)\n`
: '';
return {
content: [
{
type: 'text',
text: `Successfully generated ${result.presetCount} merged preset(s)!
Source: ${sourceDescription}
Total files: ${result.presetCount}
Additional randomness: ${config.randomness}%${libraryInfo}
Saved to: ${result.outputFolder}/RANDOM/Merged Preset/
Generated files:
${result.writtenFiles.map((f) => `- ${f}`).join('\n')}`