Skip to main content
Glama

Component Library MCP Server

index.ts•11.3 kB
#!/usr/bin/env node import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import fs from 'fs/promises'; import path from 'path'; import { glob } from 'glob'; import matter from 'gray-matter'; import type { ComponentInfo, ComponentProps, ComponentExample, ServerConfig, ListComponentsArgs, GetComponentArgs, GetExampleArgs } from './types.js'; // Configuration - Update these paths to match your setup const CONFIG: ServerConfig = { componentsPath: process.env.COMPONENTS_PATH || './components', docsPath: process.env.DOCS_PATH || './docs', examplesPath: process.env.EXAMPLES_PATH || './docs/examples' }; class ComponentLibraryServer { private server: McpServer; private componentCache: Map<string, ComponentInfo> = new Map(); private initialized: boolean = false; constructor() { this.server = new McpServer({ name: 'component-library', version: '2.0.0', }); this.setupTools(); } private async initialize(): Promise<void> { if (this.initialized) return; try { await this.scanComponents(); this.initialized = true; console.error('Component library MCP server initialized'); } catch (error) { console.error('Failed to initialize:', error); } } private async scanComponents(): Promise<void> { try { // Look for component documentation files const docFiles = await glob(`${CONFIG.docsPath}/components/**/*.{md,mdx}`, { ignore: ['**/node_modules/**'] }); for (const file of docFiles) { const componentName = path.basename(file, path.extname(file)); const content = await fs.readFile(file, 'utf-8'); // Parse frontmatter and content const { data, content: markdown } = matter(content); this.componentCache.set(componentName, { name: componentName, path: file, ...data, documentation: markdown } as ComponentInfo); } console.error(`Loaded ${this.componentCache.size} components`); } catch (error) { console.error('Error scanning components:', error); } } private async getComponentInfo(componentName: string): Promise<ComponentInfo | null> { if (!this.initialized) await this.initialize(); // Check cache first if (this.componentCache.has(componentName)) { return this.componentCache.get(componentName)!; } // Try to find component dynamically try { const docPaths = [ `${CONFIG.docsPath}/components/${componentName}.md`, `${CONFIG.docsPath}/components/${componentName}.mdx`, `${CONFIG.docsPath}/${componentName}.md`, ]; for (const docPath of docPaths) { try { const content = await fs.readFile(docPath, 'utf-8'); const { data, content: markdown } = matter(content); const componentInfo: ComponentInfo = { name: componentName, path: docPath, ...data, documentation: markdown }; // Try to get props from TypeScript definitions const props = await this.extractProps(componentName); if (props) { componentInfo.props = props; } this.componentCache.set(componentName, componentInfo); return componentInfo; } catch (e) { // File doesn't exist, try next path } } return null; } catch (error) { console.error(`Error getting component ${componentName}:`, error); return null; } } private async extractProps(componentName: string): Promise<ComponentProps | null> { try { const componentPaths = [ `${CONFIG.componentsPath}/${componentName}/index.tsx`, `${CONFIG.componentsPath}/${componentName}/${componentName}.tsx`, `${CONFIG.componentsPath}/${componentName}.tsx`, ]; for (const componentPath of componentPaths) { try { const source = await fs.readFile(componentPath, 'utf-8'); // Improved regex to extract interface props const propsMatch = source.match(/(?:interface|type)\s+(\w*Props)\s*(?:=\s*)?{([^}]*)}/s); if (propsMatch) { const propsContent = propsMatch[2]; const props: ComponentProps = {}; // Extract prop definitions with better parsing const propLines = propsContent.split('\n').filter(line => line.includes(':')); for (const line of propLines) { const match = line.match(/^\s*(\w+)(\?)?:\s*(.+?)(?:;|$)/); if (match) { const propName = match[1]; const isOptional = !!match[2]; const propType = match[3].trim(); // Extract JSDoc comments if present const commentMatch = source.match(new RegExp(`\\/\\*\\*[^*]*\\*(?:[^/*][^*]*\\*+)*\\/\\s*${propName}`)); let description = ''; if (commentMatch) { description = commentMatch[0] .replace(/\/\*\*|\*\//g, '') .replace(/\n\s*\*/g, '\n') .trim(); } props[propName] = { type: propType, required: !isOptional, ...(description && { description }) }; } } return props; } } catch (e) { // File doesn't exist, try next path } } } catch (error) { console.error(`Error extracting props for ${componentName}:`, error); } return null; } private async getComponentExample(componentName: string): Promise<ComponentExample | null> { if (!this.initialized) await this.initialize(); try { const examplePaths = [ `${CONFIG.examplesPath}/${componentName}.tsx`, `${CONFIG.examplesPath}/${componentName}.jsx`, `${CONFIG.examplesPath}/${componentName}/index.tsx`, `${CONFIG.docsPath}/examples/${componentName}.tsx`, ]; for (const examplePath of examplePaths) { try { const example = await fs.readFile(examplePath, 'utf-8'); return { componentName, code: example, path: examplePath }; } catch (e) { // File doesn't exist, try next path } } // Try to extract examples from documentation const componentInfo = await this.getComponentInfo(componentName); if (componentInfo && componentInfo.documentation) { // Extract code blocks marked as examples const codeBlockRegex = /```(?:jsx?|tsx?)\n([\s\S]*?)```/g; const examples: string[] = []; let match; while ((match = codeBlockRegex.exec(componentInfo.documentation)) !== null) { examples.push(match[1]); } if (examples.length > 0) { return { componentName, code: examples.join('\n\n// ---\n\n'), source: 'documentation' }; } } return null; } catch (error) { console.error(`Error getting example for ${componentName}:`, error); return null; } } private setupTools(): void { // List Components Tool this.server.tool( 'list_components', 'List all available components in the library', { category: z.string().optional().describe('Optional category filter (e.g., "forms", "layout", "display")') }, async (args: ListComponentsArgs) => { if (!this.initialized) await this.initialize(); const components = Array.from(this.componentCache.values()); let filtered = components; if (args.category) { filtered = components.filter(c => c.category?.toLowerCase() === args.category?.toLowerCase() ); } return { content: [ { type: 'text', text: JSON.stringify({ count: filtered.length, components: filtered.map(c => ({ name: c.name, category: c.category, description: c.description, status: c.status || 'stable' })) }, null, 2) } ] }; } ); // Get Component Tool this.server.tool( 'get_component', 'Get detailed information about a specific component including props, documentation, and usage', { componentName: z.string().describe('Name of the component to retrieve') }, async (args: GetComponentArgs) => { const componentInfo = await this.getComponentInfo(args.componentName); if (!componentInfo) { return { content: [ { type: 'text', text: `Component "${args.componentName}" not found` } ] }; } // Format the response const response = { name: componentInfo.name, description: componentInfo.description, category: componentInfo.category, props: componentInfo.props || {}, documentation: componentInfo.documentation, importPath: componentInfo.importPath || `@your-library/${args.componentName}`, status: componentInfo.status || 'stable', version: componentInfo.version, dependencies: componentInfo.dependencies, accessibility: componentInfo.accessibility }; return { content: [ { type: 'text', text: JSON.stringify(response, null, 2) } ] }; } ); // Get Example Tool this.server.tool( 'get_example', 'Get usage examples for a specific component', { componentName: z.string().describe('Name of the component to get examples for') }, async (args: GetExampleArgs) => { const example = await this.getComponentExample(args.componentName); if (!example) { return { content: [ { type: 'text', text: `No examples found for component "${args.componentName}"` } ] }; } return { content: [ { type: 'text', text: `// Example for ${args.componentName}\n\n${example.code}` } ] }; } ); } async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Component Library MCP server (v2) running on stdio'); } } // Main entry point if (import.meta.url === `file://${process.argv[1]}`) { const server = new ComponentLibraryServer(); server.run().catch(console.error); } export { ComponentLibraryServer };

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/alex-abrams711/component-library-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server