Initial commit: Clean DSS implementation
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
This commit is contained in:
549
tools/dss_mcp/integrations/storybook.py
Normal file
549
tools/dss_mcp/integrations/storybook.py
Normal file
@@ -0,0 +1,549 @@
|
||||
"""
|
||||
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}
|
||||
Reference in New Issue
Block a user