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