Skip to main content
Glama
server.py19.2 kB
#!/usr/bin/env python3 """ Remotion MCP Server - AI-powered video generation with design system approach This server provides MCP tools for creating Remotion video compositions using a design-system-first approach inspired by shadcn/ui and chuk-mcp-pptx. """ import asyncio import json import logging import os import sys from chuk_mcp_server import ChukMCPServer from chuk_virtual_fs import AsyncVirtualFileSystem # Import component auto-discovery system from .components import ( get_component_registry, register_all_builders, register_all_renderers, register_all_tools, ) from .generator.composition_builder import CompositionBuilder from .themes.youtube_themes import YOUTUBE_THEMES # Import design system modules from .tools.theme_tools import register_theme_tools from .tools.token_tools import register_token_tools from .utils.project_manager import ProjectManager logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Create the MCP server instance mcp = ChukMCPServer("chuk-motion") # Create virtual filesystem instance (using file provider for actual file operations) vfs = AsyncVirtualFileSystem(provider="file") # Create project manager instance project_manager = ProjectManager() # Register composition builder methods dynamically from components register_all_builders(CompositionBuilder) register_all_renderers(CompositionBuilder) # Register theme and token tools with virtual filesystem register_theme_tools(mcp, project_manager, vfs) register_token_tools(mcp, project_manager, vfs) # Register all component tools automatically register_all_tools(mcp, project_manager) # Get component registry for discovery tools COMPONENT_REGISTRY = get_component_registry() # ============================================================================ # DISCOVERY TOOLS - Help LLMs explore the design system # ============================================================================ @mcp.tool # type: ignore[arg-type] async def remotion_list_components(category: str | None = None) -> str: """ List available Remotion video components with their schemas. Returns all available components organized by category. Each component includes its variants, properties, and usage examples. This helps LLMs discover what building blocks are available for video creation. Args: category: Optional category filter (scene, overlay, animation, chart, layout) If not specified, returns all categories Returns: JSON object with component definitions organized by category Example: components = await remotion_list_components() # Returns all available components overlay_components = await remotion_list_components(category="overlay") # Returns only overlay components (lower thirds, captions, etc.) """ def _list(): if category: filtered = { name: comp for name, comp in COMPONENT_REGISTRY.items() if comp.get("category") == category } return json.dumps(filtered, indent=2) return json.dumps(COMPONENT_REGISTRY, indent=2) return await asyncio.get_event_loop().run_in_executor(None, _list) @mcp.tool # type: ignore[arg-type] async def remotion_search_components(query: str) -> str: """ Search for components by name or description. Performs a case-insensitive search across component names and descriptions. Useful when you know what you want but not the exact component name. Args: query: Search term to match against component names and descriptions Returns: JSON object with matching components and their details Example: results = await remotion_search_components(query="text") # Returns all components with "text" in name or description # (TitleScene, TextOverlay, TextAnimation, etc.) """ def _search(): query_lower = query.lower() results = {} for name, comp in COMPONENT_REGISTRY.items(): # Search in component name if query_lower in name.lower(): results[name] = comp continue # Search in description if query_lower in comp.get("description", "").lower(): results[name] = comp continue # Search in category if query_lower in comp.get("category", "").lower(): results[name] = comp return json.dumps(results, indent=2) return await asyncio.get_event_loop().run_in_executor(None, _search) @mcp.tool # type: ignore[arg-type] async def remotion_get_component_schema(component_name: str) -> str: """ Get detailed schema for a specific component. Returns the complete schema including all properties, variants, animations, and usage examples for a single component. Args: component_name: Name of the component (e.g., "LowerThird", "TitleScene") Returns: JSON object with component schema and examples Example: schema = await remotion_get_component_schema(component_name="LowerThird") # Returns full schema for lower third component including all variants """ def _get_schema(): if component_name not in COMPONENT_REGISTRY: return json.dumps({"error": f"Component '{component_name}' not found"}) return json.dumps(COMPONENT_REGISTRY[component_name], indent=2) return await asyncio.get_event_loop().run_in_executor(None, _get_schema) # Note: Theme tools (remotion_list_themes, remotion_get_theme_info, etc.) # are now registered via register_theme_tools() above # Note: Token tools (remotion_list_color_tokens, remotion_list_typography_tokens, # remotion_list_motion_tokens, etc.) are now registered via register_token_tools() above # ============================================================================ # PROJECT CREATION & GENERATION TOOLS # ============================================================================ # Note: Component-specific tools (remotion_add_title_scene, remotion_add_chart, etc.) # are now automatically registered via register_all_tools() from the components module @mcp.tool # type: ignore[arg-type] async def remotion_create_project( name: str, theme: str = "tech", fps: int = 30, width: int = 1920, height: int = 1080 ) -> str: """ Create a new Remotion video project. Creates a complete Remotion project with package.json, TypeScript config, and project structure ready for video generation. Args: name: Project name (will be used as directory name) theme: Theme to use (tech, finance, education, lifestyle, gaming, minimal, business) fps: Frames per second (default: 30) width: Video width in pixels (default: 1920 for 1080p) height: Video height in pixels (default: 1080 for 1080p) Returns: JSON with project information Example: project = await remotion_create_project( name="my_video", theme="tech", fps=30, width=1920, height=1080 ) """ def _create(): try: result = project_manager.create_project(name, theme, fps, width, height) return json.dumps(result, indent=2) except Exception as e: return json.dumps({"error": str(e)}) return await asyncio.get_event_loop().run_in_executor(None, _create) @mcp.tool # type: ignore[arg-type] async def remotion_generate_video() -> str: """ Generate the complete video composition and write all files. Generates all TSX components, the composition file, and updates the project with the complete video structure. After this, you can run 'npm install' and 'npm start' in the project directory to preview the video. Returns: JSON with generation results and next steps Example: result = await remotion_generate_video() # Video files generated! Run 'npm install' and 'npm start' to preview """ def _generate(): if not project_manager.current_project: return json.dumps({"error": "No active project. Create a project first."}) if not project_manager.current_timeline: return json.dumps({"error": "No timeline created. Add components first."}) try: # Generate components theme = project_manager.current_timeline.theme # Helper to recursively find all component types including nested ones def find_all_component_types(components): types = set() from chuk_motion.generator.composition_builder import ComponentInstance def collect_types(comp): if isinstance(comp, ComponentInstance): types.add(comp.component_type) # Check for nested children in props for _key, value in comp.props.items(): if isinstance(value, ComponentInstance): collect_types(value) elif isinstance(value, list): for item in value: if isinstance(item, ComponentInstance): collect_types(item) for comp in components: collect_types(comp) return types # Get unique component types from all tracks (including nested) all_components = project_manager.current_timeline.get_all_components() component_types = find_all_component_types(all_components) generated_files = [] for comp_type in component_types: # Get a sample config from the timeline # For nested components, use empty config as templates handle it file_path = project_manager.add_component_to_project(comp_type, {}, theme) generated_files.append(file_path) # Generate main composition composition_file = project_manager.generate_composition() generated_files.append(composition_file) project_info = project_manager.get_project_info() return json.dumps( { "status": "success", "project": project_info, "generated_files": generated_files, "next_steps": [ f"cd {project_info['path']}", "npm install", "npm start # Opens Remotion Studio", "npm run build # Renders the video", ], }, indent=2, ) except Exception as e: return json.dumps({"error": str(e)}) return await asyncio.get_event_loop().run_in_executor(None, _generate) @mcp.tool # type: ignore[arg-type] async def remotion_get_composition_info() -> str: """ Get information about the current composition. Returns details about the current composition including all components, timeline, duration, and configuration. Returns: JSON with composition information Example: info = await remotion_get_composition_info() # Returns composition details, components, timeline, etc. """ def _get(): if not project_manager.current_composition: return json.dumps({"error": "No active composition"}) return json.dumps(project_manager.get_project_info(), indent=2) return await asyncio.get_event_loop().run_in_executor(None, _get) @mcp.tool # type: ignore[arg-type] async def remotion_list_projects() -> str: """ List all Remotion projects in the workspace. Returns: JSON array of projects Example: projects = await remotion_list_projects() """ def _list(): projects = project_manager.list_projects() return json.dumps(projects, indent=2) return await asyncio.get_event_loop().run_in_executor(None, _list) # ============================================================================ # TRACK MANAGEMENT TOOLS # ============================================================================ @mcp.tool # type: ignore[arg-type] async def remotion_add_track( name: str, layer: int, default_gap: float = 0, description: str = "" ) -> str: """ Add a new track to the timeline. Args: name: Track name (unique identifier) layer: Z-index for rendering (higher = on top) default_gap: Default gap between components in seconds description: Human-readable description Returns: JSON with track information Example: result = await remotion_add_track( name="subtitles", layer=15, default_gap=0, description="Subtitle overlays" ) """ def _add(): if not project_manager.current_timeline: return json.dumps({"error": "No active project. Create a project first."}) try: project_manager.current_timeline.add_track(name, layer, default_gap, description) return json.dumps( { "status": "success", "track": {"name": name, "layer": layer, "default_gap": default_gap}, }, indent=2, ) except Exception as e: return json.dumps({"error": str(e)}) return await asyncio.get_event_loop().run_in_executor(None, _add) @mcp.tool # type: ignore[arg-type] async def remotion_list_tracks() -> str: """ List all tracks in the timeline. Returns: JSON array of tracks with their properties Example: tracks = await remotion_list_tracks() # Returns tracks sorted by layer (highest first) """ def _list(): if not project_manager.current_timeline: return json.dumps({"error": "No active project"}) tracks = project_manager.current_timeline.list_tracks() return json.dumps(tracks, indent=2) return await asyncio.get_event_loop().run_in_executor(None, _list) @mcp.tool # type: ignore[arg-type] async def remotion_set_active_track(name: str) -> str: """ Set the default track for component additions. Args: name: Track name to set as active Returns: JSON with status Example: result = await remotion_set_active_track(name="overlay") # Subsequent component additions will use the overlay track by default """ def _set(): if not project_manager.current_timeline: return json.dumps({"error": "No active project"}) try: project_manager.current_timeline.set_active_track(name) return json.dumps({"status": "success", "active_track": name}, indent=2) except Exception as e: return json.dumps({"error": str(e)}) return await asyncio.get_event_loop().run_in_executor(None, _set) @mcp.tool # type: ignore[arg-type] async def remotion_get_track_cursor(track_name: str) -> str: """ Get the current cursor position for a track. Args: track_name: Track name Returns: JSON with cursor position in frames and seconds Example: cursor = await remotion_get_track_cursor(track_name="main") """ def _get(): if not project_manager.current_timeline: return json.dumps({"error": "No active project"}) try: cursor_frames = project_manager.current_timeline.get_track_cursor(track_name) cursor_seconds = project_manager.current_timeline.frames_to_seconds(cursor_frames) return json.dumps( { "track": track_name, "cursor_frames": cursor_frames, "cursor_seconds": cursor_seconds, }, indent=2, ) except Exception as e: return json.dumps({"error": str(e)}) return await asyncio.get_event_loop().run_in_executor(None, _get) # ============================================================================ # INFO TOOLS # ============================================================================ @mcp.tool # type: ignore[arg-type] async def remotion_get_info() -> str: """ Get information about the Remotion MCP Server. Returns server version, capabilities, and statistics about available components, themes, and tools. Returns: JSON object with server information Example: info = await remotion_get_info() # Returns server version, component count, theme count, etc. """ def _get_info(): info = { "name": "chuk-motion", "version": "0.1.0", "description": "AI-powered video generation with design system approach", "statistics": { "components": len(COMPONENT_REGISTRY), "themes": len(YOUTUBE_THEMES), "categories": len({c.get("category") for c in COMPONENT_REGISTRY.values()}), }, "categories": list({c.get("category") for c in COMPONENT_REGISTRY.values()}), } return json.dumps(info, indent=2) return await asyncio.get_event_loop().run_in_executor(None, _get_info) def main(): """Main entry point for the MCP server. Automatically detects transport mode: - stdio: When stdin is piped or MCP_STDIO is set (for Claude Desktop) - HTTP: Default mode for API access """ import argparse parser = argparse.ArgumentParser(description="Remotion MCP Server") parser.add_argument( "mode", nargs="?", choices=["stdio", "http"], default=None, help="Transport mode (stdio for Claude Desktop, http for API)", ) parser.add_argument( "--host", default="localhost", help="Host for HTTP mode (default: localhost)" ) parser.add_argument("--port", type=int, default=8000, help="Port for HTTP mode (default: 8000)") args = parser.parse_args() # Determine transport mode if args.mode == "stdio": # Explicitly requested stdio mode logger.debug("Remotion MCP Server starting in STDIO mode") mcp.run(stdio=True) elif args.mode == "http": # Explicitly requested HTTP mode logger.info(f"Remotion MCP Server starting in HTTP mode on {args.host}:{args.port}") mcp.run(host=args.host, port=args.port, stdio=False) else: # Auto-detect mode based on environment if os.environ.get("MCP_STDIO") or (not sys.stdin.isatty()): logger.debug("Remotion MCP Server starting in STDIO mode (auto-detected)") mcp.run(stdio=True) else: logger.info(f"Remotion MCP Server starting in HTTP mode on {args.host}:{args.port}") mcp.run(host=args.host, port=args.port, stdio=False) if __name__ == "__main__": main()

Implementation Reference

Latest Blog Posts

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/chrishayuk/chuk-mcp-remotion'

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