Skip to main content
Glama
content-loader.ts9.17 kB
/** * Content loading and indexing system for MagentaA11y content */ import * as fs from 'fs/promises'; import * as path from 'path'; import { fileURLToPath } from 'url'; import Fuse from 'fuse.js'; // Get the directory of this module const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Use the copied content.json from data folder - this gets updated during build const CONTENT_JSON_PATH = path.join(__dirname, '..', 'data', 'content.json'); export interface ContentItem { label: string; name: string; type?: string; generalNotes?: string; gherkin?: string; condensed?: string; criteria?: string; videos?: string; androidDeveloperNotes?: string; iosDeveloperNotes?: string; developerNotes?: string; categoryName?: string; categoryLabel?: string; } export interface ContentCategory { label: string; name: string; children: ContentItem[]; } export interface ContentStructure { web: ContentCategory[]; native: ContentCategory[]; 'how-to-test': ContentCategory[]; } export interface ComponentMetadata { name: string; displayName: string; category: string; platform: 'web' | 'native'; hasGherkin?: boolean; hasCondensed?: boolean; hasDeveloperNotes?: boolean; hasAndroidNotes?: boolean; hasIOSNotes?: boolean; } export interface SearchResult { component: string; displayName: string; category: string; categoryLabel: string; matches: SearchMatch[]; relevance: number; } export interface SearchMatch { field: string; snippet: string; } export class ContentLoader { private content: ContentStructure | null = null; private webSearchIndex: Fuse<ContentItem> | null = null; private nativeSearchIndex: Fuse<ContentItem> | null = null; private indexed = false; /** * Initialize and index all content */ async initialize(): Promise<void> { if (this.indexed) return; try { const contentRaw = await fs.readFile(CONTENT_JSON_PATH, 'utf-8'); this.content = JSON.parse(contentRaw) as ContentStructure; // Create search indices this.webSearchIndex = this.createSearchIndex('web'); this.nativeSearchIndex = this.createSearchIndex('native'); this.indexed = true; const webCount = this.content.web.reduce((count, category) => count + category.children.length, 0); const nativeCount = this.content.native.reduce((count, category) => count + category.children.length, 0); console.error(`Indexed ${webCount} web and ${nativeCount} native components from content.json`); } catch (error) { console.error('Error loading content.json:', error); throw new Error('Failed to initialize content. Ensure content.json exists in magentaA11y/src/shared/'); } } /** * Create search index for a platform */ private createSearchIndex(platform: 'web' | 'native'): Fuse<ContentItem> { if (!this.content) throw new Error('Content not loaded'); const items: ContentItem[] = []; const categories = this.content[platform]; for (const category of categories) { for (const item of category.children) { items.push({ ...item, categoryName: category.name, categoryLabel: category.label, } as ContentItem & { categoryName: string; categoryLabel: string }); } } return new Fuse(items, { keys: [ { name: 'label', weight: 0.3 }, { name: 'name', weight: 0.3 }, { name: 'generalNotes', weight: 0.1 }, { name: 'gherkin', weight: 0.1 }, { name: 'condensed', weight: 0.1 }, { name: 'developerNotes', weight: 0.1 } ], threshold: 0.3, }); } /** * Format component name for display */ private formatDisplayName(name: string): string { return name .split('-') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } /** * List components for a platform */ listComponents(platform: 'web' | 'native', category?: string): ComponentMetadata[] { if (!this.content) throw new Error('Content not loaded'); const categories = this.content[platform]; const components: ComponentMetadata[] = []; for (const cat of categories) { if (category && cat.name !== category) continue; for (const item of cat.children) { components.push({ name: item.name, displayName: item.label, category: cat.name, platform, hasGherkin: !!item.gherkin, hasCondensed: !!item.condensed, hasDeveloperNotes: !!item.developerNotes, hasAndroidNotes: !!item.androidDeveloperNotes, hasIOSNotes: !!item.iosDeveloperNotes, }); } } return components.sort((a, b) => a.name.localeCompare(b.name)); } /** * Get categories for a platform */ getCategories(platform: 'web' | 'native'): string[] { if (!this.content) throw new Error('Content not loaded'); return this.content[platform].map(cat => cat.name).sort(); } /** * Get component details */ async getComponent(platform: 'web' | 'native', componentName: string): Promise<ContentItem> { if (!this.content) throw new Error('Content not loaded'); const categories = this.content[platform]; for (const category of categories) { const component = category.children.find(item => item.name === componentName); if (component) { return { ...component, categoryName: category.name, categoryLabel: category.label, }; } } throw new Error(`Component "${componentName}" not found`); } /** * Get specific content format for a component */ async getComponentContent(platform: 'web' | 'native', componentName: string, format: 'gherkin' | 'condensed' | 'developerNotes' | 'androidDeveloperNotes' | 'iosDeveloperNotes'): Promise<string> { const component = await this.getComponent(platform, componentName); const content = component[format]; if (!content) { throw new Error(`${format} content not available for component "${componentName}"`); } return content; } /** * Search components */ async search(platform: 'web' | 'native', query: string, maxResults: number = 10): Promise<SearchResult[]> { const searchIndex = platform === 'web' ? this.webSearchIndex : this.nativeSearchIndex; if (!searchIndex) throw new Error('Search index not initialized'); const results = searchIndex.search(query, { limit: maxResults }); return results.map(result => { const item = result.item; const matches: SearchMatch[] = []; // Extract matches from different fields if (result.matches) { for (const match of result.matches) { if (match.value && match.key) { const snippet = this.extractSnippet(match.value, query); matches.push({ field: match.key, snippet, }); } } } return { component: item.name, displayName: item.label, category: item.categoryName || '', categoryLabel: item.categoryLabel || '', matches, relevance: 1 - (result.score || 0), }; }); } /** * Extract snippet around query match */ private extractSnippet(text: string, query: string, maxLength: number = 200): string { const queryLower = query.toLowerCase(); const textLower = text.toLowerCase(); const index = textLower.indexOf(queryLower); if (index === -1) return text.substring(0, maxLength); const start = Math.max(0, index - 50); const end = Math.min(text.length, index + query.length + 150); let snippet = text.substring(start, end); if (start > 0) snippet = '...' + snippet; if (end < text.length) snippet = snippet + '...'; return snippet; } /** * Get similar component names for suggestions */ getSimilarComponents(platform: 'web' | 'native', componentName: string, limit: number = 5): string[] { const components = this.listComponents(platform); const fuse = new Fuse(components, { keys: ['name', 'displayName'], threshold: 0.5, }); const results = fuse.search(componentName); return results.slice(0, limit).map(r => r.item.name); } /** * Get all available formats for a component */ getAvailableFormats(platform: 'web' | 'native', componentName: string): string[] { try { const components = this.listComponents(platform); const component = components.find(c => c.name === componentName); if (!component) return []; const formats: string[] = []; if (component.hasGherkin) formats.push('gherkin'); if (component.hasCondensed) formats.push('condensed'); if (component.hasDeveloperNotes) formats.push('developerNotes'); if (component.hasAndroidNotes) formats.push('androidDeveloperNotes'); if (component.hasIOSNotes) formats.push('iosDeveloperNotes'); return formats; } catch { return []; } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/joe-watkins/magentaa11y-mcp-remote'

If you have feedback or need assistance with the MCP directory API, please join our Discord server