#!/usr/bin/env python3 """ DSS MCP Server MCP (Model Context Protocol) interface for DSS. Exposes design system operations as tools for AI agents. Tool Categories (32 tools): - Status & Discovery (4): get_status, list_projects, create_project, get_project - Token Ingestion (7): ingest_css, ingest_scss, ingest_tailwind, ingest_json, merge, export, validate - Analysis (11): discover_project, analyze_react, find_inline_styles, find_patterns, etc. - Storybook (5): scan, generate_story, generate_batch, theme, coverage - Utilities (5): extract_tokens, extract_components, sync_tokens, etc. """ import os import json import logging import sys from pathlib import Path from typing import Optional # Add parent to path for imports sys.path.insert(0, str(Path(__file__).parent.parent)) from mcp.server.fastmcp import FastMCP from config import config from storage.json_store import Projects, Components, SyncHistory, ActivityLog, get_stats from figma.figma_tools import FigmaToolSuite # Import new ingestion modules from ingest import ( DesignToken, TokenCollection, TokenMerger, MergeStrategy, CSSTokenSource, SCSSTokenSource, TailwindTokenSource, JSONTokenSource ) # Import analysis modules from analyze import ( ProjectScanner, ReactAnalyzer, StyleAnalyzer, DependencyGraph, QuickWinFinder, ProjectAnalysis, QuickWin, QuickWinType, QuickWinPriority ) # Import Storybook modules from storybook import ( StorybookScanner, StoryGenerator, ThemeGenerator, StoryInfo, StorybookConfig, StorybookTheme, StoryTemplate ) logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s") logger = logging.getLogger("dss-mcp") # Initialize Figma client FIGMA_TOKEN = os.environ.get("FIGMA_TOKEN", "") figma = FigmaToolSuite(FIGMA_TOKEN) if FIGMA_TOKEN else None # Create MCP server - configurable host/port for remote connections MCP_HOST = os.environ.get("DSS_MCP_HOST", "127.0.0.1") MCP_PORT = int(os.environ.get("DSS_MCP_PORT", "3457")) mcp = FastMCP("dss-design-system", host=MCP_HOST, port=MCP_PORT) @mcp.tool() async def get_status() -> str: """Get DSS server status and statistics.""" stats = get_stats() figma_configured = bool(FIGMA_TOKEN) return json.dumps({ "status": "ready", "figma": "connected" if figma_configured else "mock mode", "mode": "live" if figma_configured else "mock", "statistics": stats, "version": "0.8.0" }, indent=2) @mcp.tool() async def list_projects() -> str: """List all registered projects.""" projects = Projects.list_all() return json.dumps([p.to_dict() for p in projects], indent=2) @mcp.tool() async def create_project(name: str, description: str = "", figma_file_key: str = "") -> str: """ Create a new design system project. Args: name: Project name description: Project description figma_file_key: Figma file key for design source """ project = Projects.create(name, description, figma_file_key) return json.dumps({"success": True, "project": project.to_dict()}, indent=2) @mcp.tool() async def get_project(project_id: str) -> str: """ Get details of a specific project. Args: project_id: The project ID """ project = Projects.get(project_id) if not project: return json.dumps({"success": False, "error": "Project not found"}) return json.dumps({"success": True, "project": project.to_dict()}, indent=2) @mcp.tool() async def extract_tokens(file_key: str, format: str = "json") -> str: """ Extract design tokens from a Figma file. Args: file_key: The Figma file key (from the URL, e.g., abc123XYZ from figma.com/file/abc123XYZ/...) format: Output format - json, css, scss, or ts (default: json) Returns: JSON with extracted tokens and formatted output """ if not figma: # Mock mode - return sample tokens mock_tokens = [ {"name": "color-primary", "value": "#3B82F6", "type": "color", "category": "colors"}, {"name": "color-secondary", "value": "#10B981", "type": "color", "category": "colors"}, {"name": "spacing-sm", "value": "8px", "type": "dimension", "category": "spacing"}, {"name": "spacing-md", "value": "16px", "type": "dimension", "category": "spacing"}, {"name": "font-size-base", "value": "16px", "type": "dimension", "category": "typography"}, ] formatted = format_tokens_output(mock_tokens, format) return json.dumps({ "success": True, "mode": "mock", "tokens_count": len(mock_tokens), "format": format, "tokens": mock_tokens, "formatted_output": formatted }, indent=2) try: result = await figma.extract_variables(file_key) tokens = result.get("tokens", []) formatted = format_tokens_output(tokens, format) return json.dumps({ "success": True, "mode": "live", "tokens_count": len(tokens), "format": format, "tokens": tokens[:20], # First 20 for preview "formatted_output": formatted[:3000] if len(formatted) > 3000 else formatted }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def extract_components(file_key: str) -> str: """ Extract component definitions from a Figma file. Args: file_key: The Figma file key (from the URL) Returns: JSON with extracted components including name, variants, and properties """ if not figma: # Mock mode mock_components = [ {"name": "Button", "key": "btn-1", "description": "Primary button component", "variants": ["primary", "secondary", "ghost"]}, {"name": "Input", "key": "inp-1", "description": "Text input field", "variants": ["default", "error", "disabled"]}, {"name": "Card", "key": "card-1", "description": "Content card container", "variants": ["elevated", "outlined"]}, ] return json.dumps({ "success": True, "mode": "mock", "components_count": len(mock_components), "components": mock_components }, indent=2) try: result = await figma.extract_components(file_key) components = result.get("components", []) return json.dumps({ "success": True, "mode": "live", "components_count": len(components), "components": components }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def generate_component_code( component_name: str, framework: str = "react", tokens: str = "" ) -> str: """ Generate code for a UI component. Args: component_name: Name of the component (e.g., Button, Card, Input) framework: Target framework - react, vue, svelte, or webcomponent (default: react) tokens: Optional JSON string of design tokens to use Returns: Generated component code in the specified framework """ # Parse tokens if provided token_vars = {} if tokens: try: token_list = json.loads(tokens) for t in token_list: token_vars[t["name"]] = t["value"] except (json.JSONDecodeError, KeyError, TypeError) as e: # Invalid token format, continue with empty token_vars pass # Generate code based on framework if framework == "react": code = generate_react_component(component_name, token_vars) elif framework == "vue": code = generate_vue_component(component_name, token_vars) elif framework == "svelte": code = generate_svelte_component(component_name, token_vars) else: code = generate_webcomponent(component_name, token_vars) return json.dumps({ "success": True, "component": component_name, "framework": framework, "code": code }, indent=2) @mcp.tool() async def sync_tokens_to_file( file_key: str, output_path: str, format: str = "css" ) -> str: """ Extract tokens from Figma and save to a file. Args: file_key: The Figma file key output_path: Path to save the tokens file (relative or absolute) format: Output format - css, scss, json, or ts (default: css) Returns: Confirmation of sync with token count """ # First extract tokens result = json.loads(await extract_tokens(file_key, format)) if not result.get("success"): return json.dumps(result) try: formatted = result.get("formatted_output", "") # Ensure directory exists path = Path(output_path) path.parent.mkdir(parents=True, exist_ok=True) with open(path, "w") as f: f.write(formatted) # Log to sync history SyncHistory.log( project_id="cli", source="figma", target=str(path), tokens_count=result.get("tokens_count", 0), status="success" ) return json.dumps({ "success": True, "tokens_synced": result.get("tokens_count", 0), "output_file": str(path), "format": format }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def get_sync_history(limit: int = 10) -> str: """ Get recent token sync history. Args: limit: Maximum number of records to return (default: 10) Returns: List of recent sync operations """ history = SyncHistory.list(limit=limit) return json.dumps([h.to_dict() for h in history], indent=2) @mcp.tool() async def get_activity(limit: int = 20) -> str: """ Get recent activity log. Args: limit: Maximum number of activities to return (default: 20) Returns: List of recent activities """ activities = ActivityLog.list(limit=limit) return json.dumps([a.to_dict() for a in activities], indent=2) # === NEW: Multi-Source Token Ingestion Tools === @mcp.tool() async def ingest_css_tokens(source: str) -> str: """ Extract design tokens from CSS custom properties. Parses CSS files for :root { --var-name: value; } declarations and converts them to standardized design tokens. Args: source: File path to CSS file or CSS content string Returns: JSON with extracted tokens including name, value, type, and source attribution """ try: css_source = CSSTokenSource() collection = await css_source.extract(source) return json.dumps({ "success": True, "source_type": "css", "tokens_count": len(collection), "tokens": [ { "name": t.name, "value": t.value, "type": t.type.value, "category": t.category.value, "source": t.source, } for t in collection.tokens ], "summary": collection.summary(), }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def ingest_scss_tokens(source: str) -> str: """ Extract design tokens from SCSS/Sass variables. Parses SCSS files for $variable: value; declarations including map variables like $colors: (primary: #3B82F6); Args: source: File path to SCSS file or SCSS content string Returns: JSON with extracted tokens """ try: scss_source = SCSSTokenSource() collection = await scss_source.extract(source) return json.dumps({ "success": True, "source_type": "scss", "tokens_count": len(collection), "tokens": [ { "name": t.name, "value": t.value, "type": t.type.value, "category": t.category.value, "source": t.source, } for t in collection.tokens ], "summary": collection.summary(), }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def ingest_tailwind_tokens(source: str) -> str: """ Extract design tokens from Tailwind CSS configuration. Parses tailwind.config.js/ts files for theme values including colors, spacing, typography, etc. Args: source: Path to tailwind.config.js or directory containing it Returns: JSON with extracted tokens """ try: tailwind_source = TailwindTokenSource() collection = await tailwind_source.extract(source) return json.dumps({ "success": True, "source_type": "tailwind", "tokens_count": len(collection), "tokens": [ { "name": t.name, "value": t.value, "type": t.type.value, "category": t.category.value, "source": t.source, } for t in collection.tokens ], "summary": collection.summary(), }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def ingest_json_tokens(source: str) -> str: """ Extract design tokens from JSON token files. Supports multiple formats: - W3C Design Tokens format ($value, $type) - Style Dictionary format (value, comment) - Tokens Studio format - Generic nested JSON Args: source: Path to JSON file or JSON content string Returns: JSON with extracted tokens """ try: json_source = JSONTokenSource() collection = await json_source.extract(source) return json.dumps({ "success": True, "source_type": "json", "tokens_count": len(collection), "tokens": [ { "name": t.name, "value": t.value, "type": t.type.value, "category": t.category.value, "source": t.source, } for t in collection.tokens ], "summary": collection.summary(), }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def merge_tokens( sources: str, strategy: str = "last" ) -> str: """ Merge tokens from multiple sources with conflict resolution. Combines tokens from different sources (Figma, CSS, SCSS, etc.) into a unified token collection. Args: sources: JSON array of source configs, e.g.: [{"type": "css", "path": "/path/to/tokens.css"}, {"type": "scss", "path": "/path/to/variables.scss"}] strategy: Conflict resolution strategy: - "first": Keep first occurrence - "last": Keep last occurrence (default) - "prefer_figma": Prefer Figma sources - "prefer_code": Prefer CSS/SCSS sources - "merge_metadata": Merge metadata, keep latest value Returns: JSON with merged tokens and conflict report """ try: source_configs = json.loads(sources) collections = [] # Extract tokens from each source for config in source_configs: source_type = config.get("type", "").lower() source_path = config.get("path", "") if source_type == "css": extractor = CSSTokenSource() elif source_type == "scss": extractor = SCSSTokenSource() elif source_type == "tailwind": extractor = TailwindTokenSource() elif source_type == "json": extractor = JSONTokenSource() else: continue collection = await extractor.extract(source_path) collections.append(collection) if not collections: return json.dumps({ "success": False, "error": "No valid sources provided" }) # Merge collections merge_strategy = MergeStrategy(strategy) merger = TokenMerger(strategy=merge_strategy) result = merger.merge(collections) return json.dumps({ "success": True, "tokens_count": len(result.collection), "tokens": [ { "name": t.name, "value": t.value, "type": t.type.value, "category": t.category.value, "source": t.source, } for t in result.collection.tokens ], "conflicts": [ { "token_name": c.token_name, "existing_source": c.existing.source, "incoming_source": c.incoming.source, "resolution": c.resolution, } for c in result.conflicts ], "stats": result.stats, }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def export_tokens( source: str, format: str = "css", output_path: str = "" ) -> str: """ Export tokens to various formats. Takes tokens from any source and exports to CSS, SCSS, JSON, TypeScript, or Tailwind. Args: source: Path to token source file (CSS, SCSS, JSON, etc.) or JSON token array format: Output format: css, scss, json, ts, tailwind, w3c output_path: Optional path to save output file Returns: Formatted tokens as string, optionally saved to file """ try: # Parse source tokens if source.startswith('[') or source.startswith('{'): # JSON content json_source = JSONTokenSource() collection = await json_source.extract(source) elif source.endswith('.css'): css_source = CSSTokenSource() collection = await css_source.extract(source) elif source.endswith('.scss') or source.endswith('.sass'): scss_source = SCSSTokenSource() collection = await scss_source.extract(source) else: json_source = JSONTokenSource() collection = await json_source.extract(source) # Format output if format == "css": output = collection.to_css() elif format == "scss": output = collection.to_scss() elif format == "ts": output = collection.to_typescript() elif format == "tailwind": output = collection.to_tailwind_config() elif format == "w3c": output = collection.to_json() else: # json output = json.dumps([ {"name": t.name, "value": t.value, "type": t.type.value} for t in collection.tokens ], indent=2) # Save to file if path provided if output_path: Path(output_path).parent.mkdir(parents=True, exist_ok=True) Path(output_path).write_text(output) return json.dumps({ "success": True, "format": format, "tokens_count": len(collection), "output": output[:5000] if len(output) > 5000 else output, "output_path": output_path if output_path else None, "truncated": len(output) > 5000, }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def validate_tokens(source: str) -> str: """ Validate a token collection for issues. Checks for: - Duplicate token names - Invalid values - Missing required properties - Naming convention issues - Deprecated tokens Args: source: Path to token file or JSON token content Returns: Validation report with issues and warnings """ try: # Parse tokens if source.endswith('.css'): extractor = CSSTokenSource() elif source.endswith('.scss'): extractor = SCSSTokenSource() else: extractor = JSONTokenSource() collection = await extractor.extract(source) issues = [] warnings = [] # Check for duplicates duplicates = collection.get_duplicates() for name, tokens in duplicates.items(): issues.append({ "type": "duplicate", "token": name, "count": len(tokens), "sources": [t.source for t in tokens], }) # Check for naming issues for token in collection.tokens: # Check for spaces if ' ' in token.name: issues.append({ "type": "invalid_name", "token": token.name, "message": "Token name contains spaces", }) # Check for uppercase if token.name != token.name.lower(): warnings.append({ "type": "naming_convention", "token": token.name, "message": "Token name should be lowercase", }) # Check for deprecated if token.deprecated: warnings.append({ "type": "deprecated", "token": token.name, "message": token.deprecated_message or "Token is deprecated", }) return json.dumps({ "success": True, "valid": len(issues) == 0, "tokens_count": len(collection), "issues": issues, "warnings": warnings, "summary": { "issues_count": len(issues), "warnings_count": len(warnings), "categories": list(cat.value for cat in collection.get_categories()), } }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) # === NEW: Code Analysis & Intelligence Tools === @mcp.tool() async def discover_project(path: str) -> str: """ Analyze a project to discover its structure, framework, and styling approach. Scans the codebase to identify: - Framework (React, Next.js, Vue, etc.) - Styling approaches (CSS modules, styled-components, Tailwind, etc.) - Component count and locations - Style file inventory Args: path: Path to the project root directory Returns: JSON with complete project analysis including framework, styling, and file inventory """ try: scanner = ProjectScanner(path) analysis = await scanner.scan() return json.dumps({ "success": True, "project_path": analysis.project_path, "framework": analysis.framework.value, "framework_version": analysis.framework_version, "styling_approaches": [sp.to_dict() for sp in analysis.styling_approaches], "primary_styling": analysis.primary_styling.value if analysis.primary_styling else None, "component_count": len([f for f in analysis.style_files if f.type == 'component']), "style_files": [sf.to_dict() for sf in analysis.style_files[:20]], "stats": analysis.stats, "summary": analysis.summary(), }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def analyze_react_components(path: str) -> str: """ Analyze React components in a project. Finds all React components and extracts: - Component names and types (functional, class, memo, forwardRef) - Props definitions - Style file dependencies - Inline style usage - Child component relationships Args: path: Path to the project or specific directory to analyze Returns: JSON with component inventory and relationships """ try: analyzer = ReactAnalyzer(path) components = await analyzer.analyze() return json.dumps({ "success": True, "components_count": len(components), "components": [c.to_dict() for c in components[:30]], "summary": { "total": len(components), "with_styles": len([c for c in components if c.has_styles]), "with_inline_styles": len([c for c in components if c.inline_style_count > 0]), "by_type": { "functional": len([c for c in components if c.type == "functional"]), "class": len([c for c in components if c.type == "class"]), "memo": len([c for c in components if c.type == "memo"]), "forwardRef": len([c for c in components if c.type == "forwardRef"]), } } }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def find_inline_styles(path: str) -> str: """ Find all inline style usage in a React project. Scans JSX/TSX files for style={{ ... }} patterns and style={variable} usage. Args: path: Path to the project or directory to scan Returns: JSON with inline style locations and content """ try: analyzer = ReactAnalyzer(path) inline_styles = await analyzer.find_inline_styles() # Group by file by_file = {} for style in inline_styles: file_path = style['file'] if file_path not in by_file: by_file[file_path] = [] by_file[file_path].append(style) return json.dumps({ "success": True, "total_count": len(inline_styles), "files_affected": len(by_file), "inline_styles": inline_styles[:50], "by_file": {k: len(v) for k, v in list(by_file.items())[:20]}, }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def find_style_patterns(path: str) -> str: """ Identify all styling patterns used in a project. Detects usage of: - CSS Modules - styled-components - Emotion - Tailwind CSS - Inline styles - Regular CSS classes Args: path: Path to the project directory Returns: JSON with styling pattern inventory by type """ try: analyzer = ReactAnalyzer(path) patterns = await analyzer.find_style_patterns() summary = { pattern_type: len(occurrences) for pattern_type, occurrences in patterns.items() } return json.dumps({ "success": True, "patterns": patterns, "summary": summary, "primary": max(summary, key=summary.get) if summary else None, }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def analyze_style_values( path: str, include_inline: bool = True ) -> str: """ Analyze style values across a project to find duplicates and token candidates. Scans CSS files and inline styles to identify: - Repeated color values - Duplicate spacing values - Font values that should be tokens - Values that appear in multiple files Args: path: Path to the project directory include_inline: Whether to include inline styles in analysis Returns: JSON with duplicate values and token suggestions """ try: analyzer = StyleAnalyzer(path) analysis = await analyzer.analyze(include_inline=include_inline) return json.dumps({ "success": True, "total_values": analysis['total_values_found'], "unique_colors": analysis['unique_colors'], "unique_spacing": analysis['unique_spacing'], "duplicates": analysis['duplicates'][:20], "token_candidates": [ { "value": c.value, "suggested_name": c.suggested_name, "category": c.category, "occurrences": c.occurrences, "confidence": c.confidence, } for c in analysis['token_candidates'][:15] ], }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def find_unused_styles(path: str) -> str: """ Find CSS classes that are defined but not used in the codebase. Compares CSS class definitions against className usage in JS/JSX/TS/TSX files. Args: path: Path to the project directory Returns: JSON with potentially unused CSS classes """ try: analyzer = StyleAnalyzer(path) unused = await analyzer.find_unused_styles() return json.dumps({ "success": True, "count": len(unused), "unused_classes": unused[:30], "note": "Review carefully - some classes may be dynamically generated" }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def build_source_graph(path: str, depth: int = 3) -> str: """ Build a dependency graph of components and styles. Creates a graph showing: - Component import relationships - Style file dependencies - Component usage (which components render other components) Args: path: Path to the project directory depth: Maximum depth for traversing dependencies (default: 3) Returns: JSON with nodes (files) and edges (dependencies) """ try: graph = DependencyGraph(path) result = await graph.build(depth=depth) return json.dumps({ "success": True, "stats": result['stats'], "nodes": result['nodes'][:50], # Limit for response size "edges": result['edges'][:100], "component_tree": graph.get_component_tree(), "hubs": graph.find_hubs(min_connections=3)[:10], "orphans": graph.find_orphans()[:10], }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def get_quick_wins(path: str) -> str: """ Identify quick improvement opportunities in a codebase. Finds easy wins like: - Inline styles that can be extracted - Duplicate values that should be tokens - Unused CSS - Naming inconsistencies - Accessibility issues Args: path: Path to the project directory Returns: JSON with prioritized list of improvement opportunities """ try: finder = QuickWinFinder(path) summary = await finder.get_summary() return json.dumps({ "success": True, "total_quick_wins": summary['total'], "auto_fixable": summary['auto_fixable'], "by_priority": summary['by_priority'], "by_type": summary['by_type'], "top_wins": summary['top_wins'], }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def get_quick_wins_report(path: str) -> str: """ Generate a human-readable report of quick improvement opportunities. Creates a formatted report with: - Prioritized list of improvements - Affected files - Suggested fixes - Impact estimates Args: path: Path to the project directory Returns: Formatted text report of quick-wins """ try: finder = QuickWinFinder(path) report = await finder.get_actionable_report() return json.dumps({ "success": True, "report": report, }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def check_naming_consistency(path: str) -> str: """ Check CSS naming convention consistency across a project. Identifies the primary naming convention (BEM, kebab-case, camelCase, etc.) and flags classes that don't follow it. Args: path: Path to the project directory Returns: JSON with naming pattern analysis and inconsistencies """ try: analyzer = StyleAnalyzer(path) naming = await analyzer.analyze_naming_consistency() return json.dumps({ "success": True, "pattern_counts": naming['pattern_counts'], "primary_pattern": naming['primary_pattern'], "inconsistencies_count": len(naming['inconsistencies']), "inconsistencies": naming['inconsistencies'][:20], }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) # === NEW: Storybook Integration Tools === @mcp.tool() async def scan_storybook(path: str) -> str: """ Scan a project for Storybook configuration and stories. Discovers: - Storybook version and configuration - All story files and their components - Story coverage statistics Args: path: Path to the project directory Returns: JSON with Storybook configuration and story inventory """ try: scanner = StorybookScanner(path) result = await scanner.scan() return json.dumps({ "success": True, "config": result.get("config"), "stories_count": result.get("stories_count", 0), "components_with_stories": result.get("components_with_stories", 0), "stories": result.get("stories", [])[:30], "by_component": { k: v for k, v in list(result.get("by_component", {}).items())[:20] }, }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def generate_story( component_path: str, template: str = "csf3", include_variants: bool = True, output_path: str = "", ) -> str: """ Generate a Storybook story for a React component. Analyzes the component's props and generates appropriate stories with variants for different prop combinations. Args: component_path: Path to the component file template: Story format - 'csf3' (default), 'csf2', or 'mdx' include_variants: Generate stories for prop variants (default: True) output_path: Optional path to write the story file Returns: Generated story code """ try: # Determine root from component path comp_path = Path(component_path) if comp_path.is_absolute(): root = comp_path.parent rel_path = comp_path.name else: root = Path.cwd() rel_path = component_path generator = StoryGenerator(str(root)) story_template = StoryTemplate(template) story = await generator.generate_story( rel_path, template=story_template, include_variants=include_variants, output_path=output_path if output_path else None, ) return json.dumps({ "success": True, "component": component_path, "template": template, "story": story, "output_path": output_path if output_path else None, }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def generate_stories_batch( directory: str, template: str = "csf3", dry_run: bool = True, ) -> str: """ Generate Storybook stories for all components in a directory. Scans the directory for React components and generates stories for each one that doesn't already have a story file. Args: directory: Path to component directory template: Story format - 'csf3', 'csf2', or 'mdx' dry_run: If True, only preview what would be generated (default: True) Returns: JSON with generated stories and their paths """ try: dir_path = Path(directory) if dir_path.is_absolute(): root = dir_path.parent rel_dir = dir_path.name else: root = Path.cwd() rel_dir = directory generator = StoryGenerator(str(root)) story_template = StoryTemplate(template) results = await generator.generate_stories_for_directory( rel_dir, template=story_template, dry_run=dry_run, ) return json.dumps({ "success": True, "directory": directory, "dry_run": dry_run, "generated_count": len([r for r in results if "story" in r]), "results": results[:20], }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def generate_storybook_theme( source: str, brand_title: str = "Design System", base: str = "light", output_dir: str = "", ) -> str: """ Generate Storybook theme configuration from design tokens. Creates theme files that style Storybook UI to match your design system. Args: source: Path to token file (CSS, SCSS, JSON) or JSON token array brand_title: Brand title shown in Storybook (default: "Design System") base: Base theme - 'light' or 'dark' (default: 'light') output_dir: Optional directory to write theme files Returns: Generated theme configuration files """ try: # Parse tokens from source if source.startswith('[') or source.startswith('{'): tokens_data = json.loads(source) if isinstance(tokens_data, dict): tokens = [{"name": k, "value": v} for k, v in tokens_data.items()] else: tokens = tokens_data elif source.endswith('.css'): css_source = CSSTokenSource() collection = await css_source.extract(source) tokens = [{"name": t.name, "value": t.value} for t in collection.tokens] elif source.endswith('.scss'): scss_source = SCSSTokenSource() collection = await scss_source.extract(source) tokens = [{"name": t.name, "value": t.value} for t in collection.tokens] else: json_source = JSONTokenSource() collection = await json_source.extract(source) tokens = [{"name": t.name, "value": t.value} for t in collection.tokens] generator = ThemeGenerator() files = generator.generate_full_config( tokens, brand_title=brand_title, output_dir=output_dir if output_dir else None, ) return json.dumps({ "success": True, "tokens_used": len(tokens), "brand_title": brand_title, "base": base, "files": { name: content[:2000] if len(content) > 2000 else content for name, content in files.items() }, "output_dir": output_dir if output_dir else None, }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @mcp.tool() async def get_story_coverage(path: str) -> str: """ Get Storybook story coverage statistics for a project. Shows which components have stories and coverage metrics. Args: path: Path to the project directory Returns: JSON with coverage statistics """ try: scanner = StorybookScanner(path) coverage = await scanner.get_story_coverage() return json.dumps({ "success": True, "total_stories": coverage.get("total_stories", 0), "components_covered": coverage.get("components_covered", 0), "average_stories_per_component": coverage.get("average_stories_per_component", 0), "stories_per_component": coverage.get("stories_per_component", {}), }, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) # === Helper Functions === def format_tokens_output(tokens: list, format: str) -> str: """Format tokens into the specified output format.""" if format == "css": lines = [":root {"] for t in tokens: name = t["name"].replace(".", "-").replace("/", "-") lines.append(f" --{name}: {t['value']};") lines.append("}") return "\n".join(lines) elif format == "scss": lines = [] for t in tokens: name = t["name"].replace(".", "-").replace("/", "-") lines.append(f"${name}: {t['value']};") return "\n".join(lines) elif format == "ts": lines = ["export const tokens = {"] for t in tokens: name = t["name"].replace(".", "_").replace("/", "_").replace("-", "_") lines.append(f' {name}: "{t["value"]}",') lines.append("} as const;") return "\n".join(lines) else: # json return json.dumps(tokens, indent=2) def generate_react_component(name: str, tokens: dict) -> str: """Generate a React component.""" return f'''import React from 'react'; import styles from './{name}.module.css'; interface {name}Props {{ children?: React.ReactNode; variant?: 'primary' | 'secondary'; onClick?: () => void; }} export const {name}: React.FC<{name}Props> = ({{ children, variant = 'primary', onClick }}) => {{ return (