@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
JavaScript
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