Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
550 lines
19 KiB
Python
550 lines
19 KiB
Python
"""
|
|
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}
|