Skip to main content
Glama

DollhouseMCP

by DollhouseMCP
build-readme.js8.47 kB
#!/usr/bin/env node /** * Build README files from modular chunks * This script combines markdown chunks into complete README files * for different targets (NPM, GitHub, etc.) * * @fileoverview Modular README builder for DollhouseMCP * @author DollhouseMCP Team * @version 1.0.0 */ import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Paths - use absolute paths for safety const PROJECT_ROOT = path.resolve(__dirname, '..'); const CONFIG_PATH = path.join(PROJECT_ROOT, 'docs', 'readme', 'config.json'); const README_DIR = path.join(PROJECT_ROOT, 'docs', 'readme'); // Colors for console output const colors = { reset: '\x1b[0m', bright: '\x1b[1m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', red: '\x1b[31m' }; /** * Log a message with color formatting * @param {string} message - The message to log * @param {string} [color='reset'] - The color to use (from colors object) */ function log(message, color = 'reset') { console.log(`${colors[color]}${message}${colors.reset}`); } /** * Load and parse the configuration file * @returns {Promise<Object>} The parsed configuration object * @throws {Error} If config file cannot be loaded or parsed */ async function loadConfig() { try { // Check if config file exists await fs.access(CONFIG_PATH, fs.constants.F_OK); const configContent = await fs.readFile(CONFIG_PATH, 'utf-8'); const config = JSON.parse(configContent); // Validate config structure if (!config.versions || typeof config.versions !== 'object') { throw new Error('Invalid config: missing or invalid "versions" property'); } if (!config.chunkDirectory || typeof config.chunkDirectory !== 'string') { throw new Error('Invalid config: missing or invalid "chunkDirectory" property'); } return config; } catch (error) { if (error.code === 'ENOENT') { throw new Error(`Configuration file not found at: ${CONFIG_PATH}`); } throw new Error(`Failed to load config: ${error.message}`); } } /** * Load a chunk file and validate its content * @param {string} chunkName - The name of the chunk (without .md extension) * @param {string} chunkDirectory - The directory containing chunks * @returns {Promise<string|null>} The chunk content or null if not found */ async function loadChunk(chunkName, chunkDirectory) { // SECURITY FIX: Sanitize chunk name to prevent path traversal // Remove any path separators and parent directory references const sanitizedChunkName = chunkName .replaceAll(/[\/\\]/g, '_') // Replace path separators with underscore .replaceAll('..', '_') // Replace parent directory references .replace(/^\./, '_'); // Replace leading dots const chunkPath = path.join(README_DIR, chunkDirectory, `${sanitizedChunkName}.md`); // SECURITY FIX: Verify the resolved path is within the expected directory const resolvedPath = path.resolve(chunkPath); const expectedDir = path.resolve(path.join(README_DIR, chunkDirectory)); if (!resolvedPath.startsWith(expectedDir)) { throw new Error(`Security: Path traversal attempt detected for chunk: ${chunkName}`); } try { // Check if file exists first await fs.access(chunkPath, fs.constants.F_OK | fs.constants.R_OK); const content = await fs.readFile(chunkPath, 'utf-8'); // Validate chunk content if (content.length === 0) { log(` ⚠️ Chunk is empty: ${chunkName}.md`, 'yellow'); return null; } // Basic markdown validation - check for obvious issues if (content.includes('```') && (content.match(/```/g).length % 2 !== 0)) { log(` ⚠️ Unclosed code block in: ${chunkName}.md`, 'yellow'); } return content.trim(); } catch (error) { if (error.code === 'ENOENT') { log(` ⚠️ Chunk not found: ${chunkName}.md`, 'yellow'); } else if (error.code === 'EACCES') { log(` ❌ Permission denied reading: ${chunkName}.md`, 'red'); } else { log(` ❌ Error reading chunk ${chunkName}.md: ${error.message}`, 'red'); } return null; } } /** * Build a README for a specific target * @param {string} target - The target name (e.g., 'npm', 'github') * @param {Object} targetConfig - Configuration for this target * @param {Object} config - Global configuration * @returns {Promise<Object>} Build result with success status and statistics */ async function buildReadme(target, targetConfig, config) { log(`\nBuilding ${target} README...`, 'blue'); log(` Description: ${targetConfig.description}`); const chunks = []; const missingChunks = []; // Load all chunks for (const chunkName of targetConfig.chunks) { const content = await loadChunk(chunkName, config.chunkDirectory); if (content) { chunks.push(content); log(` ✓ Loaded: ${chunkName}.md`, 'green'); } else { missingChunks.push(chunkName); } } if (missingChunks.length > 0) { log(` Missing chunks: ${missingChunks.join(', ')}`, 'yellow'); } // Combine chunks with separator const separator = config.separator || '\n\n'; const readmeContent = chunks.join(separator); // Resolve output path (handle relative paths safely) const outputPath = path.resolve(README_DIR, targetConfig.output); // Ensure output directory exists const outputDir = path.dirname(outputPath); try { await fs.mkdir(outputDir, { recursive: true }); } catch (error) { throw new Error(`Failed to create output directory: ${error.message}`); } // Write output file with error handling try { await fs.writeFile(outputPath, readmeContent, 'utf-8'); } catch (error) { if (error.code === 'EACCES') { throw new Error(`Permission denied writing to: ${outputPath}`); } throw new Error(`Failed to write README: ${error.message}`); } // Get file size const stats = await fs.stat(outputPath); const sizeKB = (stats.size / 1024).toFixed(1); log(` ✓ Written to: ${targetConfig.output} (${sizeKB} KB)`, 'green'); log(` ✓ Combined ${chunks.length} chunks`, 'green'); return { success: true, chunks: chunks.length, size: sizeKB }; } /** * Main build process * Orchestrates the entire README building workflow * @returns {Promise<void>} */ async function main() { log('\n📚 DollhouseMCP README Builder', 'bright'); log('================================\n'); try { // Validate environment try { await fs.access(README_DIR, fs.constants.F_OK | fs.constants.R_OK); } catch (error) { throw new Error(`README directory not accessible: ${README_DIR}`); } // Load configuration log('Loading configuration...', 'blue'); const config = await loadConfig(); log('✓ Configuration loaded and validated', 'green'); // Parse command line arguments const args = process.argv.slice(2); const targetArg = args.find(arg => arg.startsWith('--target=')); const specificTarget = targetArg ? targetArg.split('=')[1] : null; // Determine which targets to build const targetsToBuild = specificTarget ? { [specificTarget]: config.versions[specificTarget] } : config.versions; if (specificTarget && !config.versions[specificTarget]) { throw new Error(`Unknown target: ${specificTarget}`); } // Build each target const results = {}; for (const [target, targetConfig] of Object.entries(targetsToBuild)) { results[target] = await buildReadme(target, targetConfig, config); } // Summary log('\n📊 Build Summary', 'bright'); log('================\n'); for (const [target, result] of Object.entries(results)) { log(`${target}: ${result.chunks} chunks, ${result.size} KB`, 'green'); } log('\n✨ Build complete!', 'bright'); // Provide next steps log('\n📋 Next Steps:', 'blue'); log('1. Review generated README files in docs/readme/'); log('2. Copy README.npm.md to README.md for NPM publishing'); log('3. Use README.github.md for GitHub repository'); } catch (error) { log(`\n❌ Error: ${error.message}`, 'red'); process.exit(1); } } // Run the builder main().catch(error => { log(`\n❌ Unexpected error: ${error.message}`, 'red'); process.exit(1); });

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