Skip to main content
Glama
__init__.py9.01 kB
# chuk-motion/src/chuk_motion/components/__init__.py """Component system for chuk-motion. Auto-discovers and loads all components from their modular folders. Each component is self-contained with its own Pydantic models. """ import importlib import logging from pathlib import Path from .base import ComponentInfo logger = logging.getLogger(__name__) def discover_components() -> dict[str, ComponentInfo]: """ Discover all components by walking the components directory structure. Returns: Dictionary mapping component names to their ComponentInfo """ components: dict[str, ComponentInfo] = {} components_dir = Path(__file__).parent # Walk through category folders (charts, overlays, etc.) for category_path in components_dir.iterdir(): if not category_path.is_dir() or category_path.name.startswith("_"): continue if category_path.name in ("__pycache__",): continue category_name = category_path.name # Walk through component folders within each category for component_path in category_path.iterdir(): if not component_path.is_dir() or component_path.name.startswith("_"): continue if component_path.name in ("__pycache__",): continue component_name = component_path.name # Check if component has required files init_file = component_path / "__init__.py" template_file = component_path / "template.tsx.j2" if not init_file.exists(): continue try: # Import the component module module_path = f"chuk_motion.components.{category_name}.{component_name}" module = importlib.import_module(module_path) # Get component metadata - try schema.py first (new refactored components) metadata = getattr(module, "METADATA", None) if not metadata: # Try importing from schema.py (new component structure) try: schema_module_path = f"{module_path}.schema" schema_module = importlib.import_module(schema_module_path) metadata = getattr(schema_module, "METADATA", None) except ImportError: pass # Skip components without metadata if not metadata: logger.debug(f"Component {component_name} has no METADATA, skipping") continue # Get register_tool function - try tool.py if not in __init__.py register_tool = getattr(module, "register_tool", None) if not register_tool: try: tool_module_path = f"{module_path}.tool" tool_module = importlib.import_module(tool_module_path) register_tool = getattr(tool_module, "register_tool", None) except ImportError: pass # Get add_to_composition function - try builder.py if not in __init__.py add_to_composition = getattr(module, "add_to_composition", None) if not add_to_composition: try: builder_module_path = f"{module_path}.builder" builder_module = importlib.import_module(builder_module_path) add_to_composition = getattr(builder_module, "add_to_composition", None) except ImportError: pass # Create ComponentInfo component_info = ComponentInfo( metadata=metadata, template_path=template_file if template_file.exists() else None, register_tool=register_tool, add_to_composition=add_to_composition, directory_name=category_name, # Store actual directory name ) components[component_name] = component_info except Exception as e: logger.warning(f"Failed to load component {component_name}: {e}", exc_info=True) continue return components def get_component_registry() -> dict[str, dict]: """ Get the component registry (MCP schemas) for MCP tools list. Returns: Dictionary mapping component names to their MCP schemas """ components = discover_components() registry = {} for name, comp_info in components.items(): try: # Import module to get MCP_SCHEMA # Use directory_name (actual folder) not category (metadata field) directory = comp_info.directory_name if not directory: continue module_path = f"chuk_motion.components.{directory}.{name}" module = importlib.import_module(module_path) mcp_schema = getattr(module, "MCP_SCHEMA", None) # Try schema.py if not found in __init__.py (new component structure) if not mcp_schema: try: schema_module_path = f"{module_path}.schema" schema_module = importlib.import_module(schema_module_path) mcp_schema = getattr(schema_module, "MCP_SCHEMA", None) except ImportError: pass if mcp_schema: registry[name] = mcp_schema except Exception as e: logger.warning(f"Could not get MCP schema for {name}: {e}") return registry def register_all_tools(mcp, project_manager): """ Register all component tools with the MCP server. Args: mcp: ChukMCPServer instance project_manager: ProjectManager instance """ components = discover_components() registered_count = 0 for name, comp_info in components.items(): if comp_info.register_tool: try: comp_info.register_tool(mcp, project_manager) registered_count += 1 except Exception as e: logger.warning(f"Failed to register tool for {name}: {e}", exc_info=True) logger.debug(f"Registered {registered_count} component tools") def register_all_builders(composition_builder_class): """ Register all composition builder methods dynamically. Args: composition_builder_class: CompositionBuilder class to add methods to """ components = discover_components() for name, comp_info in components.items(): if comp_info.add_to_composition: # Create a method name like "add_line_chart" method_name = f"add_{_camel_to_snake(name)}" # Create a wrapper function that calls the component's builder def make_method(component_builder, m_name=method_name, c_name=name): def method(self, *args, **kwargs): return component_builder(self, *args, **kwargs) method.__name__ = m_name method.__doc__ = f"Add {c_name} component to composition" return method # Add the method to the class setattr( composition_builder_class, method_name, make_method(comp_info.add_to_composition) ) def register_all_renderers(composition_builder_class): """ Register all component-specific JSX renderers dynamically. Args: composition_builder_class: CompositionBuilder class to register renderers with """ components = discover_components() registered_count = 0 for name, comp_info in components.items(): # Try to import the component's renderer module try: directory = comp_info.directory_name if not directory: continue module_path = f"chuk_motion.components.{directory}.{name}.renderer" module = importlib.import_module(module_path) # Get the render_jsx function render_jsx = getattr(module, "render_jsx", None) if render_jsx: composition_builder_class._component_renderers[name] = render_jsx registered_count += 1 except (ImportError, AttributeError): # No renderer for this component - that's okay, not all need custom rendering pass if registered_count > 0: logger.debug(f"Registered {registered_count} custom component renderers") def _camel_to_snake(name: str) -> str: """Convert CamelCase to snake_case.""" import re s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() # Export functions __all__ = [ "ComponentInfo", "discover_components", "get_component_registry", "register_all_tools", "register_all_builders", "register_all_renderers", ]

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