Skip to main content
Glama

Motion.dev MCP Server

generation.ts27.3 kB
/** * Code generation MCP tools implementation * Handles Motion.dev component generation for React, JavaScript, and Vue */ import { Framework } from '../types/motion.js'; import { logger } from '../utils/logger.js'; import { MotionMCPError, createToolError } from '../utils/errors.js'; import { ASTParser } from '../generation/ast/parser.js'; import { ASTTransformer } from '../generation/ast/transformer.js'; import { ASTGenerator } from '../generation/ast/generator.js'; import { AnimationPatterns } from '../generation/templates/patterns.js'; // Tool parameter interfaces export interface GenerateMotionComponentParams { framework: Framework; componentName: string; animations: string[]; props?: string[]; typescript?: boolean; styleSystem?: 'css' | 'styled-components' | 'emotion' | 'tailwind'; } export interface CreateAnimationSequenceParams { framework: Framework; sequence: Array<{ element: string; animate: Record<string, any>; delay?: number; duration?: number; easing?: string; }>; stagger?: boolean; timeline?: boolean; } export interface OptimizeMotionCodeParams { code: string; framework: Framework; focusAreas?: Array<'performance' | 'accessibility' | 'bundle-size'>; target?: 'production' | 'development'; } export interface ConvertBetweenFrameworksParams { from: Framework; to: Framework; code: string; preserveComments?: boolean; optimization?: boolean; } export interface ValidateMotionSyntaxParams { code: string; framework: Framework; strict?: boolean; rules?: string[]; } // Response interfaces export interface CodeGenerationResponse { success: boolean; code?: string; imports?: string[]; dependencies?: string[]; framework: Framework; typescript: boolean; metadata?: { componentName?: string; complexity: 'basic' | 'intermediate' | 'advanced'; patterns: string[]; performance: { score: number; suggestions: string[]; }; }; error?: string; warnings?: string[]; generateTime: number; } export interface ValidationResponse { valid: boolean; errors: Array<{ type: 'error' | 'warning'; message: string; line?: number; rule: string; }>; suggestions: Array<{ type: 'performance' | 'accessibility' | 'best-practice'; message: string; priority: 'high' | 'medium' | 'low'; }>; framework: Framework; analysisTime: number; } export class CodeGenerationTool { private parser: ASTParser; private transformer: ASTTransformer; private generator: ASTGenerator; private patterns: AnimationPatterns; constructor() { this.parser = new ASTParser(); this.transformer = new ASTTransformer(); this.generator = new ASTGenerator(); this.patterns = new AnimationPatterns(); } async generateMotionComponent(params: GenerateMotionComponentParams): Promise<CodeGenerationResponse> { const startTime = Date.now(); logger.logToolExecution('generate_motion_component', params); try { // Validate parameters this.validateGenerateComponentParams(params); // Build component using patterns const componentCode = await this.buildComponentFromPatterns(params); // For now, return the raw pattern code until optimization pipeline is fixed const result = { code: componentCode, imports: ['import { motion } from "framer-motion";'], dependencies: ['framer-motion'] }; const response: CodeGenerationResponse = { success: true, code: result.code, imports: result.imports, dependencies: result.dependencies || [], framework: params.framework, typescript: params.typescript || true, metadata: { componentName: params.componentName, complexity: this.determineComplexity(params.animations), patterns: params.animations, performance: await this.analyzePerformance(result.code, params.framework) }, generateTime: Date.now() - startTime }; logger.logPerformanceMetric('generate_motion_component', response.generateTime, 'ms'); return response; } catch (error) { logger.error('Motion component generation failed', error as Error); return { success: false, framework: params.framework, typescript: params.typescript || true, error: error instanceof MotionMCPError ? error.message : String(error), generateTime: Date.now() - startTime }; } } async createAnimationSequence(params: CreateAnimationSequenceParams): Promise<CodeGenerationResponse> { const startTime = Date.now(); logger.logToolExecution('create_animation_sequence', params); try { this.validateSequenceParams(params); const sequenceCode = await this.buildAnimationSequence(params); const optimizedCode = await this.optimizeSequenceCode(sequenceCode, params); const response: CodeGenerationResponse = { success: true, code: optimizedCode, framework: params.framework, typescript: true, metadata: { complexity: 'intermediate', patterns: ['sequence', 'timeline'], performance: await this.analyzePerformance(optimizedCode, params.framework) }, generateTime: Date.now() - startTime }; logger.logPerformanceMetric('create_animation_sequence', response.generateTime, 'ms'); return response; } catch (error) { logger.error('Animation sequence creation failed', error as Error); return { success: false, framework: params.framework, typescript: true, error: error instanceof MotionMCPError ? error.message : String(error), generateTime: Date.now() - startTime }; } } async optimizeMotionCode(params: OptimizeMotionCodeParams): Promise<CodeGenerationResponse> { const startTime = Date.now(); logger.logToolExecution('optimize_motion_code', params); try { // Parse existing code const ast = this.parser.parse(params.code, { framework: params.framework, typescript: params.code.includes('interface') || params.code.includes(': ') }); // Apply optimizations const context = { framework: params.framework, typescript: ast.typescript, componentName: ast.componentName, imports: new Set<string>(), dependencies: new Set<string>(), optimization: { performance: params.focusAreas?.includes('performance') || false, accessibility: params.focusAreas?.includes('accessibility') || false, bundleSize: params.focusAreas?.includes('bundle-size') || false } }; const transformedAST = this.transformer.transform(ast, context); const result = await this.generator.generate(transformedAST, context); // Apply additional optimizations let optimizedCode = result.code; if (params.focusAreas) { optimizedCode = await this.generator.optimizeCode(result.code, context, params.focusAreas); } const response: CodeGenerationResponse = { success: true, code: optimizedCode, imports: result.imports, framework: params.framework, typescript: result.typescript, metadata: { componentName: ast.componentName, complexity: 'intermediate', patterns: ['optimization'], performance: await this.analyzePerformance(optimizedCode, params.framework) }, generateTime: Date.now() - startTime }; logger.logPerformanceMetric('optimize_motion_code', response.generateTime, 'ms'); return response; } catch (error) { logger.error('Motion code optimization failed', error as Error); return { success: false, framework: params.framework, typescript: true, error: error instanceof MotionMCPError ? error.message : String(error), generateTime: Date.now() - startTime }; } } async convertBetweenFrameworks(params: ConvertBetweenFrameworksParams): Promise<CodeGenerationResponse> { const startTime = Date.now(); logger.logToolExecution('convert_between_frameworks', params); try { // Parse source code const sourceAST = this.parser.parse(params.code, { framework: params.from, typescript: params.code.includes('interface') || params.code.includes(': ') }); // Transform for target framework const targetContext = { framework: params.to, typescript: sourceAST.typescript, componentName: sourceAST.componentName, imports: new Set<string>(), dependencies: new Set<string>(), optimization: params.optimization ? { performance: true, accessibility: true, bundleSize: true } : undefined }; // Apply framework conversion transformations const convertedAST = await this.convertASTBetweenFrameworks(sourceAST, params.from, params.to); const transformedAST = this.transformer.transform(convertedAST, targetContext); const result = await this.generator.generate(transformedAST, targetContext); const response: CodeGenerationResponse = { success: true, code: result.code, imports: result.imports, framework: params.to, typescript: result.typescript, metadata: { componentName: sourceAST.componentName, complexity: 'intermediate', patterns: ['framework-conversion'], performance: await this.analyzePerformance(result.code, params.to) }, warnings: this.getConversionWarnings(params.from, params.to), generateTime: Date.now() - startTime }; logger.logPerformanceMetric('convert_between_frameworks', response.generateTime, 'ms'); return response; } catch (error) { logger.error('Framework conversion failed', error as Error); return { success: false, framework: params.to, typescript: true, error: error instanceof MotionMCPError ? error.message : String(error), generateTime: Date.now() - startTime }; } } async validateMotionSyntax(params: ValidateMotionSyntaxParams): Promise<ValidationResponse> { const startTime = Date.now(); logger.logToolExecution('validate_motion_syntax', params); try { // Parse code for validation const ast = this.parser.parse(params.code, { framework: params.framework, typescript: params.code.includes('interface') || params.code.includes(': ') }); // Extract motion elements for validation const motionElements = this.parser.extractMotionElements(ast.ast as any); // Validate syntax and patterns const validationResults = await this.performValidation(motionElements, params); const response: ValidationResponse = { valid: validationResults.errors.length === 0, errors: validationResults.errors, suggestions: validationResults.suggestions, framework: params.framework, analysisTime: Date.now() - startTime }; logger.logPerformanceMetric('validate_motion_syntax', response.analysisTime, 'ms'); return response; } catch (error) { logger.error('Motion syntax validation failed', error as Error); return { valid: false, errors: [{ type: 'error', message: error instanceof MotionMCPError ? error.message : String(error), rule: 'parse-error' }], suggestions: [], framework: params.framework, analysisTime: Date.now() - startTime }; } } // Private helper methods private validateGenerateComponentParams(params: GenerateMotionComponentParams): void { if (!params.framework || !['react', 'js', 'vue'].includes(params.framework)) { throw createToolError('generate_motion_component', 'Invalid framework specified'); } if (!params.componentName || params.componentName.length === 0) { throw createToolError('generate_motion_component', 'Component name is required'); } if (!params.animations || params.animations.length === 0) { throw createToolError('generate_motion_component', 'At least one animation pattern is required'); } // Validate animation patterns exist for (const animationId of params.animations) { const pattern = this.patterns.getPattern(animationId); if (!pattern) { throw createToolError('generate_motion_component', `Unknown animation pattern: ${animationId}`); } if (!pattern.frameworks.includes(params.framework)) { throw createToolError('generate_motion_component', `Animation pattern '${animationId}' not supported for ${params.framework}`); } } } private validateSequenceParams(params: CreateAnimationSequenceParams): void { if (!params.framework || !['react', 'js', 'vue'].includes(params.framework)) { throw createToolError('create_animation_sequence', 'Invalid framework specified'); } if (!params.sequence || params.sequence.length === 0) { throw createToolError('create_animation_sequence', 'Animation sequence cannot be empty'); } for (const step of params.sequence) { if (!step.element || !step.animate) { throw createToolError('create_animation_sequence', 'Each sequence step must have element and animate properties'); } } } private async buildComponentFromPatterns(params: GenerateMotionComponentParams): Promise<string> { // Combine multiple animation patterns into a single component if (params.animations.length === 1) { // Single animation - use direct pattern code return this.patterns.getPatternCode(params.animations[0], params.framework, params.componentName); } // Multiple animations - merge configurations const patternConfigs = params.animations.map(animationId => { const pattern = this.patterns.getPattern(animationId); return pattern?.config || {}; }); // Merge configs intelligently const mergedConfig = this.mergeAnimationConfigs(patternConfigs); // Generate combined component return this.generateCombinedComponent(mergedConfig, params); } private mergeAnimationConfigs(configs: any[]): any { const merged = { initial: {}, animate: {}, transition: { duration: 0.3, ease: 'easeOut' } }; configs.forEach(config => { if (config.initial) { Object.assign(merged.initial, config.initial); } if (config.animate) { Object.assign(merged.animate, config.animate); } if (config.transition) { // Use the longest duration and most complex transition if (config.transition.duration > merged.transition.duration) { merged.transition = { ...merged.transition, ...config.transition }; } } }); return merged; } private generateCombinedComponent(config: any, params: GenerateMotionComponentParams): string { const { componentName = 'AnimatedComponent', framework } = params; if (framework === 'react') { return `import React from 'react'; import { motion } from 'framer-motion'; const ${componentName}: React.FC = () => { return ( <motion.div initial={${JSON.stringify(config.initial, null, 2)}} animate={${JSON.stringify(config.animate, null, 2)}} transition={${JSON.stringify(config.transition, null, 2)}} > {/* Your content here */} </motion.div> ); }; export default ${componentName};`; } if (framework === 'js') { return `import { animate } from 'motion'; // ${componentName} animation animate('.${componentName.toLowerCase()}', ${JSON.stringify(config.animate, null, 2)}, { ...${JSON.stringify(config.transition, null, 2)} });`; } if (framework === 'vue') { return `<template> <Motion tag="div" :initial="${JSON.stringify(config.initial)}" :animate="${JSON.stringify(config.animate)}" :transition="${JSON.stringify(config.transition)}" > <!-- Your content here --> </Motion> </template> <script setup> import { Motion } from '@motionone/vue'; </script>`; } return this.patterns.getPatternCode(params.animations[0], framework, componentName); } // TODO: Fix optimization pipeline that was causing empty code generation /* private async generateOptimizedCode( code: string, params: GenerateMotionComponentParams ): Promise<{ code: string; imports: string[]; dependencies?: string[] }> { // Parse and optimize the generated code const ast = this.parser.parse(code, { framework: params.framework, typescript: params.typescript || true }); const context = { framework: params.framework, typescript: params.typescript || true, componentName: params.componentName, imports: new Set<string>(), dependencies: new Set<string>(), styleSystem: params.styleSystem, optimization: { performance: true, accessibility: true, bundleSize: true } }; const transformedAST = this.transformer.transform(ast, context); const result = await this.generator.generate(transformedAST, context); return { code: result.code, imports: result.imports, dependencies: Array.from(context.dependencies) }; } */ private async buildAnimationSequence(params: CreateAnimationSequenceParams): Promise<string> { switch (params.framework) { case 'react': return this.buildReactSequence(params); case 'vue': return this.buildVueSequence(params); case 'js': return this.buildJavaScriptSequence(params); default: throw createToolError('create_animation_sequence', `Unsupported framework: ${params.framework}`); } } private buildReactSequence(params: CreateAnimationSequenceParams): string { const staggerConfig = params.stagger ? { delayChildren: 0.1, staggerChildren: 0.1 } : {}; return `import React from 'react'; import { motion } from 'framer-motion'; const AnimationSequence: React.FC = () => { const containerVariants = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: ${JSON.stringify(staggerConfig, null, 6)} } }; const itemVariants = { hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0 } }; return ( <motion.div variants={containerVariants} initial="hidden" animate="visible" > ${params.sequence.map((step, index) => ` <motion.${step.element} variants={itemVariants} transition={{ duration: ${step.duration || 0.3}, delay: ${step.delay || 0} }} > Sequence item ${index + 1} </motion.${step.element}>`).join('')} </motion.div> ); }; export default AnimationSequence;`; } private buildVueSequence(params: CreateAnimationSequenceParams): string { // Simplified Vue sequence implementation return `<template> <div v-motion-initial="{ opacity: 0 }" v-motion-enter="{ opacity: 1 }"> ${params.sequence.map((step, index) => ` <${step.element} v-motion-initial="{ opacity: 0, y: 20 }" v-motion-enter="{ opacity: 1, y: 0, transition: { delay: ${(step.delay || 0) + index * 0.1} } }" > Sequence item ${index + 1} </${step.element}>`).join('')} </div> </template> <script setup lang="ts"> import { MotionPlugin } from '@vueuse/motion'; </script>`; } private buildJavaScriptSequence(params: CreateAnimationSequenceParams): string { const animationCalls = params.sequence.map((step, index) => { const delay = (step.delay || 0) + (params.stagger ? index * 100 : 0); return ` setTimeout(() => { const element${index} = document.querySelector('${step.element}'); if (element${index}) { animate(element${index}, ${JSON.stringify(step.animate)}, { duration: ${step.duration || 0.3} }); } }, ${delay});`; }).join(''); return `import { animate } from 'motion'; function runAnimationSequence() {${animationCalls} } // Start the sequence runAnimationSequence();`; } private async optimizeSequenceCode(code: string, _params: CreateAnimationSequenceParams): Promise<string> { // Apply sequence-specific optimizations return code; } private async convertASTBetweenFrameworks(ast: any, _from: Framework, _to: Framework): Promise<any> { // Framework conversion logic would go here // This is a complex transformation that would convert React JSX to Vue templates, etc. return ast; } private getConversionWarnings(from: Framework, to: Framework): string[] { const warnings: string[] = []; if (from === 'react' && to === 'vue') { warnings.push('React hooks need to be converted to Vue Composition API'); warnings.push('JSX syntax converted to Vue template syntax - manual review recommended'); } if (from === 'vue' && to === 'react') { warnings.push('Vue directives converted to React props - functionality may differ'); warnings.push('Vue template syntax converted to JSX - manual review recommended'); } return warnings; } private async performValidation( motionElements: any[], params: ValidateMotionSyntaxParams ): Promise<{ errors: Array<{ type: 'error' | 'warning'; message: string; line?: number; rule: string }>; suggestions: Array<{ type: 'performance' | 'accessibility' | 'best-practice'; message: string; priority: 'high' | 'medium' | 'low' }>; }> { const errors: any[] = []; const suggestions: any[] = []; // Validate motion elements for (const element of motionElements) { // Check for invalid property names if (element.props.animate && typeof element.props.animate === 'object') { const animateProps = element.props.animate; // Comprehensive property validation const validAnimationProps = new Set([ 'x', 'y', 'z', 'scale', 'scaleX', 'scaleY', 'rotate', 'rotateX', 'rotateY', 'rotateZ', 'opacity', 'backgroundColor', 'color', 'borderRadius', 'borderColor', 'borderWidth', 'width', 'height', 'top', 'left', 'right', 'bottom', 'margin', 'padding', 'transform', 'filter', 'clipPath', 'pathLength', 'pathOffset', 'pathSpacing', // SVG properties 'fill', 'stroke', 'strokeWidth', 'strokeDasharray', 'strokeDashoffset', 'd', 'cx', 'cy', 'r', 'rx', 'ry', 'x1', 'y1', 'x2', 'y2' ]); for (const prop in animateProps) { if (!validAnimationProps.has(prop)) { errors.push({ type: 'error', message: `Unknown animation property: '${prop}'. Valid properties include: x, y, scale, rotate, opacity, backgroundColor, etc.`, line: element.line, rule: 'invalid-animation-property' }); } } // Check for layout-triggering properties if (animateProps.width || animateProps.height) { errors.push({ type: 'warning', message: 'Animating width/height can cause layout thrashing. Consider using scale instead.', line: element.line, rule: 'avoid-layout-animations' }); } // Check for transform properties (good) if (animateProps.x || animateProps.y || animateProps.scale || animateProps.rotate) { suggestions.push({ type: 'performance', message: 'Good use of transform properties for smooth animations', priority: 'low' }); } // Validate transition values if (animateProps.transition && typeof animateProps.transition === 'object') { const transition = animateProps.transition; if (transition.duration && (transition.duration < 0 || transition.duration > 10)) { errors.push({ type: 'warning', message: 'Animation duration should be between 0 and 10 seconds', line: element.line, rule: 'invalid-duration' }); } } } // Check for accessibility if (element.props.whileHover && !element.props['aria-label']) { suggestions.push({ type: 'accessibility', message: 'Interactive elements should have aria-label for accessibility', priority: 'medium' }); } } // Framework-specific validation this.validateFrameworkSpecific(params.framework, params.code, errors, suggestions); return { errors, suggestions }; } private validateFrameworkSpecific(framework: Framework, code: string, _errors: any[], suggestions: any[]): void { if (framework === 'react') { // Check for common React-specific issues if (code.includes('motion.') && !code.includes('import { motion }')) { suggestions.push({ type: 'best-practice', message: 'Make sure to import motion from framer-motion: import { motion } from "framer-motion"', priority: 'high' }); } } else if (framework === 'js') { // Check for common JS-specific issues if (code.includes('animate(') && !code.includes('import { animate }')) { suggestions.push({ type: 'best-practice', message: 'Make sure to import animate from motion: import { animate } from "motion"', priority: 'high' }); } } else if (framework === 'vue') { // Check for common Vue-specific issues if (code.includes('<Motion') && !code.includes('import { Motion }')) { suggestions.push({ type: 'best-practice', message: 'Make sure to import Motion from @motionone/vue: import { Motion } from "@motionone/vue"', priority: 'high' }); } } } // private mergeAnimationConfigs(configs: any[]): any { // Reserved for future use // // Intelligently merge multiple animation configurations // const merged: any = {}; // // for (const config of configs) { // Object.assign(merged, config); // } // // return merged; // } private determineComplexity(animations: string[]): 'basic' | 'intermediate' | 'advanced' { if (animations.length === 1) return 'basic'; if (animations.length <= 3) return 'intermediate'; return 'advanced'; } private async analyzePerformance(code: string, _framework: Framework): Promise<{ score: number; suggestions: string[]; }> { // Simplified performance analysis let score = 100; const suggestions: string[] = []; // Check for layout-triggering properties if (code.includes('width:') || code.includes('height:')) { score -= 20; suggestions.push('Use transform properties instead of width/height for better performance'); } // Check for will-change usage if (code.includes('will-change')) { suggestions.push('Ensure will-change is removed after animation completes'); } return { score: Math.max(0, score), suggestions }; } }

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