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