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:
Digital Production Factory
2025-12-09 18:45:48 -03:00
commit 276ed71f31
884 changed files with 373737 additions and 0 deletions

View File

View File

@@ -0,0 +1,264 @@
"""
Base Integration Classes
Provides circuit breaker pattern and base classes for external integrations.
"""
import time
import asyncio
from typing import Callable, Any, Optional, Dict
from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import Enum
from ..config import mcp_config
from storage.database import get_connection
class CircuitState(Enum):
"""Circuit breaker states"""
CLOSED = "closed" # Normal operation
OPEN = "open" # Failing, reject requests
HALF_OPEN = "half_open" # Testing if service recovered
@dataclass
class CircuitBreakerStats:
"""Circuit breaker statistics"""
state: CircuitState
failure_count: int
success_count: int
last_failure_time: Optional[float]
last_success_time: Optional[float]
opened_at: Optional[float]
next_retry_time: Optional[float]
class CircuitBreakerOpen(Exception):
"""Exception raised when circuit breaker is open"""
pass
class CircuitBreaker:
"""
Circuit Breaker pattern implementation.
Protects external service calls from cascading failures.
Three states: CLOSED (normal), OPEN (failing), HALF_OPEN (testing).
"""
def __init__(
self,
integration_type: str,
failure_threshold: int = None,
timeout_seconds: int = None,
half_open_max_calls: int = 3
):
"""
Args:
integration_type: Type of integration (figma, jira, confluence, etc.)
failure_threshold: Number of failures before opening circuit
timeout_seconds: Seconds to wait before trying again
half_open_max_calls: Max successful calls in half-open before closing
"""
self.integration_type = integration_type
self.failure_threshold = failure_threshold or mcp_config.CIRCUIT_BREAKER_FAILURE_THRESHOLD
self.timeout_seconds = timeout_seconds or mcp_config.CIRCUIT_BREAKER_TIMEOUT_SECONDS
self.half_open_max_calls = half_open_max_calls
# In-memory state (could be moved to Redis for distributed setup)
self.state = CircuitState.CLOSED
self.failure_count = 0
self.success_count = 0
self.last_failure_time: Optional[float] = None
self.last_success_time: Optional[float] = None
self.opened_at: Optional[float] = None
async def call(self, func: Callable, *args, **kwargs) -> Any:
"""
Call a function through the circuit breaker.
Args:
func: Function to call (can be sync or async)
*args, **kwargs: Arguments to pass to func
Returns:
Function result
Raises:
CircuitBreakerOpen: If circuit is open
Exception: Original exception from func if it fails
"""
# Check circuit state
if self.state == CircuitState.OPEN:
# Check if timeout has elapsed
if time.time() - self.opened_at < self.timeout_seconds:
await self._record_failure("Circuit breaker is OPEN", db_only=True)
raise CircuitBreakerOpen(
f"{self.integration_type} service is temporarily unavailable. "
f"Retry after {self._seconds_until_retry():.0f}s"
)
else:
# Timeout elapsed, move to HALF_OPEN
self.state = CircuitState.HALF_OPEN
self.success_count = 0
# Execute function
try:
# Handle both sync and async functions
if asyncio.iscoroutinefunction(func):
result = await func(*args, **kwargs)
else:
result = func(*args, **kwargs)
# Success!
await self._record_success()
# If in HALF_OPEN, check if we can close the circuit
if self.state == CircuitState.HALF_OPEN:
if self.success_count >= self.half_open_max_calls:
self.state = CircuitState.CLOSED
self.failure_count = 0
return result
except Exception as e:
# Failure
await self._record_failure(str(e))
# Check if we should open the circuit
if self.failure_count >= self.failure_threshold:
self.state = CircuitState.OPEN
self.opened_at = time.time()
raise
async def _record_success(self):
"""Record successful call"""
self.success_count += 1
self.last_success_time = time.time()
# Update database
await self._update_health_db(is_healthy=True, error=None)
async def _record_failure(self, error_message: str, db_only: bool = False):
"""Record failed call"""
if not db_only:
self.failure_count += 1
self.last_failure_time = time.time()
# Update database
await self._update_health_db(is_healthy=False, error=error_message)
async def _update_health_db(self, is_healthy: bool, error: Optional[str]):
"""Update integration health in database"""
loop = asyncio.get_event_loop()
def update_db():
try:
with get_connection() as conn:
circuit_open_until = None
if self.state == CircuitState.OPEN and self.opened_at:
circuit_open_until = datetime.fromtimestamp(
self.opened_at + self.timeout_seconds
).isoformat()
if is_healthy:
conn.execute(
"""
UPDATE integration_health
SET is_healthy = 1,
failure_count = 0,
last_success_at = CURRENT_TIMESTAMP,
circuit_open_until = NULL,
updated_at = CURRENT_TIMESTAMP
WHERE integration_type = ?
""",
(self.integration_type,)
)
else:
conn.execute(
"""
UPDATE integration_health
SET is_healthy = 0,
failure_count = ?,
last_failure_at = CURRENT_TIMESTAMP,
circuit_open_until = ?,
updated_at = CURRENT_TIMESTAMP
WHERE integration_type = ?
""",
(self.failure_count, circuit_open_until, self.integration_type)
)
except Exception as e:
print(f"Error updating integration health: {e}")
await loop.run_in_executor(None, update_db)
def _seconds_until_retry(self) -> float:
"""Get seconds until circuit can be retried"""
if self.state != CircuitState.OPEN or not self.opened_at:
return 0
elapsed = time.time() - self.opened_at
remaining = self.timeout_seconds - elapsed
return max(0, remaining)
def get_stats(self) -> CircuitBreakerStats:
"""Get current circuit breaker statistics"""
next_retry_time = None
if self.state == CircuitState.OPEN and self.opened_at:
next_retry_time = self.opened_at + self.timeout_seconds
return CircuitBreakerStats(
state=self.state,
failure_count=self.failure_count,
success_count=self.success_count,
last_failure_time=self.last_failure_time,
last_success_time=self.last_success_time,
opened_at=self.opened_at,
next_retry_time=next_retry_time
)
class BaseIntegration:
"""Base class for all external integrations"""
def __init__(self, integration_type: str, config: Dict[str, Any]):
"""
Args:
integration_type: Type of integration (figma, jira, etc.)
config: Integration configuration (decrypted)
"""
self.integration_type = integration_type
self.config = config
self.circuit_breaker = CircuitBreaker(integration_type)
async def call_api(self, func: Callable, *args, **kwargs) -> Any:
"""
Call external API through circuit breaker.
Args:
func: API function to call
*args, **kwargs: Arguments to pass
Returns:
API response
Raises:
CircuitBreakerOpen: If circuit is open
Exception: Original API exception
"""
return await self.circuit_breaker.call(func, *args, **kwargs)
def get_health(self) -> Dict[str, Any]:
"""Get integration health status"""
stats = self.circuit_breaker.get_stats()
return {
"integration_type": self.integration_type,
"state": stats.state.value,
"is_healthy": stats.state == CircuitState.CLOSED,
"failure_count": stats.failure_count,
"success_count": stats.success_count,
"last_failure_time": stats.last_failure_time,
"last_success_time": stats.last_success_time,
"next_retry_time": stats.next_retry_time
}

