Skip to main content
Glama
AnthropicToDollhouseConverter.ts19.7 kB
/** * AnthropicToDollhouseConverter - Converts multi-file Anthropic Skills to single-file DollhouseMCP skills * * This is the INVERSE of DollhouseToAnthropicConverter. * * Implements the reverse transformation: * Anthropic Skill (directory with separated components) → DollhouseMCP Skill (single .md file) * * Algorithm (inverse of decomposition): * 1. Read SKILL.md and extract minimal YAML frontmatter * 2. Enrich YAML with DollhouseMCP fields (version, created, modified, tags, etc.) * 3. Read all scripts/ files and embed as code blocks * 4. Read all reference/ files and embed as documentation sections * 5. Read all examples/ files and embed as example sections * 6. Read all themes/ files and embed as templates * 7. Combine all content into single .md file with rich frontmatter * 8. Return single-file content * * SECURITY MODEL: * - This is a FORMAT TRANSFORMER, not a security boundary * - Preserves content fidelity - no modification, sanitization, or validation during conversion * - YAML parsing uses CORE_SCHEMA to prevent deserialization attacks only * - Output validation happens when user loads skill via SkillManager.load() * - SkillManager.load() applies SecureYamlParser and full security validation * - Converted skills must pass DollhouseMCP security checks before activation */ import * as fs from 'node:fs'; import * as path from 'node:path'; import * as yaml from 'js-yaml'; import { SchemaMapper, type AnthropicSkillMetadata, type DollhouseMCPSkillMetadata } from './SchemaMapper.js'; import type { AnthropicSkillStructure } from './DollhouseToAnthropicConverter.js'; export interface AnthropicSkillDirectory { skillMD: { metadata: AnthropicSkillMetadata; content: string; }; scripts: Map<string, { content: string; language: string }>; reference: Map<string, string>; examples: Map<string, string>; themes: Map<string, string>; metadata?: Map<string, string>; license?: string; } export class AnthropicToDollhouseConverter { private readonly schemaMapper: SchemaMapper; constructor() { this.schemaMapper = new SchemaMapper(); } /** * Convert an Anthropic Skill directory to a single DollhouseMCP skill file * * INVERSE ALGORITHM: * 1. Read SKILL.md and extract minimal YAML * 2. Enrich YAML with DollhouseMCP fields * 3. Read all scripts/ files and embed as code blocks * 4. Read all reference/ files and embed as documentation sections * 5. Read all examples/ files and embed as example sections * 6. Read all themes/ files and embed as templates * 7. Combine all content into single .md file * 8. Return single-file content */ async convertSkill(skillDirPath: string, options?: { preserveSource?: boolean; customMetadata?: Partial<DollhouseMCPSkillMetadata>; }): Promise<string> { // Step 1: Read the Anthropic skill directory structure // NOTE: No Unicode normalization - preserves content fidelity // Output will be validated when loaded via SkillManager.load() const skillData = await this.readAnthropicStructure(skillDirPath); // Step 2: Check for preserved DollhouseMCP metadata, or enrich if not found let enrichedMetadata: DollhouseMCPSkillMetadata; if (skillData.metadata?.has('dollhouse.yaml')) { // Use preserved metadata for perfect roundtrip const preservedYAML = skillData.metadata.get('dollhouse.yaml')!; // FIX (DMCP-SEC-005): Use CORE_SCHEMA to prevent YAML deserialization attacks enrichedMetadata = yaml.load(preservedYAML, { schema: yaml.CORE_SCHEMA }) as DollhouseMCPSkillMetadata; // Apply any custom metadata overrides if (options?.customMetadata) { Object.assign(enrichedMetadata, options.customMetadata); } } else { // Fall back to enrichment if no preserved metadata enrichedMetadata = this.enrichMetadata(skillData.skillMD.metadata, options); } // Step 3-6: Combine all components const combinedContent = this.combineComponents(skillData); // Step 7: Create single .md file with rich frontmatter const dollhouseSkill = this.createDollhouseSkill(enrichedMetadata, combinedContent); return dollhouseSkill; } /** * Convert in-memory Anthropic skill structure to DollhouseMCP format */ convertFromStructure(structure: AnthropicSkillStructure, options?: { preserveSource?: boolean; customMetadata?: Partial<DollhouseMCPSkillMetadata>; }): string { // Parse SKILL.md // NOTE: No Unicode normalization - preserves content fidelity const { metadata, content } = this.parseSkillMD(structure['SKILL.md']); // Convert structure to directory format const skillData = this.buildSkillDataFromStructure(structure, metadata, content); // Get enriched metadata (either preserved or generated) const enrichedMetadata = this.getEnrichedMetadata(skillData, metadata, options); // Combine components and create final skill const combinedContent = this.combineComponents(skillData); return this.createDollhouseSkill(enrichedMetadata, combinedContent); } /** * Build skill data structure from Anthropic structure * REFACTORED: Extracted to reduce cognitive complexity */ private buildSkillDataFromStructure( structure: AnthropicSkillStructure, metadata: AnthropicSkillMetadata, content: string ): AnthropicSkillDirectory { const skillData: AnthropicSkillDirectory = { skillMD: { metadata, content }, scripts: new Map(), reference: new Map(), examples: new Map(), themes: new Map() }; // Process all structure components this.processStructureScripts(structure, skillData); this.processStructureDirectory(structure['reference/'], skillData.reference); this.processStructureDirectory(structure['examples/'], skillData.examples); this.processStructureDirectory(structure['themes/'], skillData.themes); this.processStructureMetadata(structure, skillData); if (structure['LICENSE.txt']) { skillData.license = structure['LICENSE.txt']; } return skillData; } /** * Process scripts from structure * REFACTORED: Extracted to reduce cognitive complexity */ private processStructureScripts(structure: AnthropicSkillStructure, skillData: AnthropicSkillDirectory): void { if (!structure['scripts/']) return; for (const [filename, content] of Object.entries(structure['scripts/'])) { const language = this.inferLanguageFromFilename(filename); const cleanContent = this.removeShebangAndHeaders(content); skillData.scripts.set(filename, { content: cleanContent, language }); } } /** * Process generic directory structure (reference, examples, themes) * REFACTORED: Extracted to reduce cognitive complexity */ private processStructureDirectory( sourceDir: Record<string, string> | undefined, targetMap: Map<string, string> ): void { if (!sourceDir) return; for (const [filename, content] of Object.entries(sourceDir)) { targetMap.set(filename, content); } } /** * Process metadata from structure * REFACTORED: Extracted to reduce cognitive complexity */ private processStructureMetadata(structure: AnthropicSkillStructure, skillData: AnthropicSkillDirectory): void { if (!structure['metadata/']) return; skillData.metadata = new Map(); for (const [filename, content] of Object.entries(structure['metadata/'])) { skillData.metadata.set(filename, content); } } /** * Get enriched metadata (preserved or generated) * REFACTORED: Extracted to reduce cognitive complexity */ private getEnrichedMetadata( skillData: AnthropicSkillDirectory, metadata: AnthropicSkillMetadata, options?: { preserveSource?: boolean; customMetadata?: Partial<DollhouseMCPSkillMetadata>; } ): DollhouseMCPSkillMetadata { // Check for preserved DollhouseMCP metadata if (skillData.metadata?.has('dollhouse.yaml')) { return this.loadPreservedMetadata(skillData.metadata.get('dollhouse.yaml')!, options); } // Fall back to enrichment if no preserved metadata return this.enrichMetadata(metadata, options); } /** * Load preserved metadata from YAML * REFACTORED: Extracted to reduce cognitive complexity */ private loadPreservedMetadata( preservedYAML: string, options?: { customMetadata?: Partial<DollhouseMCPSkillMetadata>; } ): DollhouseMCPSkillMetadata { // FIX (DMCP-SEC-005): Use CORE_SCHEMA to prevent YAML deserialization attacks const enrichedMetadata = yaml.load(preservedYAML, { schema: yaml.CORE_SCHEMA }) as DollhouseMCPSkillMetadata; // Apply any custom metadata overrides if (options?.customMetadata) { Object.assign(enrichedMetadata, options.customMetadata); } return enrichedMetadata; } /** * Read Anthropic skill directory structure from disk * REFACTORED: Simplified by extracting directory reading logic */ async readAnthropicStructure(skillDirPath: string): Promise<AnthropicSkillDirectory> { this.validateSkillDirectory(skillDirPath); // Read and parse SKILL.md const { metadata, content } = this.readSkillMD(skillDirPath); // Initialize skill data structure const skillData: AnthropicSkillDirectory = { skillMD: { metadata, content }, scripts: new Map(), reference: new Map(), examples: new Map(), themes: new Map() }; // Read all directory components this.readScriptsDirectory(skillDirPath, skillData); this.readFilesDirectory(skillDirPath, 'reference', skillData.reference); this.readFilesDirectory(skillDirPath, 'examples', skillData.examples); this.readFilesDirectory(skillDirPath, 'themes', skillData.themes); this.readMetadataDirectory(skillDirPath, skillData); this.readLicenseFile(skillDirPath, skillData); return skillData; } /** * Validate skill directory exists and has SKILL.md * REFACTORED: Extracted to reduce cognitive complexity */ private validateSkillDirectory(skillDirPath: string): void { if (!fs.existsSync(skillDirPath)) { throw new Error(`Skill directory not found: ${skillDirPath}`); } const skillMDPath = path.join(skillDirPath, 'SKILL.md'); if (!fs.existsSync(skillMDPath)) { throw new Error(`SKILL.md not found in ${skillDirPath}`); } } /** * Read and parse SKILL.md file * REFACTORED: Extracted to reduce cognitive complexity */ private readSkillMD(skillDirPath: string): { metadata: AnthropicSkillMetadata; content: string } { const skillMDPath = path.join(skillDirPath, 'SKILL.md'); const skillMDContent = fs.readFileSync(skillMDPath, 'utf-8'); return this.parseSkillMD(skillMDContent); } /** * Read scripts directory and process script files * REFACTORED: Extracted to reduce cognitive complexity */ private readScriptsDirectory(skillDirPath: string, skillData: AnthropicSkillDirectory): void { const scriptsDir = path.join(skillDirPath, 'scripts'); if (!fs.existsSync(scriptsDir)) return; const scriptFiles = fs.readdirSync(scriptsDir); for (const filename of scriptFiles) { const filePath = path.join(scriptsDir, filename); const content = fs.readFileSync(filePath, 'utf-8'); const language = this.inferLanguageFromFilename(filename); const cleanContent = this.removeShebangAndHeaders(content); skillData.scripts.set(filename, { content: cleanContent, language }); } } /** * Read generic files directory (reference, examples, themes) * REFACTORED: Extracted to reduce cognitive complexity and reuse code */ private readFilesDirectory( skillDirPath: string, dirName: string, targetMap: Map<string, string> ): void { const dirPath = path.join(skillDirPath, dirName); if (!fs.existsSync(dirPath)) return; const files = fs.readdirSync(dirPath); for (const filename of files) { const filePath = path.join(dirPath, filename); const content = fs.readFileSync(filePath, 'utf-8'); targetMap.set(filename, content); } } /** * Read metadata directory * REFACTORED: Extracted to reduce cognitive complexity */ private readMetadataDirectory(skillDirPath: string, skillData: AnthropicSkillDirectory): void { const metadataDir = path.join(skillDirPath, 'metadata'); if (!fs.existsSync(metadataDir)) return; skillData.metadata = new Map(); const metadataFiles = fs.readdirSync(metadataDir); for (const filename of metadataFiles) { const filePath = path.join(metadataDir, filename); const content = fs.readFileSync(filePath, 'utf-8'); skillData.metadata.set(filename, content); } } /** * Read LICENSE.txt file if it exists * REFACTORED: Extracted to reduce cognitive complexity */ private readLicenseFile(skillDirPath: string, skillData: AnthropicSkillDirectory): void { const licensePath = path.join(skillDirPath, 'LICENSE.txt'); if (fs.existsSync(licensePath)) { skillData.license = fs.readFileSync(licensePath, 'utf-8'); } } /** * Parse SKILL.md and extract metadata and content */ private parseSkillMD(skillMDContent: string): { metadata: AnthropicSkillMetadata; content: string } { const yamlMatch = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(skillMDContent); if (!yamlMatch) { throw new Error('No YAML frontmatter found in SKILL.md'); } // FIX (DMCP-SEC-005): Use CORE_SCHEMA to prevent YAML deserialization attacks const metadata = yaml.load(yamlMatch[1], { schema: yaml.CORE_SCHEMA }) as AnthropicSkillMetadata; const content = yamlMatch[2].trim(); return { metadata, content }; } /** * Enrich minimal Anthropic metadata with DollhouseMCP fields */ private enrichMetadata( anthropicMeta: AnthropicSkillMetadata, options?: { preserveSource?: boolean; customMetadata?: Partial<DollhouseMCPSkillMetadata>; } ): DollhouseMCPSkillMetadata { // Infer tags and category from name/description const inferredTags = this.schemaMapper.inferTags(anthropicMeta.name, anthropicMeta.description); const inferredCategory = this.schemaMapper.inferCategory(anthropicMeta.name, anthropicMeta.description); // Use SchemaMapper to convert const enriched = this.schemaMapper.anthropicToDollhouse(anthropicMeta, { inferredTags, inferredCategory, inferredType: 'skill' }); // Apply custom metadata overrides if (options?.customMetadata) { Object.assign(enriched, options.customMetadata); } return enriched; } /** * Combine all Anthropic skill components into single markdown content */ private combineComponents(skillData: AnthropicSkillDirectory): string { const sections: string[] = []; // Start with main content from SKILL.md sections.push(skillData.skillMD.content); // Add embedded scripts as code blocks if (skillData.scripts.size > 0) { sections.push('\n## Scripts\n'); for (const [filename, { content, language }] of skillData.scripts) { const title = this.filenameToTitle(filename); sections.push(`### ${title}\n`, '```' + language, content, '```\n'); } } // Add reference documentation sections if (skillData.reference.size > 0) { for (const [, content] of skillData.reference) { sections.push('\n' + content); } } // Add examples if (skillData.examples.size > 0) { sections.push('\n## Examples\n'); for (const [, content] of skillData.examples) { sections.push(content, '\n'); } } // Add themes/templates if (skillData.themes.size > 0) { sections.push('\n## Templates\n'); for (const [, content] of skillData.themes) { sections.push(content, '\n'); } } return sections.join('\n').trim(); } /** * Create final DollhouseMCP skill with rich frontmatter */ private createDollhouseSkill(metadata: DollhouseMCPSkillMetadata, content: string): string { const yamlString = yaml.dump(metadata); return `---\n${yamlString}---\n\n${content}\n`; } /** * Infer programming language from filename */ private inferLanguageFromFilename(filename: string): string { const ext = path.extname(filename).toLowerCase(); const languageMap: Record<string, string> = { '.sh': 'bash', '.bash': 'bash', '.py': 'python', '.js': 'javascript', '.ts': 'typescript', '.rb': 'ruby', '.pl': 'perl', '.php': 'php' }; return languageMap[ext] || 'text'; } /** * Remove shebang and auto-generated headers from extracted scripts */ private removeShebangAndHeaders(content: string): string { const lines = content.split('\n'); let startIndex = 0; // Skip shebang if (lines[0]?.startsWith('#!')) { startIndex = 1; } // Skip auto-generated header comments if (lines[startIndex]?.startsWith('# Extracted script')) { startIndex++; } // Skip empty lines after headers while (startIndex < lines.length && lines[startIndex]?.trim() === '') { startIndex++; } return lines.slice(startIndex).join('\n').trim(); } /** * Convert filename to readable title */ private filenameToTitle(filename: string): string { // Remove extension const nameWithoutExt = path.basename(filename, path.extname(filename)); // Convert hyphens/underscores to spaces and capitalize return nameWithoutExt .replaceAll(/[-_]/g, ' ') .split(' ') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } /** * Write DollhouseMCP skill to disk */ async writeToFile(skillContent: string, outputPath: string): Promise<void> { // Ensure output directory exists const outputDir = path.dirname(outputPath); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } // Write skill file fs.writeFileSync(outputPath, skillContent, 'utf-8'); } }

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

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