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

117 lines (116 loc) 5.32 kB
import fetch from 'node-fetch'; import { BaseService } from './BaseService.js'; import { RHDS_STANDARDS, DEFAULT_PACKAGE } from '../constants/index.js'; export class ManifestService extends BaseService { manifestCache = new Map(); componentCache = new Map(); getCacheKey(packageName, version = 'latest') { return `${packageName}@${version}`; } /** * Loads the Custom Elements Manifest for a given package and version. * Caches the manifest to avoid redundant network requests. * If the manifest is already cached, it returns the cached version. */ async loadManifest(packageName, version = 'latest') { const cacheKey = this.getCacheKey(packageName, version); if (this.manifestCache.has(cacheKey)) { return this.manifestCache.get(cacheKey); } return this.safeExecute(async () => { const manifest = await this.fetchManifestFromCdn(packageName, version); if (manifest) { this.manifestCache.set(cacheKey, manifest); } return manifest; }, `Failed to load manifest for ${packageName}@${version}`); } /** * Retrieves all components from the Custom Elements Manifest for a given package and version. * If the manifest is not found, it returns an empty array. * Caches the components to avoid redundant processing. * If the components are already cached, it returns the cached version. */ async getComponents(packageName, version = 'latest') { const cacheKey = this.getCacheKey(packageName, version); if (this.componentCache.has(cacheKey)) { return this.componentCache.get(cacheKey); } const manifest = await this.loadManifest(packageName, version); if (!manifest) { return []; } const components = this.extractComponents(manifest); this.componentCache.set(cacheKey, components); return components; } async isComponentValid(tagName, packageName = DEFAULT_PACKAGE) { const components = await this.getComponents(packageName); return components.some(c => c.tagName === tagName); } /** * Fetches the Custom Elements Manifest from a CDN (unpkg) for a given package and version. * It first checks for a customElements field in package.json, then falls back to the standard location. * Returns the manifest as a CustomElementsManifest object or null if not found. */ async fetchManifestFromCdn(packageName, version) { // Try package.json first for customElements field const packageJsonUrl = `https://unpkg.com/${packageName}@${version}/package.json`; const packageResponse = await fetch(packageJsonUrl); if (packageResponse.ok) { const packageJson = await packageResponse.json(); if (packageJson.customElements) { const manifestUrl = `https://unpkg.com/${packageName}@${version}/${packageJson.customElements}`; const manifestResponse = await fetch(manifestUrl); if (manifestResponse.ok) { return await manifestResponse.json(); } } } // Fallback to standard location const fallbackUrl = `https://unpkg.com/${packageName}@${version}/custom-elements.json`; const fallbackResponse = await fetch(fallbackUrl); if (!fallbackResponse.ok) { throw new Error(`HTTP ${fallbackResponse.status}: ${fallbackResponse.statusText}`); } return await fallbackResponse.json(); } extractComponents(manifest) { const components = []; for (const module of manifest.modules) { if (module.declarations) { for (const declaration of module.declarations) { if (declaration.customElement && declaration.tagName) { const component = this.createComponentFromDeclaration(declaration); components.push(component); } } } } return components; } /** * Creates a RHDSComponent from a Declaration object. * Extracts tagName, required attributes, accessibility requirements, and documentation URL. * Returns a RHDSComponent object with the necessary properties. */ createComponentFromDeclaration(declaration) { const tagName = declaration.tagName; const requiredAttributes = declaration.attributes?.filter(attr => attr.required).map(attr => attr.name) || []; const accessibilityAttributes = [...(RHDS_STANDARDS.accessibilityRequirements[tagName] || [])]; // Remove rh- prefix for documentation URL const componentName = tagName.replace(/^rh-/, ''); return { name: declaration.name, tagName, description: declaration.description || declaration.summary || '', attributes: declaration.attributes || [], slots: declaration.slots || [], examples: [], documentationUrl: `https://ux.redhat.com/elements/${componentName}/guidelines`, category: 'component', requiredAttributes, accessibilityAttributes }; } }