Skip to main content
Glama

Motion.dev MCP Server

motion-doc-service.ts16.6 kB
import { DatabaseAdapter, createDatabaseAdapter } from '../database/database-adapter'; import { MotionRepository, MotionDoc, MotionComponent, MotionExample } from '../database/motion-repository'; import { DocumentationFetcher } from '../docs/fetcher'; import { Logger } from '../utils/logger'; import * as cheerio from 'cheerio'; import TurndownService from 'turndown'; /** * Service for managing Motion.dev documentation in SQLite database */ export class MotionDocService { private repository!: MotionRepository; private fetcher: DocumentationFetcher; private logger = Logger.getInstance(); private turndown = new TurndownService(); private db!: DatabaseAdapter; private initialized = false; private initializationPromise?: Promise<void>; constructor(fetcher?: DocumentationFetcher, _cache?: any, dbPath: string = 'docs/motion-docs.db') { this.fetcher = fetcher || new DocumentationFetcher(); this.setupTurndown(); this.initializationPromise = this.initializeDatabase(dbPath); } private async initializeDatabase(dbPath: string): Promise<void> { try { this.db = await createDatabaseAdapter(dbPath); this.repository = new MotionRepository(this.db); this.initialized = true; this.logger.info('Motion.dev documentation database initialized'); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error('Failed to initialize Motion.dev database', new Error(errorMessage)); throw new Error(`Database initialization failed: ${errorMessage}`); } } async ensureInitialized(): Promise<void> { if (!this.initialized && this.initializationPromise) { await this.initializationPromise; } } private setupTurndown(): void { // Configure Turndown for better Markdown conversion this.turndown.addRule('codeBlock', { filter: ['pre'], replacement: (content, node) => { const code = node.querySelector('code'); if (code) { const className = code.getAttribute('class') || ''; const language = className.replace('language-', '').replace('hljs', '').trim(); return `\`\`\`${language}\n${code.textContent}\n\`\`\``; } return `\`\`\`\n${content}\n\`\`\``; } }); this.turndown.addRule('inlineCode', { filter: ['code'], replacement: (content) => `\`${content}\`` }); } /** * Populate database with Motion.dev documentation */ async populateDatabase(): Promise<void> { this.logger.info('Starting Motion.dev documentation population...'); try { await this.populateReactDocs(); await this.populateJSDocs(); await this.populateVueDocs(); await this.populateExamples(); const stats = this.repository.getStatistics(); this.logger.info('Documentation population completed', stats); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error('Failed to populate documentation database', new Error(errorMessage)); throw new Error(`Documentation population failed: ${errorMessage}`); } } /** * Populate React documentation */ private async populateReactDocs(): Promise<void> { this.logger.info('Populating React documentation...'); const reactEndpoints = [ '/docs/react', '/docs/react-animation', '/docs/react-gestures', '/docs/react-layout-animations', '/docs/react-scroll-animations', '/docs/react-exit-animations', '/docs/react-motion-values', '/docs/react-animate-presence', '/docs/react-drag', '/docs/react-in-view' ]; for (const endpoint of reactEndpoints) { try { await this.fetchAndStoreDocs(endpoint, 'react'); await this.sleep(100); // Rate limiting } catch (error) { this.logger.warn(`Failed to fetch React docs for ${endpoint}:`, error instanceof Error ? error : new Error(String(error))); } } } /** * Populate JavaScript documentation */ private async populateJSDocs(): Promise<void> { this.logger.info('Populating JavaScript documentation...'); const jsEndpoints = [ '/docs/quick-start', '/docs/animate', '/docs/scroll', '/docs/spring', '/docs/timeline', '/docs/stagger', '/docs/velocity' ]; for (const endpoint of jsEndpoints) { try { await this.fetchAndStoreDocs(endpoint, 'js'); await this.sleep(100); // Rate limiting } catch (error) { this.logger.warn(`Failed to fetch JS docs for ${endpoint}:`, error instanceof Error ? error : new Error(String(error))); } } } /** * Populate Vue documentation */ private async populateVueDocs(): Promise<void> { this.logger.info('Populating Vue documentation...'); const vueEndpoints = [ '/docs/vue', '/docs/vue-animation', '/docs/vue-gestures' ]; for (const endpoint of vueEndpoints) { try { await this.fetchAndStoreDocs(endpoint, 'vue'); await this.sleep(100); // Rate limiting } catch (error) { this.logger.warn(`Failed to fetch Vue docs for ${endpoint}:`, error instanceof Error ? error : new Error(String(error))); } } } /** * Fetch and store documentation for a specific endpoint */ private async fetchAndStoreDocs(endpoint: string, framework: 'react' | 'js' | 'vue'): Promise<void> { try { const url = `https://motion.dev${endpoint}`; const response = await this.fetcher.fetchDoc(url); const content = response.success && response.document ? response.document.content : null; if (!content) { this.logger.warn(`No content found for ${url}`); return; } const $ = cheerio.load(content || ''); // Extract documentation data const title = $('h1').first().text().trim() || endpoint.replace('/docs/', '').replace('-', ' '); const description = $('meta[name="description"]').attr('content') || $('p').first().text().trim(); // Extract main content (remove navigation, header, footer) $('nav, header, footer, .navigation, .sidebar').remove(); const mainContent = $('main, .content, article, .docs-content').html() || $('body').html(); if (!mainContent) { this.logger.warn(`No main content found for ${url}`); return; } // Convert HTML to Markdown const markdownContent = this.turndown.turndown(mainContent); // Extract code examples const examples = this.extractCodeExamples($, framework); // Extract API reference if present const apiReference = this.extractApiReference($); // Determine category from URL const category = this.categorizeEndpoint(endpoint); // Create documentation entry const doc: MotionDoc = { url, title, framework, category, description, content: markdownContent, examples: examples.length > 0 ? JSON.stringify(examples) : undefined, apiReference: apiReference ? JSON.stringify(apiReference) : undefined, isReact: framework === 'react', isJs: framework === 'js', isVue: framework === 'vue', tags: JSON.stringify(this.extractTags(endpoint, title, markdownContent)) }; // Save to database this.repository.saveDoc(doc); // Extract and save components const components = this.extractComponents($, framework); for (const component of components) { this.repository.saveComponent(component); } this.logger.debug(`Saved documentation for ${url}`); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`Failed to fetch and store docs for ${endpoint}:`, new Error(errorMessage)); } } /** * Extract code examples from documentation */ private extractCodeExamples($: any, framework: 'react' | 'js' | 'vue'): any[] { const examples: any[] = []; $('pre code, .code-block code').each((_i: number, element: any) => { const code = $(element).text().trim(); if (code && code.length > 10) { // Filter out very short code snippets const language = $(element).attr('class')?.replace('language-', '') || framework; examples.push({ title: `Example ${_i + 1}`, code, language, framework }); } }); return examples; } /** * Extract API reference information */ private extractApiReference($: any): any | null { const apiSections = $('.api-reference, .props-table, .method-table'); if (apiSections.length === 0) return null; const apiInfo: any = { props: [], methods: [], types: [] }; // Extract props/parameters $('.props-table tr, .api-table tr').each((_i: number, row: any) => { const cells = $(row).find('td'); if (cells.length >= 2) { const name = $(cells[0]).text().trim(); const description = $(cells[1]).text().trim(); const type = cells.length > 2 ? $(cells[2]).text().trim() : 'unknown'; if (name && description) { apiInfo.props.push({ name, type, description }); } } }); return apiInfo.props.length > 0 || apiInfo.methods.length > 0 ? apiInfo : null; } /** * Extract component information */ private extractComponents($: any, framework: 'react' | 'js' | 'vue'): MotionComponent[] { const components: MotionComponent[] = []; // Extract component names from headings and code examples const componentNames = new Set<string>(); // Look for common Motion.dev components in headings $('h1, h2, h3').each((_i: number, element: any) => { const heading = $(element).text().trim(); const motionComponents = heading.match(/(motion\.\w+|animate|spring|scroll|timeline|stagger)/gi); if (motionComponents) { motionComponents.forEach((name: string) => componentNames.add(name)); } }); // Look for components in code examples $('code').each((_i: number, element: any) => { const code = $(element).text(); const motionComponents = code.match(/(motion\.\w+|animate\(|spring\(|scroll\(|timeline\(|stagger\()/g); if (motionComponents) { motionComponents.forEach((match: string) => { const name = match.replace('(', ''); componentNames.add(name); }); } }); // Create component entries componentNames.forEach(name => { if (name && name.length > 2) { const component: MotionComponent = { name, framework, type: name.startsWith('motion.') ? 'component' : 'function', description: `Motion.dev ${name} component/function`, }; components.push(component); } }); return components; } /** * Populate examples database */ private async populateExamples(): Promise<void> { this.logger.info('Populating examples...'); // Create some basic examples for each framework const exampleTemplates = [ { title: 'Basic Animation', description: 'Simple fade in animation', category: 'basic', difficulty: 'beginner' as const, examples: { react: `import { motion } from "motion/react"; export function FadeIn() { return ( <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.5 }} > Hello World </motion.div> ); }`, js: `import { animate } from "motion"; animate(".fade-in", { opacity: [0, 1] }, { duration: 0.5 } );`, vue: `<template> <Motion :initial="{ opacity: 0 }" :animate="{ opacity: 1 }" :transition="{ duration: 0.5 }" > Hello World </Motion> </template> <script setup> import { Motion } from "motion/vue"; </script>` } }, { title: 'Spring Animation', description: 'Bouncy spring animation', category: 'springs', difficulty: 'intermediate' as const, examples: { react: `import { motion } from "motion/react"; export function SpringAnimation() { return ( <motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ type: "spring", stiffness: 100, damping: 10 }} > Spring Animation </motion.div> ); }`, js: `import { animate } from "motion"; animate(".spring", { scale: [0, 1] }, { type: "spring", stiffness: 100, damping: 10 } );`, vue: `<template> <Motion :initial="{ scale: 0 }" :animate="{ scale: 1 }" :transition="{ type: 'spring', stiffness: 100, damping: 10 }" > Spring Animation </Motion> </template>` } } ]; for (const template of exampleTemplates) { for (const [framework, code] of Object.entries(template.examples)) { const example: MotionExample = { title: `${template.title} (${framework.toUpperCase()})`, description: template.description, framework: framework as 'react' | 'js' | 'vue', category: template.category, code, difficulty: template.difficulty, tags: JSON.stringify([framework, template.category, template.difficulty]) }; this.repository.saveExample(example); } } } /** * Categorize documentation endpoint */ private categorizeEndpoint(endpoint: string): string { if (endpoint.includes('animation')) return 'animations'; if (endpoint.includes('gesture')) return 'gestures'; if (endpoint.includes('scroll')) return 'scroll'; if (endpoint.includes('layout')) return 'layout'; if (endpoint.includes('spring')) return 'springs'; if (endpoint.includes('drag')) return 'gestures'; if (endpoint.includes('quick-start')) return 'getting-started'; return 'general'; } /** * Extract tags from documentation */ private extractTags(endpoint: string, title: string, content: string): string[] { const tags = new Set<string>(); // Add framework-specific tags if (endpoint.includes('react')) tags.add('react'); if (endpoint.includes('vue')) tags.add('vue'); if (endpoint.includes('js') || endpoint.includes('javascript')) tags.add('javascript'); // Add feature tags based on content const features = [ 'animation', 'gesture', 'scroll', 'spring', 'drag', 'layout', 'keyframes', 'timeline', 'stagger', 'motion-values', 'variants' ]; for (const feature of features) { if (title.toLowerCase().includes(feature) || content.toLowerCase().includes(feature)) { tags.add(feature); } } return Array.from(tags); } /** * Get documentation by URL */ async getDocumentation(url: string): Promise<MotionDoc | null> { return this.repository.getDocByUrl(url); } /** * Search documentation */ async searchDocumentation(query: string, options?: { framework?: 'react' | 'js' | 'vue'; category?: string; limit?: number }): Promise<MotionDoc[]> { return this.repository.searchDocs(query, options); } /** * Get component information */ async getComponent(name: string, framework: 'react' | 'js' | 'vue'): Promise<MotionComponent | null> { return this.repository.getComponent(name, framework); } /** * Search examples */ async searchExamples(query: string, options?: { framework?: 'react' | 'js' | 'vue'; category?: string; limit?: number }): Promise<MotionExample[]> { return this.repository.searchExamples(query, options); } /** * Get examples by category */ async getExamplesByCategory(category: string, framework?: 'react' | 'js' | 'vue'): Promise<MotionExample[]> { return this.repository.getExamplesByCategory(category, framework); } /** * Get database statistics */ async getStatistics() { await this.ensureInitialized(); return this.repository.getStatistics(); } /** * Close database connection */ close(): void { this.repository.close(); } private sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } }

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