UNPKG

@rpironato/mcp-reactbits-server

Version:

Enhanced MCP server for accessing ReactBits component library with GitHub API integration, advanced search, and comprehensive metadata - 107 components with real code access from DavidHDev/react-bits repository

508 lines (507 loc) 22.7 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { ALL_TOOLS } from './tools/index.js'; import { ALL_COMPONENTS, CATEGORIES, searchComponents, getComponentByName, getComponentsByCategory } from './data/components.js'; import { getComponentCode, generateCLICommands } from './utils/codeRetrieval.js'; import { ReactBitsService } from './services/reactbitsService.js'; /** * ReactBits MCP Server * * Provides Model Context Protocol (MCP) access to ReactBits component library. * This server allows LLMs and other AI systems to browse, search, and retrieve * React components from the ReactBits library. * * Features: * - List all 107 ReactBits components * - Search components by name or category * - Get component source code in multiple variants (JS, JS+Tailwind, TS, TS+Tailwind) * - List categories with component counts * - Generate installation commands for components */ class ReactBitsServer { server; reactBitsService; constructor() { this.reactBitsService = new ReactBitsService(); this.server = new Server({ name: 'reactbits-server', version: '1.0.0', }, { capabilities: { tools: {}, }, }); this.setupToolHandlers(); this.setupErrorHandling(); } /** * Setup MCP tool handlers * Tools will be implemented in subsequent tasks following TurnBold methodology */ setupToolHandlers() { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: ALL_TOOLS }; }); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; switch (name) { case 'list_components': return await this.handleListComponents(args); case 'get_component_code': return await this.handleGetComponentCode(args); case 'search_components': return await this.handleSearchComponents(args); case 'list_categories': return await this.handleListCategories(args); case 'install_component': return await this.handleInstallComponent(args); case 'get_reactbits_stats': return await this.handleGetReactBitsStats(args); case 'advanced_search': return await this.handleAdvancedSearch(args); case 'get_component_docs': return await this.handleGetComponentDocs(args); default: throw new Error(`Unknown tool: ${name}`); } }); } /** * Setup error handling for the MCP server */ setupErrorHandling() { this.server.onerror = (error) => { // eslint-disable-next-line no-console console.error('[MCP Error]', error); }; process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } /** * Handle list_components tool */ async handleListComponents(args) { const { category, limit = 107, offset = 0 } = args || {}; let components; if (category) { components = getComponentsByCategory(category, limit, offset); } else { components = ALL_COMPONENTS.slice(offset, offset + limit); } return { content: [ { type: "text", text: `Found ${components.length} ReactBits components${category ? ` in category '${category}'` : ''}:\n\n` + components.map(comp => `• **${comp.name}** (${comp.category})\n ${comp.description}\n Keywords: ${comp.keywords.join(', ')}\n Versions: ${comp.versions.join(', ')}\n Dependencies: ${comp.dependencies.join(', ')}`).join('\n\n') } ] }; } /** * Handle get_component_code tool (com integração GitHub) */ async handleGetComponentCode(args) { const { name, version } = args || {}; if (!name || !version) { throw new Error('Both name and version are required'); } try { const result = await getComponentCode(name, version); const cliCommands = generateCLICommands(name, version); let output = `# ${result.component.name} Component (${version})\n\n`; output += `**Category:** ${result.component.category}\n`; output += `**Description:** ${result.component.description}\n`; output += `**Keywords:** ${result.component.keywords.join(', ')}\n`; output += `**Dependencies:** ${result.component.dependencies.join(', ')}\n`; output += `**File Size:** ${result.metadata.fileSize} characters\n`; output += `**Source:** ${result.metadata.source}\n\n`; // CLI Installation Commands output += `## Installation via jsrepo CLI\n\n`; output += `\`\`\`bash\n${cliCommands.jsrepo}\n\`\`\`\n\n`; output += `Or manually:\n\`\`\`bash\n${cliCommands.manual}\n\`\`\`\n\n`; output += `**Note:** ${cliCommands.note}\n\n`; // JSX/TSX Code output += `## Component Code (${version.includes('ts') ? 'TypeScript' : 'JavaScript'})\n\n`; output += `\`\`\`${version.includes('ts') ? 'typescript' : 'javascript'}\n`; output += result.code.jsx; output += `\n\`\`\`\n\n`; // CSS Code (if exists) if (result.code.css && result.metadata.hasCSS) { output += `## CSS Styles\n\n`; output += `\`\`\`css\n`; output += result.code.css; output += `\n\`\`\`\n\n`; } // Metadata output += `## Component Metadata\n\n`; output += `**Imports:** ${result.metadata.imports.length > 0 ? result.metadata.imports.join(', ') : 'None'}\n`; output += `**Exports:** ${result.metadata.exports.join(', ')}\n`; output += `**Props:** ${result.metadata.props.length > 0 ? result.metadata.props.join(', ') : 'None detected'}\n`; output += `**Has CSS:** ${result.metadata.hasCSS ? 'Yes' : 'No'}\n`; output += `**Code Source:** ${result.metadata.source === 'github' ? 'GitHub Repository (DavidHDev/react-bits)' : result.metadata.source === 'local' ? 'Local Files' : 'Generated Template'}\n`; return { content: [ { type: "text", text: output } ] }; } catch (error) { // Se falhar completamente, retornar template const component = getComponentByName(name); if (!component) { throw error; } const templateCode = this.generateComponentTemplate(component, version); return { content: [ { type: "text", text: `# ${component.name} Component Template (${version})\n\n` + `**⚠️ Note:** This is a template version. Real component code could not be retrieved.\n` + `**Error:** ${error instanceof Error ? error.message : 'Unknown error'}\n\n` + `**Category:** ${component.category}\n` + `**Description:** ${component.description}\n` + `**Keywords:** ${component.keywords.join(', ')}\n` + `**Dependencies:** ${component.dependencies.join(', ')}\n\n` + `## Template Code\n\n` + `\`\`\`${version.includes('ts') ? 'typescript' : 'javascript'}\n` + templateCode + `\n\`\`\`\n\n` + `## Installation\n\n` + `\`\`\`bash\nnpm install @rpironato/react-components\n\`\`\`\n\n` + `Then import:\n\`\`\`${version.includes('ts') ? 'typescript' : 'javascript'}\n` + `import { ${component.name} } from '@rpironato/react-components';\n\`\`\`` } ] }; } } /** * Gerar template básico do componente (fallback) */ generateComponentTemplate(component, version) { const isTS = version.includes('ts'); const isTailwind = version.includes('tailwind'); const imports = [ "import React from 'react';", ...component.dependencies .filter(dep => dep !== 'react' && dep !== 'react-dom') .map(dep => `import ${dep.includes('/') ? `{ /* imports */ } from '${dep}'` : `'${dep}'`};`) ]; if (isTailwind && !component.dependencies.includes('tailwindcss')) { imports.push("// Tailwind CSS classes used in this component"); } const typeDefinition = isTS ? ` interface ${component.name}Props { className?: string; children?: React.ReactNode; // Add specific props based on component functionality }` : ''; const functionSignature = isTS ? `export default function ${component.name}({ className, children, ...props }: ${component.name}Props)` : `export default function ${component.name}({ className, children, ...props })`; const styles = isTailwind ? 'className={`${className || ""}`}' : 'className={`${component.name.toLowerCase()} ${className || ""}`}'; return `${imports.join('\n')}${typeDefinition} ${functionSignature} { return ( <div ${styles}> {/* ${component.description} */} {children || <span>{component.name} Component</span>} </div> ); }`; } /** * Handle search_components tool */ async handleSearchComponents(args) { const { search, category, limit = 10 } = args || {}; if (!search) { throw new Error('Search term is required'); } const results = searchComponents(search, category, limit); return { content: [ { type: "text", text: `Search results for "${search}"${category ? ` in ${category}` : ''} (${results.length} found):\n\n` + results.map(comp => `• **${comp.name}** (${comp.category})\n ${comp.description}\n Keywords: ${comp.keywords.join(', ')}\n URL: ${comp.urlSlug}`).join('\n\n') + (results.length === 0 ? `No components found matching "${search}". Try searching for terms like: button, menu, nav, card, animation, text, background, etc.` : '') } ] }; } /** * Handle list_categories tool */ async handleListCategories(args) { const { includeComponents = false } = args || {}; let output = 'ReactBits Categories:\n\n'; for (const cat of CATEGORIES) { output += `• **${cat.name}** (${cat.count} components)\n ${cat.description}\n`; if (includeComponents) { const components = getComponentsByCategory(cat.name, 5) .map(comp => comp.name); output += ` Components: ${components.join(', ')}${components.length < cat.count ? '...' : ''}\n`; } output += '\n'; } return { content: [ { type: "text", text: output } ] }; } /** * Handle install_component tool */ async handleInstallComponent(args) { const { name, version, package_manager = 'npm' } = args || {}; if (!name || !version) { throw new Error('Both name and version are required'); } // Verificar se o componente existe const component = getComponentByName(name); if (!component) { throw new Error(`Component "${name}" not found. Use search_components to find available components.`); } const commands = { npm: `npm install @rpironato/react-components`, yarn: `yarn add @rpironato/react-components`, pnpm: `pnpm add @rpironato/react-components`, bun: `bun add @rpironato/react-components` }; const command = commands[package_manager] || commands.npm; // Gerar comandos de instalação de dependências const dependencyInstalls = component.dependencies .filter(dep => dep !== 'react' && dep !== 'react-dom') .map(dep => { const depCommands = { npm: `npm install ${dep}`, yarn: `yarn add ${dep}`, pnpm: `pnpm add ${dep}`, bun: `bun add ${dep}` }; return depCommands[package_manager] || depCommands.npm; }); return { content: [ { type: "text", text: `Installation guide for **${component.name}** (${version}):\n\n` + `**1. Install ReactBits package:**\n\`\`\`bash\n${command}\n\`\`\`\n\n` + (dependencyInstalls.length > 0 ? `**2. Install dependencies:**\n\`\`\`bash\n${dependencyInstalls.join('\n')}\n\`\`\`\n\n` : '') + `**3. Import and use:**\n\`\`\`${version.includes('ts') ? 'typescript' : 'javascript'}\n` + `import { ${component.name} } from '@rpironato/react-components';\n\n` + `function App() {\n return <${component.name} />;\n}\n\`\`\`\n\n` + `**Component info:**\n` + `- Category: ${component.category}\n` + `- Description: ${component.description}\n` + `- URL Slug: ${component.urlSlug}\n` + `- Keywords: ${component.keywords.join(', ')}` } ] }; } /** * Handle get_reactbits_stats tool */ async handleGetReactBitsStats(args) { const stats = this.reactBitsService.getReactBitsStats(); if (args?.includeReport) { const report = this.reactBitsService.generateStatsReport(); return { content: [ { type: "text", text: JSON.stringify({ ...stats, report }, null, 2) } ] }; } return { content: [ { type: "text", text: JSON.stringify(stats, null, 2) } ] }; } /** * Handle advanced_search tool (corrigido) */ async handleAdvancedSearch(args) { const { query, category, dependencies, keywords, isNew, isUpdated, sortBy = 'relevance', limit = 10 } = args || {}; // Validar parâmetros if (!query && !category && !dependencies && !keywords && isNew === undefined && isUpdated === undefined) { // Se nenhum filtro for fornecido, retornar primeiros componentes const defaultResults = ALL_COMPONENTS.slice(0, limit); return { content: [ { type: "text", text: `Advanced search results (${defaultResults.length} found - showing default results):\n\n` + defaultResults.map(comp => `• **${comp.name}** (${comp.category})\n ${comp.description}\n Keywords: ${comp.keywords.join(', ')}\n Dependencies: ${comp.dependencies.join(', ')}\n URL: ${comp.urlSlug}`).join('\n\n') } ] }; } const results = this.reactBitsService.advancedSearch({ query, category, dependencies, keywords, isNew, isUpdated, sortBy, limit }); if (results.length === 0) { // Sugerir buscas alternativas se não encontrar resultados const suggestions = []; if (query) { const partialResults = this.reactBitsService.advancedSearch({ query: query.substring(0, Math.ceil(query.length / 2)), limit: 3 }); if (partialResults.length > 0) { suggestions.push(`Try partial search: "${query.substring(0, Math.ceil(query.length / 2))}"`); } } if (category) { const categoryResults = this.reactBitsService.advancedSearch({ category, limit: 3 }); suggestions.push(`${categoryResults.length} components available in ${category} category`); } return { content: [ { type: "text", text: `Advanced search found 0 results for your criteria.\n\n` + `**Search criteria:**\n` + (query ? `- Query: "${query}"\n` : '') + (category ? `- Category: ${category}\n` : '') + (dependencies ? `- Dependencies: ${dependencies.join(', ')}\n` : '') + (keywords ? `- Keywords: ${keywords.join(', ')}\n` : '') + (isNew !== undefined ? `- New components only: ${isNew}\n` : '') + (isUpdated !== undefined ? `- Updated components only: ${isUpdated}\n` : '') + `\n**Suggestions:**\n` + (suggestions.length > 0 ? suggestions.map(s => `- ${s}`).join('\n') : '- Try broader search terms\n- Remove some filters\n- Use regular search_components tool') + `\n\n**Available categories:** ${CATEGORIES.map(c => c.name).join(', ')}` } ] }; } return { content: [ { type: "text", text: `Advanced search results (${results.length} found):\n\n` + results.map(comp => `• **${comp.name}** (${comp.category})\n ${comp.description}\n Keywords: ${comp.keywords.join(', ')}\n Dependencies: ${comp.dependencies.join(', ')}\n URL: ${comp.urlSlug}${comp.isNew ? ' 🆕' : ''}${comp.isUpdated ? ' 🔄' : ''}`).join('\n\n') + (results.length === limit ? `\n\n*Limited to ${limit} results. Use limit parameter to see more.*` : '') } ] }; } /** * Handle get_component_docs tool */ async handleGetComponentDocs(args) { const { name, includeRelated = true } = args || {}; if (!name) { throw new Error('Component name is required'); } try { const docs = this.reactBitsService.getComponentDocumentation(name); if (includeRelated) { const component = this.reactBitsService.findComponent(name); if (component) { const related = this.reactBitsService.advancedSearch({ category: component.category, limit: 5 }).filter(c => c.name !== component.name); return { content: [ { type: "text", text: `# ${docs.component.name} Documentation\n\n` + `## Overview\n${docs.overview}\n\n` + `## Installation\n${docs.installation.setupSteps.join('\n')}\n\n` + `## Basic Usage\n\`\`\`javascript\n${docs.usage.basicExample}\n\`\`\`\n\n` + `## Advanced Usage\n\`\`\`javascript\n${docs.usage.advancedExample}\n\`\`\`\n\n` + `## Props\n${docs.usage.props.map(prop => `- **${prop.name}** (${prop.type}${prop.required ? ', required' : ''}): ${prop.description}`).join('\n')}\n\n` + `## Styling\n- CSS Classes: ${docs.styling.cssClasses.join(', ')}\n- ${docs.styling.customization}\n\n` + `## Accessibility\n${docs.accessibility.features.map(f => `- ${f}`).join('\n')}\n\n` + `## Related Components\n${related.map(c => `- ${c.name} (${c.category})`).join('\n')}` } ] }; } } return { content: [ { type: "text", text: JSON.stringify(docs, null, 2) } ] }; } catch (error) { throw new Error(`Failed to get documentation for component "${name}": ${error}`); } } /** * Start the ReactBits MCP server */ async start() { const transport = new StdioServerTransport(); await this.server.connect(transport); // eslint-disable-next-line no-console console.error('ReactBits MCP Server started successfully'); // eslint-disable-next-line no-console console.error('Ready to serve ReactBits component library via MCP'); } } /** * Initialize and start the ReactBits MCP Server */ async function main() { try { const server = new ReactBitsServer(); await server.start(); } catch (error) { // eslint-disable-next-line no-console console.error('Failed to start ReactBits MCP Server:', error); process.exit(1); } } // Start the server // eslint-disable-next-line no-console main().catch(console.error); export { ReactBitsServer }; //# sourceMappingURL=index.js.map