""" Storybook Integration for MCP Provides Storybook tools for scanning, generating stories, creating themes, and configuration. """ from typing import Dict, Any, Optional, List from pathlib import Path from mcp import types from .base import BaseIntegration from ..context.project_context import get_context_manager # Storybook MCP Tool Definitions STORYBOOK_TOOLS = [ types.Tool( name="storybook_scan", description="Scan project for existing Storybook configuration and stories. Returns story inventory, configuration details, and coverage statistics.", inputSchema={ "type": "object", "properties": { "project_id": { "type": "string", "description": "Project ID" }, "path": { "type": "string", "description": "Optional: Specific path to scan (defaults to project root)" } }, "required": ["project_id"] } ), types.Tool( name="storybook_generate_stories", description="Generate Storybook stories for React components. Supports CSF3, CSF2, and MDX formats with automatic prop detection.", inputSchema={ "type": "object", "properties": { "project_id": { "type": "string", "description": "Project ID" }, "component_path": { "type": "string", "description": "Path to component file or directory" }, "template": { "type": "string", "description": "Story format template", "enum": ["csf3", "csf2", "mdx"], "default": "csf3" }, "include_variants": { "type": "boolean", "description": "Generate variant stories (default: true)", "default": True }, "dry_run": { "type": "boolean", "description": "Preview without writing files (default: true)", "default": True } }, "required": ["project_id", "component_path"] } ), types.Tool( name="storybook_generate_theme", description="Generate Storybook theme configuration from design tokens. Creates manager.ts, preview.ts, and theme files.", inputSchema={ "type": "object", "properties": { "project_id": { "type": "string", "description": "Project ID" }, "brand_title": { "type": "string", "description": "Brand title for Storybook UI", "default": "Design System" }, "base_theme": { "type": "string", "description": "Base theme (light or dark)", "enum": ["light", "dark"], "default": "light" }, "output_dir": { "type": "string", "description": "Output directory (default: .storybook)" }, "write_files": { "type": "boolean", "description": "Write files to disk (default: false - preview only)", "default": False } }, "required": ["project_id"] } ), types.Tool( name="storybook_get_status", description="Get Storybook installation and configuration status for a project.", inputSchema={ "type": "object", "properties": { "project_id": { "type": "string", "description": "Project ID" } }, "required": ["project_id"] } ), types.Tool( name="storybook_configure", description="Configure or update Storybook for a project with DSS integration.", inputSchema={ "type": "object", "properties": { "project_id": { "type": "string", "description": "Project ID" }, "action": { "type": "string", "description": "Configuration action", "enum": ["init", "update", "add_theme"], "default": "init" }, "options": { "type": "object", "description": "Configuration options", "properties": { "framework": { "type": "string", "enum": ["react", "vue", "angular"] }, "builder": { "type": "string", "enum": ["vite", "webpack5"] }, "typescript": { "type": "boolean" } } } }, "required": ["project_id"] } ) ] class StorybookIntegration(BaseIntegration): """Storybook integration wrapper for DSS tools""" def __init__(self, config: Optional[Dict[str, Any]] = None): """ Initialize Storybook integration. Args: config: Optional Storybook configuration """ super().__init__("storybook", config or {}) self.context_manager = get_context_manager() async def _get_project_path(self, project_id: str) -> Path: """ Get project path from context manager. Args: project_id: Project ID Returns: Project path as Path object """ context = await self.context_manager.get_context(project_id) if not context or not context.path: raise ValueError(f"Project not found: {project_id}") return Path(context.path) async def scan_storybook(self, project_id: str, path: Optional[str] = None) -> Dict[str, Any]: """ Scan for Storybook config and stories. Args: project_id: Project ID path: Optional specific path to scan Returns: Storybook scan results """ try: from dss.storybook.scanner import StorybookScanner project_path = await self._get_project_path(project_id) # Ensure path is within project directory for security if path: scan_path = project_path / path # Validate path doesn't escape project directory if not scan_path.resolve().is_relative_to(project_path.resolve()): raise ValueError("Path must be within project directory") else: scan_path = project_path scanner = StorybookScanner(str(scan_path)) result = await scanner.scan() if hasattr(scanner.scan, '__await__') else scanner.scan() coverage = await scanner.get_story_coverage() if hasattr(scanner.get_story_coverage, '__await__') else scanner.get_story_coverage() return { "project_id": project_id, "path": str(scan_path), "config": result.get("config") if isinstance(result, dict) else None, "stories_count": result.get("stories_count", 0) if isinstance(result, dict) else 0, "components_with_stories": result.get("components_with_stories", []) if isinstance(result, dict) else [], "stories": result.get("stories", []) if isinstance(result, dict) else [], "coverage": coverage if coverage else {} } except Exception as e: return { "error": f"Failed to scan Storybook: {str(e)}", "project_id": project_id } async def generate_stories( self, project_id: str, component_path: str, template: str = "csf3", include_variants: bool = True, dry_run: bool = True ) -> Dict[str, Any]: """ Generate stories for components. Args: project_id: Project ID component_path: Path to component file or directory template: Story format (csf3, csf2, mdx) include_variants: Whether to generate variant stories dry_run: Preview without writing files Returns: Generation results """ try: from dss.storybook.generator import StoryGenerator project_path = await self._get_project_path(project_id) generator = StoryGenerator(str(project_path)) full_path = project_path / component_path # Check if path exists and is directory or file if not full_path.exists(): return { "error": f"Path not found: {component_path}", "project_id": project_id } if full_path.is_dir(): # Generate for directory func = generator.generate_stories_for_directory if hasattr(func, '__await__'): results = await func( component_path, template=template.upper(), dry_run=dry_run ) else: results = func( component_path, template=template.upper(), dry_run=dry_run ) return { "project_id": project_id, "path": component_path, "generated_count": len([r for r in (results if isinstance(results, list) else []) if "story" in str(r)]), "results": results if isinstance(results, list) else [], "dry_run": dry_run, "template": template } else: # Generate for single file func = generator.generate_story if hasattr(func, '__await__'): story = await func( component_path, template=template.upper(), include_variants=include_variants ) else: story = func( component_path, template=template.upper(), include_variants=include_variants ) return { "project_id": project_id, "component": component_path, "story": story, "template": template, "include_variants": include_variants, "dry_run": dry_run } except Exception as e: return { "error": f"Failed to generate stories: {str(e)}", "project_id": project_id, "component_path": component_path } async def generate_theme( self, project_id: str, brand_title: str = "Design System", base_theme: str = "light", output_dir: Optional[str] = None, write_files: bool = False ) -> Dict[str, Any]: """ Generate Storybook theme from design tokens. Args: project_id: Project ID brand_title: Brand title for Storybook base_theme: Base theme (light or dark) output_dir: Output directory for theme files write_files: Write files to disk or preview only Returns: Theme generation results """ try: from dss.storybook.theme import ThemeGenerator from dss.themes import get_default_light_theme, get_default_dark_theme # Get project tokens from context context = await self.context_manager.get_context(project_id) if not context: return {"error": f"Project not found: {project_id}"} # Convert tokens to list format for ThemeGenerator tokens_list = [ {"name": name, "value": token.get("value") if isinstance(token, dict) else token} for name, token in (context.tokens.items() if hasattr(context, 'tokens') else {}.items()) ] generator = ThemeGenerator() if write_files and output_dir: # Generate and write files func = generator.generate_full_config if hasattr(func, '__await__'): files = await func( tokens=tokens_list, brand_title=brand_title, output_dir=output_dir ) else: files = func( tokens=tokens_list, brand_title=brand_title, output_dir=output_dir ) return { "project_id": project_id, "files_written": list(files.keys()) if isinstance(files, dict) else [], "output_dir": output_dir, "brand_title": brand_title } else: # Preview mode - generate file contents try: func = generator.generate_from_tokens if hasattr(func, '__await__'): theme = await func(tokens_list, brand_title, base_theme) else: theme = func(tokens_list, brand_title, base_theme) except Exception: # Fallback to default theme theme_obj = get_default_light_theme() if base_theme == "light" else get_default_dark_theme() theme = { "name": theme_obj.name if hasattr(theme_obj, 'name') else "Default", "colors": {} } # Generate theme file content theme_file = f"// Storybook theme for {brand_title}\nexport default {str(theme)};" manager_file = f"import addons from '@storybook/addons';\nimport theme from './dss-theme';\naddons.setConfig({{ theme }});" preview_file = f"import '../dss-theme';\nexport default {{ parameters: {{ actions: {{ argTypesRegex: '^on[A-Z].*' }} }} }};" return { "project_id": project_id, "preview": True, "brand_title": brand_title, "base_theme": base_theme, "files": { "dss-theme.ts": theme_file, "manager.ts": manager_file, "preview.ts": preview_file }, "token_count": len(tokens_list) } except Exception as e: return { "error": f"Failed to generate theme: {str(e)}", "project_id": project_id } async def get_status(self, project_id: str) -> Dict[str, Any]: """ Get Storybook installation and configuration status. Args: project_id: Project ID Returns: Storybook status information """ try: from dss.storybook.config import get_storybook_status project_path = await self._get_project_path(project_id) func = get_storybook_status if hasattr(func, '__await__'): status = await func(str(project_path)) else: status = func(str(project_path)) return { "project_id": project_id, "path": str(project_path), **(status if isinstance(status, dict) else {}) } except Exception as e: return { "error": f"Failed to get Storybook status: {str(e)}", "project_id": project_id, "installed": False } async def configure( self, project_id: str, action: str = "init", options: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: """ Configure or update Storybook for project. Args: project_id: Project ID action: Configuration action (init, update, add_theme) options: Configuration options Returns: Configuration results """ try: from dss.storybook.config import write_storybook_config_file project_path = await self._get_project_path(project_id) options = options or {} # Map action to configuration config = { "action": action, "framework": options.get("framework", "react"), "builder": options.get("builder", "vite"), "typescript": options.get("typescript", True) } func = write_storybook_config_file if hasattr(func, '__await__'): result = await func(str(project_path), config) else: result = func(str(project_path), config) return { "project_id": project_id, "action": action, "success": True, "path": str(project_path), "config_path": str(project_path / ".storybook"), "options": config } except Exception as e: return { "error": f"Failed to configure Storybook: {str(e)}", "project_id": project_id, "action": action, "success": False } class StorybookTools: """MCP tool executor for Storybook integration""" def __init__(self, config: Optional[Dict[str, Any]] = None): """ Args: config: Optional Storybook configuration """ self.storybook = StorybookIntegration(config) async def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]: """ Execute Storybook tool. Args: tool_name: Name of tool to execute arguments: Tool arguments Returns: Tool execution result """ handlers = { "storybook_scan": self.storybook.scan_storybook, "storybook_generate_stories": self.storybook.generate_stories, "storybook_generate_theme": self.storybook.generate_theme, "storybook_get_status": self.storybook.get_status, "storybook_configure": self.storybook.configure } handler = handlers.get(tool_name) if not handler: return {"error": f"Unknown Storybook tool: {tool_name}"} try: # Remove internal prefixes and execute clean_args = {k: v for k, v in arguments.items() if not k.startswith("_")} result = await handler(**clean_args) return result except Exception as e: return {"error": f"Tool execution failed: {str(e)}", "tool": tool_name}