UNPKG

reactbits-mcp-server

Version:

MCP Server for React Bits - Access 99+ React components with animations, backgrounds, and UI elements

218 lines (215 loc) 8.67 kB
import * as fs from 'fs/promises'; import * as path from 'path'; import { fileURLToPath } from 'url'; import { logError, logInfo, logDebug } from './logger.js'; import { circuitBreakers } from './circuit-breaker.js'; /** * Base path to the react-bits components */ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const REACT_BITS_BASE_PATH = path.resolve(__dirname, '..', 'src'); const CONTENT_PATH = path.join(REACT_BITS_BASE_PATH, 'content'); const DEMO_PATH = path.join(REACT_BITS_BASE_PATH, 'demo'); /** * Valid component categories */ const VALID_CATEGORIES = ['Animations', 'Backgrounds', 'Components', 'TextAnimations']; /** * Get all available components from the file system * @param category - Optional category filter * @returns Promise with array of component information */ export async function getAvailableComponents(category) { return circuitBreakers.filesystem.execute(async () => { const components = []; try { const categoriesToScan = category ? [category] : VALID_CATEGORIES; for (const cat of categoriesToScan) { const categoryPath = path.join(CONTENT_PATH, cat); try { const items = await fs.readdir(categoryPath, { withFileTypes: true }); for (const item of items) { if (item.isDirectory()) { const componentPath = path.join(categoryPath, item.name); const componentFiles = await fs.readdir(componentPath); // Look for .jsx or .tsx files const mainFile = componentFiles.find(file => file.endsWith('.jsx') || file.endsWith('.tsx')); if (mainFile) { components.push({ name: item.name, category: cat, path: path.join(componentPath, mainFile) }); } } } } catch (error) { logError(`Error reading category ${cat}`, error); } } logInfo(`Found ${components.length} components`); return components; } catch (error) { logError('Error getting available components', error); throw error; } }); } /** * Get component source code * @param componentName - Name of the component * @param category - Optional category to narrow search * @returns Promise with component source code */ export async function getComponentSource(componentName, category) { return circuitBreakers.filesystem.execute(async () => { try { const components = await getAvailableComponents(category); const component = components.find(c => c.name === componentName); if (!component) { throw new Error(`Component '${componentName}' not found`); } const source = await fs.readFile(component.path, 'utf-8'); logDebug(`Retrieved source for component: ${componentName}`); return source; } catch (error) { logError(`Error getting component source for ${componentName}`, error); throw error; } }); } /** * Get component demo code * @param componentName - Name of the component * @param category - Optional category to narrow search * @returns Promise with demo source code */ export async function getComponentDemo(componentName, category) { return circuitBreakers.filesystem.execute(async () => { try { const categoriesToSearch = category ? [category] : VALID_CATEGORIES; for (const cat of categoriesToSearch) { const demoPath = path.join(DEMO_PATH, cat, componentName); try { const demoFiles = await fs.readdir(demoPath); const demoFile = demoFiles.find(file => file.endsWith('.jsx') || file.endsWith('.tsx')); if (demoFile) { const demoSource = await fs.readFile(path.join(demoPath, demoFile), 'utf-8'); logDebug(`Retrieved demo for component: ${componentName}`); return demoSource; } } catch (error) { // Continue searching in other categories continue; } } // If no demo found, return a basic usage example const basicDemo = `import ${componentName} from './${componentName}'; function Demo() { return ( <div> <${componentName} /> </div> ); } export default Demo;`; logDebug(`Generated basic demo for component: ${componentName}`); return basicDemo; } catch (error) { logError(`Error getting component demo for ${componentName}`, error); throw error; } }); } /** * Get component metadata by analyzing the source code * @param componentName - Name of the component * @param category - Optional category to narrow search * @returns Promise with component metadata */ export async function getComponentMetadata(componentName, category) { return circuitBreakers.filesystem.execute(async () => { try { const source = await getComponentSource(componentName, category); // Extract metadata from source code const metadata = { name: componentName, category: category || 'Components' }; // Extract dependencies from imports const importMatches = source.match(/import\s+.*?from\s+['"]([^'"]+)['"]/g); if (importMatches) { metadata.dependencies = importMatches .map(imp => { const match = imp.match(/from\s+['"]([^'"]+)['"]/); ; return match ? match[1] : null; }) .filter(Boolean); } // Extract props from PropTypes or TypeScript interfaces const propsMatch = source.match(/interface\s+\w*Props\s*{([^}]+)}/s) || source.match(/PropTypes\s*=\s*{([^}]+)}/s); if (propsMatch) { metadata.props = {}; // Could be enhanced to parse actual prop definitions } // Extract description from comments const descriptionMatch = source.match(/\/\*\*\s*\n\s*\*\s*(.+?)\s*\n/s); if (descriptionMatch) { metadata.description = descriptionMatch[1].trim(); } logDebug(`Retrieved metadata for component: ${componentName}`); return metadata; } catch (error) { logError(`Error getting component metadata for ${componentName}`, error); throw error; } }); } /** * Search components by name or description * @param query - Search query * @param category - Optional category filter * @returns Promise with array of matching components */ export async function searchComponents(query, category) { return circuitBreakers.filesystem.execute(async () => { try { const allComponents = await getAvailableComponents(category); const searchTerm = query.toLowerCase(); const matchingComponents = allComponents.filter(component => { return component.name.toLowerCase().includes(searchTerm) || component.category.toLowerCase().includes(searchTerm); }); logDebug(`Found ${matchingComponents.length} components matching query: ${query}`); return matchingComponents; } catch (error) { logError(`Error searching components with query: ${query}`, error); throw error; } }); } /** * Check if a component exists * @param componentName - Name of the component * @param category - Optional category to narrow search * @returns Promise with boolean indicating if component exists */ export async function componentExists(componentName, category) { try { const components = await getAvailableComponents(category); return components.some(c => c.name === componentName); } catch (error) { logError(`Error checking if component exists: ${componentName}`, error); return false; } }