View File

@@ -0,0 +1,262 @@
"""
Confluence Integration for MCP
Provides Confluence API tools for documentation and knowledge base.
"""
from typing import Dict, Any, List, Optional
from atlassian import Confluence
from mcp import types
from .base import BaseIntegration
# Confluence MCP Tool Definitions
CONFLUENCE_TOOLS = [
types.Tool(
name="confluence_create_page",
description="Create a new Confluence page",
inputSchema={
"type": "object",
"properties": {
"space_key": {
"type": "string",
"description": "Confluence space key"
},
"title": {
"type": "string",
"description": "Page title"
},
"body": {
"type": "string",
"description": "Page content (HTML or wiki markup)"
},
"parent_id": {
"type": "string",
"description": "Optional parent page ID"
}
},
"required": ["space_key", "title", "body"]
}
),
types.Tool(
name="confluence_get_page",
description="Get Confluence page by ID or title",
inputSchema={
"type": "object",
"properties": {
"page_id": {
"type": "string",
"description": "Page ID (use this OR title)"
},
"space_key": {
"type": "string",
"description": "Space key (required if using title)"
},
"title": {
"type": "string",
"description": "Page title (use this OR page_id)"
},
"expand": {
"type": "string",
"description": "Comma-separated list of expansions (body.storage, version, etc.)",
"default": "body.storage,version"
}
}
}
),
types.Tool(
name="confluence_update_page",
description="Update an existing Confluence page",
inputSchema={
"type": "object",
"properties": {
"page_id": {
"type": "string",
"description": "Page ID to update"
},
"title": {
"type": "string",
"description": "New page title"
},
"body": {
"type": "string",
"description": "New page content"
}
},
"required": ["page_id", "title", "body"]
}
),
types.Tool(
name="confluence_search",
description="Search Confluence pages using CQL",
inputSchema={
"type": "object",
"properties": {
"cql": {
"type": "string",
"description": "CQL query (e.g., 'space=DSS AND type=page')"
},
"limit": {
"type": "integer",
"description": "Maximum number of results",
"default": 25
}
},
"required": ["cql"]
}
),
types.Tool(
name="confluence_get_space",
description="Get Confluence space details",
inputSchema={
"type": "object",
"properties": {
"space_key": {
"type": "string",
"description": "Space key"
}
},
"required": ["space_key"]
}
)
]
class ConfluenceIntegration(BaseIntegration):
"""Confluence API integration with circuit breaker"""
def __init__(self, config: Dict[str, Any]):
"""
Initialize Confluence integration.
Args:
config: Must contain 'url', 'username', 'api_token'
"""
super().__init__("confluence", config)
url = config.get("url")
username = config.get("username")
api_token = config.get("api_token")
if not all([url, username, api_token]):
raise ValueError("Confluence configuration incomplete: url, username, api_token required")
self.confluence = Confluence(
url=url,
username=username,
password=api_token,
cloud=True
)
async def create_page(
self,
space_key: str,
title: str,
body: str,
parent_id: Optional[str] = None
) -> Dict[str, Any]:
"""Create a new page"""
def _create():
return self.confluence.create_page(
space=space_key,
title=title,
body=body,
parent_id=parent_id,
representation="storage"
)
return await self.call_api(_create)
async def get_page(
self,
page_id: Optional[str] = None,
space_key: Optional[str] = None,
title: Optional[str] = None,
expand: str = "body.storage,version"
) -> Dict[str, Any]:
"""Get page by ID or title"""
def _get():
if page_id:
return self.confluence.get_page_by_id(
page_id=page_id,
expand=expand
)
elif space_key and title:
return self.confluence.get_page_by_title(
space=space_key,
title=title,
expand=expand
)
else:
raise ValueError("Must provide either page_id or (space_key + title)")
return await self.call_api(_get)
async def update_page(
self,
page_id: str,
title: str,
body: str
) -> Dict[str, Any]:
"""Update an existing page"""
def _update():
# Get current version
page = self.confluence.get_page_by_id(page_id, expand="version")
current_version = page["version"]["number"]
return self.confluence.update_page(
page_id=page_id,
title=title,
body=body,
parent_id=None,
type="page",
representation="storage",
minor_edit=False,
version_comment="Updated via DSS MCP",
version_number=current_version + 1
)
return await self.call_api(_update)
async def search(self, cql: str, limit: int = 25) -> Dict[str, Any]:
"""Search pages using CQL"""
def _search():
return self.confluence.cql(cql, limit=limit)
return await self.call_api(_search)
async def get_space(self, space_key: str) -> Dict[str, Any]:
"""Get space details"""
def _get():
return self.confluence.get_space(space_key)
return await self.call_api(_get)
class ConfluenceTools:
"""MCP tool executor for Confluence integration"""
def __init__(self, config: Dict[str, Any]):
self.confluence = ConfluenceIntegration(config)
async def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""Execute Confluence tool"""
handlers = {
"confluence_create_page": self.confluence.create_page,
"confluence_get_page": self.confluence.get_page,
"confluence_update_page": self.confluence.update_page,
"confluence_search": self.confluence.search,
"confluence_get_space": self.confluence.get_space
}
handler = handlers.get(tool_name)
if not handler:
return {"error": f"Unknown Confluence tool: {tool_name}"}
try:
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": str(e)}

View File

@@ -0,0 +1,260 @@
"""
Figma Integration for MCP
Provides Figma API tools through circuit breaker pattern.
"""
import httpx
from typing import Dict, Any, List, Optional
from mcp import types
from .base import BaseIntegration
from ..config import integration_config
# Figma MCP Tool Definitions
FIGMA_TOOLS = [
types.Tool(
name="figma_get_file",
description="Get Figma file metadata and structure",
inputSchema={
"type": "object",
"properties": {
"file_key": {
"type": "string",
"description": "Figma file key"
}
},
"required": ["file_key"]
}
),
types.Tool(
name="figma_get_styles",
description="Get design styles (colors, text, effects) from Figma file",
inputSchema={
"type": "object",
"properties": {
"file_key": {
"type": "string",
"description": "Figma file key"
}
},
"required": ["file_key"]
}
),
types.Tool(
name="figma_get_components",
description="Get component definitions from Figma file",
inputSchema={
"type": "object",
"properties": {
"file_key": {
"type": "string",
"description": "Figma file key"
}
},
"required": ["file_key"]
}
),
types.Tool(
name="figma_extract_tokens",
description="Extract design tokens (variables) from Figma file",
inputSchema={
"type": "object",
"properties": {
"file_key": {
"type": "string",
"description": "Figma file key"
}
},
"required": ["file_key"]
}
),
types.Tool(
name="figma_get_node",
description="Get specific node/component by ID from Figma file",
inputSchema={
"type": "object",
"properties": {
"file_key": {
"type": "string",
"description": "Figma file key"
},
"node_id": {
"type": "string",
"description": "Node ID to fetch"
}
},
"required": ["file_key", "node_id"]
}
)
]
class FigmaIntegration(BaseIntegration):
"""Figma API integration with circuit breaker"""
FIGMA_API_BASE = "https://api.figma.com/v1"
def __init__(self, config: Dict[str, Any]):
"""
Initialize Figma integration.
Args:
config: Must contain 'api_token' or use FIGMA_TOKEN from env
"""
super().__init__("figma", config)
self.api_token = config.get("api_token") or integration_config.FIGMA_TOKEN
if not self.api_token:
raise ValueError("Figma API token not configured")
self.headers = {
"X-Figma-Token": self.api_token
}
async def get_file(self, file_key: str) -> Dict[str, Any]:
"""
Get Figma file metadata and structure.
Args:
file_key: Figma file key
Returns:
File data
"""
async def _fetch():
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.FIGMA_API_BASE}/files/{file_key}",
headers=self.headers,
timeout=30.0
)
response.raise_for_status()
return response.json()
return await self.call_api(_fetch)
async def get_styles(self, file_key: str) -> Dict[str, Any]:
"""
Get all styles from Figma file.
Args:
file_key: Figma file key
Returns:
Styles data
"""
async def _fetch():
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.FIGMA_API_BASE}/files/{file_key}/styles",
headers=self.headers,
timeout=30.0
)
response.raise_for_status()
return response.json()
return await self.call_api(_fetch)
async def get_components(self, file_key: str) -> Dict[str, Any]:
"""
Get all components from Figma file.
Args:
file_key: Figma file key
Returns:
Components data
"""
async def _fetch():
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.FIGMA_API_BASE}/files/{file_key}/components",
headers=self.headers,
timeout=30.0
)
response.raise_for_status()
return response.json()
return await self.call_api(_fetch)
async def extract_tokens(self, file_key: str) -> Dict[str, Any]:
"""
Extract design tokens (variables) from Figma file.
Args:
file_key: Figma file key
Returns:
Variables/tokens data
"""
async def _fetch():
async with httpx.AsyncClient() as client:
# Get local variables
response = await client.get(
f"{self.FIGMA_API_BASE}/files/{file_key}/variables/local",
headers=self.headers,
timeout=30.0
)
response.raise_for_status()
return response.json()
return await self.call_api(_fetch)
async def get_node(self, file_key: str, node_id: str) -> Dict[str, Any]:
"""
Get specific node from Figma file.
Args:
file_key: Figma file key
node_id: Node ID
Returns:
Node data
"""
async def _fetch():
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.FIGMA_API_BASE}/files/{file_key}/nodes",
headers=self.headers,
params={"ids": node_id},
timeout=30.0
)
response.raise_for_status()
return response.json()
return await self.call_api(_fetch)
class FigmaTools:
"""MCP tool executor for Figma integration"""
def __init__(self, config: Dict[str, Any]):
"""
Args:
config: Figma configuration (with api_token)
"""
self.figma = FigmaIntegration(config)
async def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""Execute Figma tool"""
handlers = {
"figma_get_file": self.figma.get_file,
"figma_get_styles": self.figma.get_styles,
"figma_get_components": self.figma.get_components,
"figma_extract_tokens": self.figma.extract_tokens,
"figma_get_node": self.figma.get_node
}
handler = handlers.get(tool_name)
if not handler:
return {"error": f"Unknown Figma tool: {tool_name}"}
try:
# Remove tool-specific prefix from arguments if needed
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": str(e)}

