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
JavaScript
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;
}
}