Skip to main content
Glama

Motion.dev MCP Server

sitemap.ts11.5 kB
/** * Motion.dev sitemap parsing and endpoint extraction */ import * as cheerio from 'cheerio'; import { DocumentationEndpoint, CategorizedEndpoints, Framework, DocumentationCategory } from '../types/motion.js'; import { createParseError } from '../utils/errors.js'; import { logger } from '../utils/logger.js'; export class SitemapProcessor { // private readonly baseUrl = 'https://motion.dev'; // Reserved for future use async parseDocumentationUrls(sitemapXml: string): Promise<DocumentationEndpoint[]> { logger.debug('Parsing sitemap XML for documentation URLs'); try { const $ = cheerio.load(sitemapXml, { xmlMode: true }); const endpoints: DocumentationEndpoint[] = []; $('url loc').each((_index, element) => { const url = $(element).text().trim(); // Only process documentation URLs if (this.isDocumentationUrl(url)) { const endpoint = this.createEndpointFromUrl(url); if (endpoint) { endpoints.push(endpoint); } } }); logger.info(`Parsed ${endpoints.length} documentation endpoints from sitemap`); return this.sortEndpoints(endpoints); } catch (error) { logger.error('Failed to parse sitemap XML', error as Error); throw createParseError('sitemap XML', sitemapXml, (error as Error).message); } } categorizeEndpoints(endpoints: DocumentationEndpoint[]): CategorizedEndpoints { const categorized: CategorizedEndpoints = { react: [], js: [], vue: [], general: [] }; for (const endpoint of endpoints) { switch (endpoint.framework) { case 'react': categorized.react.push(endpoint); break; case 'js': categorized.js.push(endpoint); break; case 'vue': categorized.vue.push(endpoint); break; default: categorized.general.push(endpoint); break; } } logger.debug('Endpoints categorized by framework', { react: categorized.react.length, js: categorized.js.length, vue: categorized.vue.length, general: categorized.general.length }); return categorized; } private isDocumentationUrl(url: string): boolean { // Check if URL contains documentation paths const docPatterns = [ '/docs/', '/guide/', '/api/', '/tutorial/', '/examples/' ]; return docPatterns.some(pattern => url.includes(pattern)); } private createEndpointFromUrl(url: string): DocumentationEndpoint | null { try { const urlObj = new URL(url); const path = urlObj.pathname; // Extract the relative path for our endpoint const docPath = path.startsWith('/docs/') ? path : `/docs${path}`; return { url: docPath, framework: this.extractFrameworkFromUrl(path), category: this.extractCategoryFromUrl(path), title: this.generateTitleFromPath(path), lastModified: new Date().toISOString() }; } catch (error) { logger.warn(`Failed to create endpoint from URL: ${url}`, { error: (error as Error).message }); return null; } } private extractFrameworkFromUrl(path: string): Framework | 'general' { const lowerPath = path.toLowerCase(); // Check for explicit framework mentions if (lowerPath.includes('/react') || lowerPath.includes('react-')) { return 'react'; } if (lowerPath.includes('/vue') || lowerPath.includes('vue-')) { return 'vue'; } if (lowerPath.includes('/js') || lowerPath.includes('/javascript') || lowerPath.includes('vanilla')) { return 'js'; } // Check for framework-specific patterns const reactPatterns = [ 'component', 'jsx', 'hook', 'framer-motion', 'animate-presence' ]; const vuePatterns = [ 'transition', 'v-motion', 'composable' ]; const jsPatterns = [ 'vanilla', 'animate-function', 'dom-animation' ]; if (reactPatterns.some(pattern => lowerPath.includes(pattern))) { return 'react'; } if (vuePatterns.some(pattern => lowerPath.includes(pattern))) { return 'vue'; } if (jsPatterns.some(pattern => lowerPath.includes(pattern))) { return 'js'; } return 'general'; } private extractCategoryFromUrl(path: string): DocumentationCategory { const lowerPath = path.toLowerCase(); // Category mapping patterns const categoryPatterns: Array<{ patterns: string[]; category: DocumentationCategory }> = [ { patterns: ['animation', 'animate', 'motion', 'keyframe', 'transition'], category: 'animation' }, { patterns: ['gesture', 'drag', 'hover', 'tap', 'pan', 'pinch'], category: 'gestures' }, { patterns: ['layout', 'shared-layout', 'layout-animation'], category: 'layout-animations' }, { patterns: ['scroll', 'scroll-trigger', 'inview', 'viewport'], category: 'scroll-animations' }, { patterns: ['component', 'motion-component', 'api'], category: 'components' }, { patterns: ['api', 'reference', 'props', 'methods'], category: 'api-reference' }, { patterns: ['example', 'demo', 'showcase', 'tutorial'], category: 'examples' }, { patterns: ['best-practice', 'performance', 'optimization', 'accessibility', 'tips'], category: 'best-practices' } ]; for (const { patterns, category } of categoryPatterns) { if (patterns.some(pattern => lowerPath.includes(pattern))) { return category; } } return 'guides'; // Default category } private generateTitleFromPath(path: string): string { // Extract the last segment and clean it up const segments = path.split('/').filter(Boolean); const lastSegment = segments[segments.length - 1] || ''; // Convert kebab-case or snake_case to Title Case return lastSegment .replace(/[-_]/g, ' ') .replace(/\b\w/g, char => char.toUpperCase()) .trim() || 'Documentation'; } private sortEndpoints(endpoints: DocumentationEndpoint[]): DocumentationEndpoint[] { return endpoints.sort((a, b) => { // First sort by framework if (a.framework !== b.framework) { const frameworkOrder = ['react', 'js', 'vue', 'general']; const aIndex = frameworkOrder.indexOf(a.framework as string); const bIndex = frameworkOrder.indexOf(b.framework as string); return aIndex - bIndex; } // Then by category if (a.category !== b.category) { const categoryOrder: DocumentationCategory[] = [ 'guides', 'animation', 'components', 'gestures', 'scroll-animations', 'layout-animations', 'api-reference', 'examples', 'best-practices' ]; const aIndex = categoryOrder.indexOf(a.category); const bIndex = categoryOrder.indexOf(b.category); return aIndex - bIndex; } // Finally by title return a.title.localeCompare(b.title); }); } // Utility methods for endpoint analysis getFrameworkCounts(endpoints: DocumentationEndpoint[]): Record<string, number> { const counts: Record<string, number> = { react: 0, js: 0, vue: 0, general: 0 }; for (const endpoint of endpoints) { counts[endpoint.framework as string] = (counts[endpoint.framework as string] || 0) + 1; } return counts; } getCategoryCounts(endpoints: DocumentationEndpoint[]): Record<DocumentationCategory, number> { const counts = {} as Record<DocumentationCategory, number>; for (const endpoint of endpoints) { counts[endpoint.category] = (counts[endpoint.category] || 0) + 1; } return counts; } filterEndpoints( endpoints: DocumentationEndpoint[], filters: { framework?: Framework | 'general'; category?: DocumentationCategory; search?: string; } ): DocumentationEndpoint[] { return endpoints.filter(endpoint => { if (filters.framework && endpoint.framework !== filters.framework) { return false; } if (filters.category && endpoint.category !== filters.category) { return false; } if (filters.search) { const searchLower = filters.search.toLowerCase(); return ( endpoint.title.toLowerCase().includes(searchLower) || endpoint.url.toLowerCase().includes(searchLower) ); } return true; }); } validateEndpoint(endpoint: DocumentationEndpoint): boolean { // Basic validation if (!endpoint.url || !endpoint.title || !endpoint.framework || !endpoint.category) { return false; } // URL validation if (!endpoint.url.startsWith('/')) { return false; } // Framework validation const validFrameworks = ['react', 'js', 'vue', 'general']; if (!validFrameworks.includes(endpoint.framework as string)) { return false; } // Category validation const validCategories: DocumentationCategory[] = [ 'animation', 'gestures', 'layout-animations', 'scroll-animations', 'components', 'api-reference', 'guides', 'examples', 'best-practices' ]; if (!validCategories.includes(endpoint.category)) { return false; } return true; } // Debug and analysis methods analyzeEndpoints(endpoints: DocumentationEndpoint[]): { total: number; byFramework: Record<string, number>; byCategory: Record<DocumentationCategory, number>; coverage: { reactCoverage: number; vueCoverage: number; jsCoverage: number; }; } { const total = endpoints.length; const byFramework = this.getFrameworkCounts(endpoints); const byCategory = this.getCategoryCounts(endpoints); const coverage = { reactCoverage: byFramework.react / total, vueCoverage: byFramework.vue / total, jsCoverage: byFramework.js / total }; return { total, byFramework, byCategory, coverage }; } generateSummaryReport(endpoints: DocumentationEndpoint[]): string { const analysis = this.analyzeEndpoints(endpoints); return ` Motion.dev Documentation Endpoints Summary ========================================== Total Endpoints: ${analysis.total} Framework Distribution: - React: ${analysis.byFramework.react} (${(analysis.coverage.reactCoverage * 100).toFixed(1)}%) - JavaScript: ${analysis.byFramework.js} (${(analysis.coverage.jsCoverage * 100).toFixed(1)}%) - Vue: ${analysis.byFramework.vue} (${(analysis.coverage.vueCoverage * 100).toFixed(1)}%) - General: ${analysis.byFramework.general} Category Distribution: ${Object.entries(analysis.byCategory) .map(([category, count]) => `- ${category}: ${count}`) .join('\n')} Coverage Analysis: - React has the highest coverage at ${(analysis.coverage.reactCoverage * 100).toFixed(1)}% - Vue coverage: ${(analysis.coverage.vueCoverage * 100).toFixed(1)}% - JavaScript coverage: ${(analysis.coverage.jsCoverage * 100).toFixed(1)}% `.trim(); } }

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/Abhishekrajpurohit/motion-dev-mcp'

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