UNPKG

@scarlet-mesh/mcp-rhds

Version:

RHDS MCP Server - All-in-One Model Context Protocol server for Red Hat Design System components with manifest discovery, HTML validation, and developer tooling

258 lines (257 loc) 11.6 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 { ServiceContainer } from './container/ServiceContainer.js'; import { ResponseFormatter } from './formatters/ResponseFormatter.js'; const server = new McpServer({ name: 'rhds', version: '1.0.0', capabilities: { resources: {}, tools: {}, }, }); const services = ServiceContainer.getInstance(); // Tool 1: Create Component server.tool('create-component', 'Create a new RHDS component with proper structure, IDs, and attributes', { tagName: z.string().describe('RHDS component tag name (e.g., "rh-button", "rh-card")'), id: z.string().optional().describe('Custom ID for the component (will be generated if not provided)'), attributes: z.record(z.string()).optional().describe('Component attributes as key-value pairs'), content: z.string().optional().describe('Inner content for the component'), slots: z.record(z.string()).optional().describe('Slot content as slot-name: content pairs'), includeAccessibility: z.boolean().optional().default(true).describe('Include accessibility attributes'), packageName: z.string().optional().default('@rhds/elements').describe('Package name for component definitions') }, async ({ tagName, id, attributes = {}, content, slots, includeAccessibility = true, packageName = '@rhds/elements' }) => { try { const componentService = services.getComponentService(); const result = await componentService.createComponent({ tagName, id, attributes, content, slots, includeAccessibility }, packageName); if (!result.success) { return { content: [{ type: 'text', text: `${result.error}` }] }; } // Get component details for formatting const manifestService = services.getManifestService(); const components = await manifestService.getComponents(packageName); const component = components.find(c => c.tagName === tagName); const finalAttributes = { id: id || services.getIdGeneratorService().generateId(tagName.replace('rh-', ''), 'component'), ...attributes }; const formattedResult = ResponseFormatter.formatComponentResult(result.data, component, finalAttributes, result.warnings); return { content: [{ type: 'text', text: formattedResult }] }; } catch (error) { return { content: [{ type: 'text', text: `Error creating component: ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } }); // Tool 2: Generate Component ID server.tool('generate-component-id', 'Generate RHDS-compliant IDs for components following naming conventions', { componentType: z.string().describe('Type of component (e.g., "button", "card", "navigation")'), purpose: z.string().describe('Purpose or context (e.g., "primary", "header", "sidebar")'), context: z.string().optional().describe('Additional context (e.g., "form", "modal", "menu")'), validate: z.boolean().optional().default(true).describe('Validate the generated ID against RHDS standards') }, async ({ componentType, purpose, context, validate = true }) => { try { const idGenerator = services.getIdGeneratorService(); const generatedId = idGenerator.generateId(componentType, purpose, context); let validation; if (validate) { validation = idGenerator.validateIdFormat(generatedId, componentType); } const formattedResult = ResponseFormatter.formatIdGenerationResult(generatedId, componentType, purpose, context, validation); return { content: [{ type: 'text', text: formattedResult }] }; } catch (error) { return { content: [{ type: 'text', text: `Error generating ID: ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } }); // Tool 3: Get Component Template server.tool('get-component-template', 'Get a complete template for an RHDS component with all required attributes and structure', { tagName: z.string().describe('RHDS component tag name (e.g., "rh-button", "rh-card")'), includeOptional: z.boolean().optional().default(false).describe('Include optional attributes in template'), includeAccessibility: z.boolean().optional().default(true).describe('Include accessibility attributes'), packageName: z.string().optional().default('@rhds/elements').describe('Package name for component definitions') }, async ({ tagName, includeOptional = false, includeAccessibility = true, packageName = '@rhds/elements' }) => { try { const componentService = services.getComponentService(); const result = await componentService.getTemplate(tagName, { includeOptional, includeAccessibility }, packageName); if (!result.success) { return { content: [{ type: 'text', text: `${result.error}` }] }; } // Get component details for formatting const manifestService = services.getManifestService(); const components = await manifestService.getComponents(packageName); const component = components.find(c => c.tagName === tagName); const formattedResult = ResponseFormatter.formatTemplateResult(result.data, component, includeOptional); return { content: [{ type: 'text', text: formattedResult }] }; } catch (error) { return { content: [{ type: 'text', text: `Error getting template: ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } }); // Tool 4: Validate Component server.tool('validate-component', 'Validate an RHDS component against standards and provide improvement suggestions', { html: z.string().describe('HTML content containing RHDS components to validate'), checkAccessibility: z.boolean().optional().default(true).describe('Check accessibility requirements'), checkIds: z.boolean().optional().default(true).describe('Validate ID naming conventions'), packageName: z.string().optional().default('@rhds/elements').describe('Package name for component definitions') }, async ({ html, checkAccessibility = true, checkIds = true, packageName = '@rhds/elements' }) => { try { const validationService = services.getValidationService(); const result = await validationService.validateComponent(html, { checkAccessibility, checkIds, packageName }); const formattedResult = ResponseFormatter.formatValidationResult(result); return { content: [{ type: 'text', text: formattedResult }] }; } catch (error) { return { content: [{ type: 'text', text: `Error validating component: ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } }); // Tool 5: List Available Components server.tool('list-components', 'List all available RHDS components with their descriptions and capabilities', { packageName: z.string().optional().default('@rhds/elements').describe('Package name for component definitions'), category: z.string().optional().describe('Filter by component category'), search: z.string().optional().describe('Search components by name or description') }, async ({ packageName = '@rhds/elements', category, search }) => { try { const componentService = services.getComponentService(); const result = await componentService.listComponents({ category, search }, packageName); if (!result.success) { return { content: [{ type: 'text', text: `${result.error}` }] }; } if (result.data.length === 0) { return { content: [{ type: 'text', text: `No components found matching your criteria.` }] }; } const formattedResult = ResponseFormatter.formatComponentList(result.data, packageName); return { content: [{ type: 'text', text: formattedResult }] }; } catch (error) { return { content: [{ type: 'text', text: `Error listing components: ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } }); // Tool 6: Create Component Composition server.tool('create-composition', 'Create a composition of multiple RHDS components with proper structure and relationships', { components: z.array(z.object({ tagName: z.string(), id: z.string().optional(), attributes: z.record(z.string()).optional(), content: z.string().optional(), slots: z.record(z.string()).optional(), children: z.array(z.any()).optional() })).describe('Array of components to compose'), wrapperElement: z.string().optional().describe('HTML element to wrap the composition (e.g., "div", "section")'), wrapperId: z.string().optional().describe('ID for the wrapper element'), wrapperClass: z.string().optional().describe('CSS class for the wrapper element'), packageName: z.string().optional().default('@rhds/elements').describe('Package name for component definitions') }, async ({ components: requestedComponents, wrapperElement, wrapperId, wrapperClass, packageName = '@rhds/elements' }) => { try { const componentService = services.getComponentService(); const result = await componentService.createComposition(requestedComponents, { wrapperElement, wrapperId, wrapperClass }, packageName); if (!result.success) { return { content: [{ type: 'text', text: `${result.error}` }] }; } const formattedResult = ResponseFormatter.formatCompositionResult(result.data, requestedComponents.length, packageName, wrapperElement, result.warnings); return { content: [{ type: 'text', text: formattedResult }] }; } catch (error) { return { content: [{ type: 'text', text: `Error creating composition: ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } }); async function main() { try { await services.initialize(); const transport = new StdioServerTransport(); await server.connect(transport); console.error('RHDS MCP Server running on stdio'); } catch (error) { console.error('Fatal error starting server:', error); process.exit(1); } } main();