@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
JavaScript
#!/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();