View File

@@ -0,0 +1,215 @@
"""
Jira Integration for MCP
Provides Jira API tools for issue tracking and project management.
"""
from typing import Dict, Any, List, Optional
from atlassian import Jira
from mcp import types
from .base import BaseIntegration
# Jira MCP Tool Definitions
JIRA_TOOLS = [
types.Tool(
name="jira_create_issue",
description="Create a new Jira issue",
inputSchema={
"type": "object",
"properties": {
"project_key": {
"type": "string",
"description": "Jira project key (e.g., 'DSS')"
},
"summary": {
"type": "string",
"description": "Issue summary/title"
},
"description": {
"type": "string",
"description": "Issue description"
},
"issue_type": {
"type": "string",
"description": "Issue type (Story, Task, Bug, etc.)",
"default": "Task"
}
},
"required": ["project_key", "summary"]
}
),
types.Tool(
name="jira_get_issue",
description="Get Jira issue details by key",
inputSchema={
"type": "object",
"properties": {
"issue_key": {
"type": "string",
"description": "Issue key (e.g., 'DSS-123')"
}
},
"required": ["issue_key"]
}
),
types.Tool(
name="jira_search_issues",
description="Search Jira issues using JQL",
inputSchema={
"type": "object",
"properties": {
"jql": {
"type": "string",
"description": "JQL query (e.g., 'project=DSS AND status=Open')"
},
"max_results": {
"type": "integer",
"description": "Maximum number of results",
"default": 50
}
},
"required": ["jql"]
}
),
types.Tool(
name="jira_update_issue",
description="Update a Jira issue",
inputSchema={
"type": "object",
"properties": {
"issue_key": {
"type": "string",
"description": "Issue key to update"
},
"fields": {
"type": "object",
"description": "Fields to update (summary, description, status, etc.)"
}
},
"required": ["issue_key", "fields"]
}
),
types.Tool(
name="jira_add_comment",
description="Add a comment to a Jira issue",
inputSchema={
"type": "object",
"properties": {
"issue_key": {
"type": "string",
"description": "Issue key"
},
"comment": {
"type": "string",
"description": "Comment text"
}
},
"required": ["issue_key", "comment"]
}
)
]
class JiraIntegration(BaseIntegration):
"""Jira API integration with circuit breaker"""
def __init__(self, config: Dict[str, Any]):
"""
Initialize Jira integration.
Args:
config: Must contain 'url', 'username', 'api_token'
"""
super().__init__("jira", config)
url = config.get("url")
username = config.get("username")
api_token = config.get("api_token")
if not all([url, username, api_token]):
raise ValueError("Jira configuration incomplete: url, username, api_token required")
self.jira = Jira(
url=url,
username=username,
password=api_token,
cloud=True
)
async def create_issue(
self,
project_key: str,
summary: str,
description: str = "",
issue_type: str = "Task"
) -> Dict[str, Any]:
"""Create a new Jira issue"""
def _create():
fields = {
"project": {"key": project_key},
"summary": summary,
"description": description,
"issuetype": {"name": issue_type}
}
return self.jira.create_issue(fields)
return await self.call_api(_create)
async def get_issue(self, issue_key: str) -> Dict[str, Any]:
"""Get issue details"""
def _get():
return self.jira.get_issue(issue_key)
return await self.call_api(_get)
async def search_issues(self, jql: str, max_results: int = 50) -> Dict[str, Any]:
"""Search issues with JQL"""
def _search():
return self.jira.jql(jql, limit=max_results)
return await self.call_api(_search)
async def update_issue(self, issue_key: str, fields: Dict[str, Any]) -> Dict[str, Any]:
"""Update issue fields"""
def _update():
self.jira.update_issue_field(issue_key, fields)
return {"status": "updated", "issue_key": issue_key}
return await self.call_api(_update)
async def add_comment(self, issue_key: str, comment: str) -> Dict[str, Any]:
"""Add comment to issue"""
def _comment():
return self.jira.issue_add_comment(issue_key, comment)
return await self.call_api(_comment)
class JiraTools:
"""MCP tool executor for Jira integration"""
def __init__(self, config: Dict[str, Any]):
self.jira = JiraIntegration(config)
async def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""Execute Jira tool"""
handlers = {
"jira_create_issue": self.jira.create_issue,
"jira_get_issue": self.jira.get_issue,
"jira_search_issues": self.jira.search_issues,
"jira_update_issue": self.jira.update_issue,
"jira_add_comment": self.jira.add_comment
}
handler = handlers.get(tool_name)
if not handler:
return {"error": f"Unknown Jira tool: {tool_name}"}
try:
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": str(e)}

View 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}

File diff suppressed because it is too large Load Diff