@bottobot/td-mcp
Version:
TouchDesigner Documentation MCP Server v2.3 - Pure MCP server for VS Code/Codium with comprehensive operator documentation for 629 operators + 7 tutorials across all categories (TOP, CHOP, SOP, DAT, MAT, COMP, POP). Features experimental POP operators wit
385 lines (330 loc) • 13.2 kB
JavaScript
/**
* Parameter - Data model for TouchDesigner operator parameters
* Represents individual parameters with their properties, constraints, and metadata
*/
export class Parameter {
/**
* Create a new Parameter instance
* @param {Object} data - Initial parameter data
*/
constructor(data = {}) {
// Core identification
this.id = data.id || null;
this.name = data.name || '';
this.label = data.label || data.name || '';
this.group = data.group || '';
this.page = data.page || '';
// Parameter type and properties
this.type = data.type || ''; // Float, Int, String, Toggle, Menu, etc.
this.dataType = data.dataType || 'number'; // number, string, boolean, array
this.style = data.style || ''; // UI style (slider, field, menu, etc.)
// Value constraints
this.defaultValue = data.defaultValue !== undefined ? data.defaultValue : null;
this.minValue = data.minValue !== undefined ? data.minValue : null;
this.maxValue = data.maxValue !== undefined ? data.maxValue : null;
this.step = data.step !== undefined ? data.step : null;
// Menu/choice parameters
this.menuItems = data.menuItems || [];
this.menuLabels = data.menuLabels || [];
this.allowCustom = data.allowCustom || false;
// String parameters
this.maxLength = data.maxLength || null;
this.pattern = data.pattern || null; // Regex pattern
// Multi-value parameters
this.isArray = data.isArray || false;
this.arraySize = data.arraySize || 1;
this.dimensions = data.dimensions || 1; // 1D, 2D, 3D, 4D for vectors
// Documentation
this.description = data.description || '';
this.tooltip = data.tooltip || '';
this.help = data.help || '';
this.units = data.units || '';
this.examples = data.examples || [];
// Behavior flags
this.isReadOnly = data.isReadOnly || false;
this.isAdvanced = data.isAdvanced || false;
this.isHidden = data.isHidden || false;
this.isAnimatable = data.isAnimatable !== undefined ? data.isAnimatable : true;
this.isExpression = data.isExpression || false;
this.isPython = data.isPython || false;
// Relationships
this.dependsOn = data.dependsOn || []; // Other parameters this depends on
this.affects = data.affects || []; // Parameters this affects
this.linkedTo = data.linkedTo || []; // Parameters linked to this one
// Expression and scripting
this.expressionLanguage = data.expressionLanguage || ''; // python, tscript, etc.
this.defaultExpression = data.defaultExpression || '';
this.commonExpressions = data.commonExpressions || [];
// UI and display
this.order = data.order || 0;
this.isVisible = data.isVisible !== undefined ? data.isVisible : true;
this.conditionalDisplay = data.conditionalDisplay || null;
// Validation and processing
this.isValid = data.isValid !== undefined ? data.isValid : true;
this.validationErrors = data.validationErrors || [];
this.lastUpdated = data.lastUpdated || new Date().toISOString();
// Raw extraction data
this.rawData = data.rawData || {};
this.sourceElement = data.sourceElement || null;
}
/**
* Validate the parameter data
* @returns {Object} Validation result with isValid boolean and errors array
*/
validate() {
const errors = [];
// Required fields
if (!this.name) errors.push('Parameter name is required');
if (!this.type) errors.push('Parameter type is required');
// Type validation
const validTypes = [
'Float', 'Int', 'String', 'Toggle', 'Menu', 'Pulse', 'Momentary',
'File', 'Folder', 'Color', 'UV', 'XY', 'XYZ', 'XYZW', 'WH',
'OP', 'CHOP', 'TOP', 'SOP', 'DAT', 'MAT', 'COMP', 'POP'
];
if (this.type && !validTypes.includes(this.type)) {
errors.push(`Invalid parameter type: ${this.type}`);
}
// Value range validation
if (this.minValue !== null && this.maxValue !== null && this.minValue > this.maxValue) {
errors.push('Minimum value cannot be greater than maximum value');
}
// Default value validation
if (this.defaultValue !== null && this.minValue !== null && this.defaultValue < this.minValue) {
errors.push('Default value is below minimum value');
}
if (this.defaultValue !== null && this.maxValue !== null && this.defaultValue > this.maxValue) {
errors.push('Default value is above maximum value');
}
// Menu validation
if (this.type === 'Menu' && this.menuItems.length === 0) {
errors.push('Menu parameters must have menu items');
}
// Array validation
if (this.isArray && this.arraySize < 1) {
errors.push('Array size must be at least 1');
}
this.validationErrors = errors;
this.isValid = errors.length === 0;
return {
isValid: this.isValid,
errors: this.validationErrors
};
}
/**
* Get parameter summary for display
* @returns {Object} Summary data
*/
getSummary() {
return {
id: this.id,
name: this.name,
label: this.label,
type: this.type,
group: this.group,
defaultValue: this.defaultValue,
description: this.description,
isAdvanced: this.isAdvanced,
isAnimatable: this.isAnimatable
};
}
/**
* Check if parameter matches search criteria
* @param {string} query - Search query
* @returns {Object} Match result with score and details
*/
matches(query) {
const queryLower = query.toLowerCase();
let score = 0;
const matches = [];
// Name matching (highest priority)
if (this.name.toLowerCase().includes(queryLower)) {
score += 10;
matches.push({ field: 'name', content: this.name });
}
// Label matching
if (this.label.toLowerCase().includes(queryLower)) {
score += 8;
matches.push({ field: 'label', content: this.label });
}
// Description matching
if (this.description.toLowerCase().includes(queryLower)) {
score += 5;
matches.push({ field: 'description', content: this.description });
}
// Group matching
if (this.group.toLowerCase().includes(queryLower)) {
score += 3;
matches.push({ field: 'group', content: this.group });
}
// Type matching
if (this.type.toLowerCase().includes(queryLower)) {
score += 2;
matches.push({ field: 'type', content: this.type });
}
return {
score,
matches,
parameter: this.getSummary()
};
}
/**
* Get formatted value based on parameter type
* @param {*} value - Raw value to format
* @returns {string} Formatted value string
*/
formatValue(value) {
if (value === null || value === undefined) return '';
switch (this.type) {
case 'Float':
return parseFloat(value).toFixed(3);
case 'Int':
return parseInt(value).toString();
case 'Toggle':
return value ? 'On' : 'Off';
case 'Menu':
const index = parseInt(value);
return this.menuLabels[index] || this.menuItems[index] || value.toString();
case 'Color':
if (Array.isArray(value)) {
return `rgb(${value.map(v => Math.round(v * 255)).join(', ')})`;
}
return value.toString();
default:
return value.toString();
}
}
/**
* Generate parameter documentation
* @returns {string} Formatted documentation
*/
generateDocumentation() {
let doc = `**${this.label || this.name}** (${this.type})`;
if (this.description) {
doc += `\n${this.description}`;
}
if (this.defaultValue !== null) {
doc += `\nDefault: ${this.formatValue(this.defaultValue)}`;
}
if (this.minValue !== null || this.maxValue !== null) {
doc += `\nRange: ${this.minValue || '∞'} to ${this.maxValue || '∞'}`;
}
if (this.units) {
doc += `\nUnits: ${this.units}`;
}
if (this.menuItems.length > 0) {
doc += `\nOptions: ${this.menuItems.join(', ')}`;
}
if (this.examples.length > 0) {
doc += `\nExamples: ${this.examples.join(', ')}`;
}
return doc;
}
/**
* Convert to JSON-serializable object
* @param {boolean} includeRawData - Whether to include raw extraction data
* @returns {Object} Serializable object
*/
toJSON(includeRawData = false) {
const data = { ...this };
if (!includeRawData) {
delete data.rawData;
delete data.sourceElement;
}
return data;
}
/**
* Create Parameter from JSON data
* @param {Object} jsonData - JSON data to restore from
* @returns {Parameter} New Parameter instance
*/
static fromJSON(jsonData) {
return new Parameter(jsonData);
}
/**
* Generate a unique ID for the parameter
* @param {string} operatorName - Parent operator name
* @param {string} paramName - Parameter name
* @returns {string} Generated ID
*/
static generateId(operatorName, paramName) {
const cleanOp = operatorName.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
const cleanParam = paramName.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
return `${cleanOp}_${cleanParam}`;
}
/**
* Parse parameter type from TouchDesigner documentation
* @param {string} typeString - Raw type string from documentation
* @returns {Object} Parsed type information
*/
static parseType(typeString) {
const type = typeString.trim();
// Common type mappings
const typeMap = {
'float': 'Float',
'integer': 'Int',
'int': 'Int',
'string': 'String',
'toggle': 'Toggle',
'menu': 'Menu',
'pulse': 'Pulse',
'momentary': 'Momentary',
'file': 'File',
'folder': 'Folder',
'rgb': 'Color',
'rgba': 'Color',
'uv': 'UV',
'xy': 'XY',
'xyz': 'XYZ',
'xyzw': 'XYZW',
'wh': 'WH'
};
const normalizedType = type.toLowerCase();
const mappedType = typeMap[normalizedType] || type;
// Detect array types
const isArray = type.includes('[') || type.includes('vector') || type.includes('array');
let arraySize = 1;
if (isArray) {
const sizeMatch = type.match(/\[(\d+)\]/);
if (sizeMatch) {
arraySize = parseInt(sizeMatch[1]);
}
}
return {
type: mappedType,
isArray,
arraySize,
dataType: this.inferDataType(mappedType)
};
}
/**
* Infer JavaScript data type from TouchDesigner parameter type
* @param {string} tdType - TouchDesigner parameter type
* @returns {string} JavaScript data type
*/
static inferDataType(tdType) {
switch (tdType) {
case 'Float':
case 'Int':
return 'number';
case 'Toggle':
return 'boolean';
case 'String':
case 'File':
case 'Folder':
return 'string';
case 'Menu':
return 'number'; // Menu indices are numbers
case 'Color':
case 'UV':
case 'XY':
case 'XYZ':
case 'XYZW':
case 'WH':
return 'array';
default:
return 'string';
}
}
}
export default Parameter;