Skip to main content
Glama

DollhouseMCP

by DollhouseMCP
Template.tsโ€ข33.3 kB
/** * Template element class implementing IElement interface. * Represents reusable content structures with variable substitution and dynamic content. * * SECURITY FIXES IMPLEMENTED (Following PR #319 patterns): * 1. CRITICAL: Template injection prevention - no eval() or Function() constructor * 2. CRITICAL: Path traversal prevention for template includes * 3. HIGH: Input validation and sanitization for all template variables * 4. MEDIUM: Memory limits to prevent resource exhaustion (100KB templates, 100 variables) * 5. MEDIUM: Audit logging for all security operations via SecurityMonitor * 6. MEDIUM: Unicode normalization to prevent homograph attacks */ import { BaseElement } from '../BaseElement.js'; import { IElement, IElementMetadata, ElementValidationResult } from '../../types/elements/index.js'; import { ElementType } from '../../portfolio/types.js'; import { logger } from '../../utils/logger.js'; import { ErrorHandler, ErrorCategory } from '../../utils/ErrorHandler.js'; import { ValidationErrorCodes } from '../../utils/errorCodes.js'; import { sanitizeInput } from '../../security/InputValidator.js'; import { UnicodeValidator } from '../../security/validators/unicodeValidator.js'; import { SecurityMonitor } from '../../security/securityMonitor.js'; import * as path from 'path'; // Extend IElementMetadata with template-specific fields export interface TemplateMetadata extends IElementMetadata { category?: string; // Template category (documents, emails, code, etc.) output_format?: string; // Output format (markdown, html, json, yaml, etc.) variables?: TemplateVariable[]; // Variable definitions includes?: string[]; // Other templates to include tags?: string[]; // Searchable tags usage_count?: number; // Track popularity last_used?: string; // ISO date string examples?: TemplateExample[]; // Usage examples /** * Action verbs that trigger this template (e.g., "create", "generate", "draft") * Used by Enhanced Capability Index for intelligent template suggestions * @since v1.9.10 */ triggers?: string[]; } export interface TemplateVariable { name: string; // Variable name (e.g., "project_name") type: 'string' | 'number' | 'boolean' | 'date' | 'array' | 'object'; description?: string; // Help text for the variable required?: boolean; // Is this variable required? default?: unknown; // Default value if not provided (type-safe) validation?: string; // Regex pattern for validation (string type only) options?: string[]; // For enum-like strings format?: string; // Date format string (date type only) } export interface TemplateExample { title: string; description?: string; variables: Record<string, unknown>; output?: string; } export class Template extends BaseElement implements IElement { public declare metadata: TemplateMetadata; public content: string; private compiledTemplate?: CompiledTemplate; // SECURITY FIX #4: Memory management constants // Prevents unbounded template size and variable count that could exhaust memory private readonly MAX_TEMPLATE_SIZE = 100 * 1024; // 100KB max template size private readonly MAX_VARIABLE_COUNT = 100; // Max variables per template private readonly MAX_INCLUDE_DEPTH = 5; // Prevent infinite include loops private readonly MAX_STRING_LENGTH = 10000; // Max length for string variables constructor(metadata: Partial<TemplateMetadata>, content: string = '') { // SECURITY FIX #3 & #6: Validate and sanitize ALL metadata fields // Unicode normalization prevents homograph attacks // Input sanitization prevents XSS and injection attacks const sanitizedMetadata = { ...metadata, name: metadata.name ? sanitizeInput(UnicodeValidator.normalize(metadata.name).normalizedContent, 100) : undefined, description: metadata.description ? sanitizeInput(UnicodeValidator.normalize(metadata.description).normalizedContent, 500) : undefined, category: metadata.category ? sanitizeInput(UnicodeValidator.normalize(metadata.category).normalizedContent, 50) : undefined, output_format: metadata.output_format ? sanitizeInput(metadata.output_format, 20) : undefined }; super(ElementType.TEMPLATE, sanitizedMetadata); // SECURITY FIX #4: Enforce template size limit if (content.length > this.MAX_TEMPLATE_SIZE) { SecurityMonitor.logSecurityEvent({ type: 'CONTENT_SIZE_EXCEEDED', severity: 'HIGH', source: 'Template.constructor', details: `Template size ${content.length} exceeds maximum ${this.MAX_TEMPLATE_SIZE}` }); throw ErrorHandler.createError(`Template content exceeds maximum size of ${this.MAX_TEMPLATE_SIZE} bytes`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.TEMPLATE_TOO_LARGE); } // SECURITY FIX #3: Sanitize template content // Note: We preserve the template syntax but normalize Unicode this.content = UnicodeValidator.normalize(content).normalizedContent; // Ensure template-specific metadata this.metadata = { ...this.metadata, category: sanitizedMetadata.category || 'general', output_format: sanitizedMetadata.output_format || 'markdown', variables: metadata.variables || [], includes: metadata.includes || [], tags: metadata.tags || [], usage_count: metadata.usage_count || 0, examples: metadata.examples || [] }; // SECURITY FIX #3 & #4: Validate variables if (this.metadata.variables) { if (this.metadata.variables.length > this.MAX_VARIABLE_COUNT) { throw ErrorHandler.createError(`Variable count ${this.metadata.variables.length} exceeds maximum ${this.MAX_VARIABLE_COUNT}`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.TOO_MANY_VARIABLES); } this.metadata.variables = this.metadata.variables.map(variable => ({ ...variable, name: sanitizeInput(UnicodeValidator.normalize(variable.name).normalizedContent, 50), description: variable.description ? sanitizeInput(UnicodeValidator.normalize(variable.description).normalizedContent, 200) : undefined, validation: variable.validation ? sanitizeInput(variable.validation, 200) : undefined })); } // SECURITY FIX #2: Validate include paths if (this.metadata.includes) { this.metadata.includes = this.metadata.includes.map(includePath => { const sanitizedPath = sanitizeInput(includePath, 200); // Prevent path traversal attacks if (!this.isValidIncludePath(sanitizedPath)) { SecurityMonitor.logSecurityEvent({ type: 'PATH_TRAVERSAL_ATTEMPT', severity: 'CRITICAL', source: 'Template.constructor', details: `Invalid include path: ${sanitizedPath}` }); throw ErrorHandler.createError(`Invalid include path: ${sanitizedPath}`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.INVALID_INCLUDE_PATH); } return sanitizedPath; }); } } /** * Validate include paths to prevent directory traversal * SECURITY FIX #2: Prevents accessing files outside template directory */ private isValidIncludePath(includePath: string): boolean { // Normalize the path const normalized = path.normalize(includePath); // Check for path traversal patterns if (normalized.includes('..') || normalized.includes('~') || path.isAbsolute(normalized)) { return false; } // Only allow alphanumeric, dash, underscore, forward slash, backslash (for Windows), and .md extension // Note: We test against the original path to preserve cross-platform compatibility // FIX: Remove unnecessary escape for / (SonarCloud S6535) const validPathPattern = /^[a-zA-Z0-9\-_/\\]+\.md$/; return validPathPattern.test(includePath); } /** * Compile the template for efficient rendering * SECURITY FIX #1: Safe template compilation without eval() or Function() * Uses regex-based token replacement instead of dynamic code execution */ private compile(): CompiledTemplate { if (this.compiledTemplate) { return this.compiledTemplate; } // Extract all variable tokens from the template // FIX: Use \w shorthand instead of [a-zA-Z0-9_] (SonarCloud S6353) const variablePattern = /\{\{\s*([a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*)\s*\}\}/g; const tokens: TemplateToken[] = []; let match; while ((match = variablePattern.exec(this.content)) !== null) { tokens.push({ token: match[0], variable: match[1], position: match.index }); } this.compiledTemplate = { content: this.content, tokens, variables: this.metadata.variables || [] }; return this.compiledTemplate; } /** * Render the template with provided variables * SECURITY FIX #1: Safe rendering without code execution * SECURITY FIX #3: All variables are validated and sanitized * TYPE SAFETY: Strong typing for variables with runtime validation */ async render<T extends Record<string, unknown>>( variables: T = {} as T, includeDepth: number = 0 ): Promise<string> { // SECURITY FIX #4: Prevent infinite include loops if (includeDepth > this.MAX_INCLUDE_DEPTH) { SecurityMonitor.logSecurityEvent({ type: 'INCLUDE_DEPTH_EXCEEDED', severity: 'HIGH', source: 'Template.render', details: `Include depth ${includeDepth} exceeds maximum ${this.MAX_INCLUDE_DEPTH}` }); throw ErrorHandler.createError('Maximum template include depth exceeded', ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.MAX_INCLUDE_DEPTH); } // Compile the template const compiled = this.compile(); // Validate and sanitize all provided variables const sanitizedVariables = await this.validateAndSanitizeVariables(variables); // Start with the template content let rendered = compiled.content; // Replace tokens in reverse order to maintain positions const sortedTokens = [...compiled.tokens].sort((a, b) => b.position - a.position); for (const token of sortedTokens) { const value = this.resolveVariable(token.variable, sanitizedVariables); const stringValue = this.formatValue(value); // Replace the token with the sanitized value rendered = rendered.substring(0, token.position) + stringValue + rendered.substring(token.position + token.token.length); } // Process includes if any if (this.metadata.includes && this.metadata.includes.length > 0) { rendered = await this.processIncludes(rendered, sanitizedVariables, includeDepth); } // Update usage statistics // NOTE: These updates are not atomic and may have race conditions under concurrent access // This is acceptable for usage statistics which don't require perfect accuracy // For production systems requiring atomic counters, consider using a database or atomic operations this.metadata.usage_count = (this.metadata.usage_count || 0) + 1; this.metadata.last_used = new Date().toISOString(); // SECURITY FIX #5: Log template usage for audit trail SecurityMonitor.logSecurityEvent({ type: 'TEMPLATE_RENDERED', severity: 'LOW', source: 'Template.render', details: `Template ${this.metadata.name} rendered with ${Object.keys(sanitizedVariables).length} variables` }); return rendered; } /** * Validate and sanitize variables according to their definitions * SECURITY FIX #3: Comprehensive validation of all input variables * TYPE SAFETY: Improved type safety for variable validation */ private async validateAndSanitizeVariables( variables: Record<string, unknown> ): Promise<Record<string, unknown>> { const sanitized: Record<string, unknown> = {}; // Check required variables for (const varDef of this.metadata.variables || []) { if (varDef.required && !(varDef.name in variables)) { if (varDef.default !== undefined) { sanitized[varDef.name] = varDef.default; } else { throw ErrorHandler.createError(`Required variable '${varDef.name}' not provided`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.REQUIRED_VARIABLE); } } } // Validate and sanitize provided variables for (const [name, value] of Object.entries(variables)) { const varDef = this.metadata.variables?.find(v => v.name === name); if (!varDef) { // Skip unknown variables (they won't be used anyway) logger.warn(`Unknown variable '${name}' provided to template`); continue; } // Type validation and sanitization const sanitizedValue = await this.sanitizeVariableValue(value, varDef); sanitized[name] = sanitizedValue; } // Apply defaults for missing optional variables for (const varDef of this.metadata.variables || []) { if (!varDef.required && !(varDef.name in sanitized) && varDef.default !== undefined) { sanitized[varDef.name] = varDef.default; } } return sanitized; } /** * Sanitize a single variable value according to its definition * SECURITY FIX #3 & #6: Type-specific validation and Unicode normalization */ private async sanitizeVariableValue(value: any, varDef: TemplateVariable): Promise<any> { switch (varDef.type) { case 'string': // SECURITY FIX #6: Unicode normalization const normalized = UnicodeValidator.normalize(String(value)); let stringValue = sanitizeInput(normalized.normalizedContent, this.MAX_STRING_LENGTH); // Apply regex validation if specified if (varDef.validation) { // SECURITY FIX: Validate regex complexity to prevent ReDoS attacks // Previously: User-provided regex executed without limits // Now: Check for dangerous patterns and limit execution time try { // Check for dangerous regex patterns if (this.isDangerousRegex(varDef.validation)) { throw ErrorHandler.createError(`Variable '${varDef.name}' has potentially dangerous validation pattern`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.DANGEROUS_PATTERN); } const regex = new RegExp(varDef.validation); // Use a simple timeout mechanism - in production, consider using a worker thread const startTime = Date.now(); const result = regex.test(stringValue); const duration = Date.now() - startTime; // If regex takes too long, it might be malicious if (duration > 100) { // 100ms threshold SecurityMonitor.logSecurityEvent({ type: 'CONTENT_INJECTION_ATTEMPT', severity: 'HIGH', source: 'Template.sanitizeVariableValue', details: `Regex validation took ${duration}ms for variable '${varDef.name}', possible ReDoS` }); throw ErrorHandler.createError(`Variable '${varDef.name}' validation pattern is too complex`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.PATTERN_TOO_COMPLEX); } if (!result) { throw ErrorHandler.createError(`Variable '${varDef.name}' does not match validation pattern`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.PATTERN_MISMATCH); } } catch (e) { if (e instanceof SyntaxError) { throw ErrorHandler.createError(`Variable '${varDef.name}' has invalid validation pattern`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.INVALID_PATTERN); } throw e; } } // Check enum options if specified if (varDef.options && !varDef.options.includes(stringValue)) { throw ErrorHandler.createError(`Variable '${varDef.name}' must be one of: ${varDef.options.join(', ')}`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.INVALID_OPTIONS); } return stringValue; case 'number': const num = Number(value); if (Number.isNaN(num)) { throw ErrorHandler.createError(`Variable '${varDef.name}' must be a number`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.INVALID_NUMBER); } return num; case 'boolean': return Boolean(value); case 'date': const date = new Date(value); if (Number.isNaN(date.getTime())) { throw ErrorHandler.createError(`Variable '${varDef.name}' must be a valid date`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.INVALID_DATE); } return date; case 'array': if (!Array.isArray(value)) { throw ErrorHandler.createError(`Variable '${varDef.name}' must be an array`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.INVALID_ARRAY); } // SECURITY FIX: Limit array size to prevent memory exhaustion attacks // Previously: No limit on array size could lead to DoS // Now: Enforces reasonable size limit with logging const MAX_ARRAY_SIZE = 1000; if (value.length > MAX_ARRAY_SIZE) { SecurityMonitor.logSecurityEvent({ type: 'CONTENT_SIZE_EXCEEDED', severity: 'MEDIUM', source: 'Template.sanitizeVariableValue', details: `Array variable '${varDef.name}' has ${value.length} items, limiting to ${MAX_ARRAY_SIZE}` }); value = value.slice(0, MAX_ARRAY_SIZE); } // Sanitize string elements in arrays return value.map((item: any) => typeof item === 'string' ? sanitizeInput(item, 1000) : item ); case 'object': if (typeof value !== 'object' || value === null) { throw ErrorHandler.createError(`Variable '${varDef.name}' must be an object`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.INVALID_OBJECT); } // Deep sanitize string values in objects return this.sanitizeObject(value); default: return value; } } /** * Recursively sanitize string values in objects * SECURITY FIX: Truncate deep nesting instead of throwing to prevent DoS */ private sanitizeObject(obj: any, depth: number = 0): any { // SECURITY FIX: Return safe default instead of throwing to prevent DoS attacks // Previously: Threw error on deep nesting which could be exploited // Now: Returns string representation for excessively nested objects if (depth > 10) { SecurityMonitor.logSecurityEvent({ type: 'CONTENT_SIZE_EXCEEDED', severity: 'MEDIUM', source: 'Template.sanitizeObject', details: 'Object nesting depth exceeded, truncating to string representation' }); return '[Object too deeply nested]'; } if (typeof obj !== 'object' || obj === null) { return obj; } if (Array.isArray(obj)) { // SECURITY FIX: Limit array size to prevent memory exhaustion const MAX_ARRAY_SIZE = 1000; if (obj.length > MAX_ARRAY_SIZE) { SecurityMonitor.logSecurityEvent({ type: 'CONTENT_SIZE_EXCEEDED', severity: 'MEDIUM', source: 'Template.sanitizeObject', details: `Array size ${obj.length} exceeds maximum ${MAX_ARRAY_SIZE}, truncating` }); obj = obj.slice(0, MAX_ARRAY_SIZE); } return obj.map((item: any) => this.sanitizeObject(item, depth + 1)); } const sanitized: Record<string, any> = {}; // SECURITY FIX: Limit number of object properties to prevent memory exhaustion const MAX_OBJECT_KEYS = 100; const entries = Object.entries(obj); if (entries.length > MAX_OBJECT_KEYS) { SecurityMonitor.logSecurityEvent({ type: 'CONTENT_SIZE_EXCEEDED', severity: 'MEDIUM', source: 'Template.sanitizeObject', details: `Object has ${entries.length} keys, limiting to ${MAX_OBJECT_KEYS}` }); } for (let i = 0; i < Math.min(entries.length, MAX_OBJECT_KEYS); i++) { const [key, value] = entries[i]; const sanitizedKey = sanitizeInput(key, 50); if (typeof value === 'string') { sanitized[sanitizedKey] = sanitizeInput(value, 1000); } else if (typeof value === 'object') { sanitized[sanitizedKey] = this.sanitizeObject(value, depth + 1); } else { sanitized[sanitizedKey] = value; } } return sanitized; } /** * Resolve nested variable paths (e.g., "user.name") * TYPE SAFETY: Improved type safety for variable resolution */ private resolveVariable(path: string, variables: Record<string, unknown>): unknown { const parts = path.split('.'); let value: unknown = variables; for (const part of parts) { if (value && typeof value === 'object' && value !== null && part in value) { // Type assertion with runtime check - safe because we verified the property exists value = (value as Record<string, unknown>)[part]; } else { return undefined; } } return value; } /** * Check if a regex pattern is potentially dangerous (ReDoS) * SECURITY FIX: Detect patterns that could cause exponential backtracking */ private isDangerousRegex(pattern: string): boolean { // Check for nested quantifiers which can cause exponential backtracking // SECURITY FIX: Use safe, specific patterns to detect dangerous regex constructs // Previously: Used [^)]* patterns that could cause ReDoS in detection itself // Now: Use safer, bounded character classes and specific string checks // FIX: Use character class and remove unnecessary escape (SonarCloud S6035, S6535) const dangerousPatterns = [ /[+*]{2,}/, // Multiple consecutive quantifiers /\(.{0,50}\+\)[+*]/, // Quantified groups with quantifiers inside (bounded) /\[[^\]]{0,20}\+\][+*]/, // Quantified character classes with quantifiers (bounded) /(\\[dws])\1{2,}/, // Repeated character classes /\(\?<[!=][^)]{0,30}\)/, // Complex lookbehinds (bounded) ]; // String-based checks for common catastrophic patterns (safer than regex) const dangerousStringPatterns = [ '(.+)+', // (.+)+ catastrophic backtracking '(.*)++', // (.*)++ '(.*)*', // (.*)* catastrophic backtracking '(.+)*', // (.+)* '(a+)+', // (a+)+ type patterns '(a*)*', // (a*)* type patterns '(a|a)*', // Overlapping alternation '(a|b)*+', // Possessive quantifiers with alternation ]; // Check regex-based dangerous patterns for (const dangerous of dangerousPatterns) { if (dangerous.test(pattern)) { return true; } } // Check string-based dangerous patterns (safer than complex regex) for (const dangerousString of dangerousStringPatterns) { if (pattern.includes(dangerousString)) { return true; } } // Check for excessive backtracking potential // Count groups and quantifiers (using safe, simple regex) const groups = (pattern.match(/\(/g) || []).length; const quantifiers = (pattern.match(/[+*?{]/g) || []).length; // If there are many groups and quantifiers, it's potentially dangerous if (groups > 5 && quantifiers > 5) { return true; } return false; } /** * Format a value for template output */ private formatValue(value: any): string { if (value === undefined || value === null) { return ''; } if (value instanceof Date) { return value.toISOString(); } if (Array.isArray(value)) { return value.join(', '); } if (typeof value === 'object') { return JSON.stringify(value, null, 2); } return String(value); } /** * Process template includes * SECURITY FIX #2: Safe include processing with path validation * * TODO: Implement actual include processing functionality * This is currently a placeholder that validates the security model * but does not actually load and render included templates. * * Future implementation should: * 1. Load templates from validated paths * 2. Render them with current variables * 3. Replace include markers in content * 4. Respect includeDepth limit */ private async processIncludes( content: string, variables: Record<string, unknown>, includeDepth: number ): Promise<string> { // TODO: Implement actual template include processing // Current implementation only validates the security model if (!this.metadata.includes || this.metadata.includes.length === 0) { return content; } // Log security event for audit trail SecurityMonitor.logSecurityEvent({ type: 'TEMPLATE_INCLUDE', severity: 'LOW', source: 'Template.processIncludes', details: `Processing ${this.metadata.includes.length} includes at depth ${includeDepth}` }); // TODO: Future implementation would: // for (const includePath of this.metadata.includes) { // const template = await this.loadIncludedTemplate(includePath); // const rendered = await template.render(variables, includeDepth + 1); // content = content.replace(`{{include:${includePath}}}`, rendered); // } return content; } /** * Template-specific validation */ public override validate(): ElementValidationResult { const result = super.validate(); // Initialize arrays if not present // FIX: Use nullish coalescing assignment (SonarCloud S6606) result.errors ??= []; result.warnings ??= []; // Content validation if (!this.content || this.content.trim().length === 0) { result.errors.push({ field: 'content', message: 'Template content cannot be empty', code: 'EMPTY_CONTENT' }); } // Check for unmatched tokens const openTokens = (this.content.match(/\{\{/g) || []).length; const closeTokens = (this.content.match(/\}\}/g) || []).length; if (openTokens !== closeTokens) { result.errors.push({ field: 'content', message: 'Template has unmatched variable tokens', code: 'UNMATCHED_TOKENS' }); } // Validate output format const validFormats = ['markdown', 'html', 'json', 'yaml', 'text', 'xml']; if (this.metadata.output_format && !validFormats.includes(this.metadata.output_format)) { result.warnings.push({ field: 'output_format', message: `Unknown output format '${this.metadata.output_format}'. Common formats: ${validFormats.join(', ')}`, severity: 'low' }); } // Validate variables if (this.metadata.variables) { const variableNames = new Set<string>(); this.metadata.variables.forEach((variable, index) => { // Check for duplicate names if (variableNames.has(variable.name)) { result.errors!.push({ field: `variables[${index}].name`, message: `Duplicate variable name '${variable.name}'`, code: 'DUPLICATE_VARIABLE' }); } variableNames.add(variable.name); // Validate regex patterns if (variable.validation) { try { new RegExp(variable.validation); } catch (e) { result.errors!.push({ field: `variables[${index}].validation`, message: `Invalid regex pattern: ${e}`, code: 'INVALID_REGEX' }); } } }); } // Check if all tokens have corresponding variable definitions const compiled = this.compile(); const definedVars = new Set(this.metadata.variables?.map(v => v.name) || []); const usedVars = new Set(compiled.tokens.map(t => t.variable.split('.')[0])); usedVars.forEach(varName => { if (!definedVars.has(varName)) { result.warnings!.push({ field: 'variables', message: `Template uses undefined variable '${varName}'`, severity: 'medium' }); } }); // Warnings for best practices // FIX: Remove unnecessary non-null assertion (SonarCloud S4325) if (!this.metadata.tags || this.metadata.tags.length === 0) { result.warnings.push({ field: 'tags', message: 'Consider adding tags for better searchability', severity: 'low' }); } if (!this.metadata.examples || this.metadata.examples.length === 0) { result.warnings.push({ field: 'examples', message: 'Adding examples improves template usability', severity: 'medium' }); } // Update the valid flag based on final errors result.valid = (result.errors?.length || 0) === 0; return result; } /** * Serialize to JSON format for internal use and testing */ public override serializeToJSON(): string { const data = { id: this.id, type: this.type, version: this.version, metadata: this.metadata, content: this.content, references: this.references, extensions: this.extensions, ratings: this.ratings }; return JSON.stringify(data, null, 2); } /** * Get content for serialization */ protected override getContent(): string { return this.content; } /** * Serialize template to markdown format with YAML frontmatter * FIX: Changed from JSON to markdown for GitHub portfolio compatibility */ public override serialize(): string { // Template content is already the main content // Just use base class serialize which outputs markdown return super.serialize(); } /** * Deserialize template from JSON format */ public override deserialize(data: string): void { try { const parsed = JSON.parse(data); // Update metadata this.metadata = { ...this.metadata, ...parsed.metadata }; // Update content this.content = parsed.content || ''; // Update other properties this.references = parsed.references || []; this.extensions = parsed.extensions || {}; this.ratings = parsed.ratings || this.ratings; // Update ID and version if provided if (parsed.id) this.id = parsed.id; if (parsed.version) this.version = parsed.version; // Clear compiled template cache this.compiledTemplate = undefined; this._isDirty = true; logger.debug(`Deserialized template: ${this.metadata.name}`); } catch (error) { logger.error(`Failed to deserialize template: ${error}`); throw ErrorHandler.wrapError(error, 'Template deserialization failed', ErrorCategory.SYSTEM_ERROR); } } /** * Get a preview of the template with sample data */ async preview(): Promise<string> { const sampleVars: Record<string, any> = {}; // Generate sample data for each variable for (const varDef of this.metadata.variables || []) { switch (varDef.type) { case 'string': sampleVars[varDef.name] = varDef.default || `[${varDef.name}]`; break; case 'number': sampleVars[varDef.name] = varDef.default || 42; break; case 'boolean': sampleVars[varDef.name] = varDef.default || true; break; case 'date': sampleVars[varDef.name] = varDef.default || new Date(); break; case 'array': sampleVars[varDef.name] = varDef.default || ['item1', 'item2']; break; case 'object': sampleVars[varDef.name] = varDef.default || { key: 'value' }; break; } } return this.render(sampleVars); } /** * Template activation lifecycle */ public override async activate(): Promise<void> { logger.info(`Activating template: ${this.metadata.name} (${this.id})`); // Compile the template to check for errors this.compile(); await super.activate?.(); } /** * Template deactivation lifecycle */ public override async deactivate(): Promise<void> { logger.info(`Deactivating template: ${this.metadata.name} (${this.id})`); // Clear compiled template cache this.compiledTemplate = undefined; await super.deactivate?.(); } } // Internal types for template compilation interface CompiledTemplate { content: string; tokens: TemplateToken[]; variables: TemplateVariable[]; } interface TemplateToken { token: string; // The full token including braces (e.g., "{{ name }}") variable: string; // The variable path (e.g., "user.name") position: number; // Position in the template string }

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/DollhouseMCP/DollhouseMCP'

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