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:
0
tools/dss_mcp/integrations/__init__.py
Normal file
0
tools/dss_mcp/integrations/__init__.py
Normal file
264
tools/dss_mcp/integrations/base.py
Normal file
264
tools/dss_mcp/integrations/base.py
Normal 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
|
||||
}
|
||||
262
tools/dss_mcp/integrations/confluence.py
Normal file
262
tools/dss_mcp/integrations/confluence.py
Normal 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)}
|
||||
260
tools/dss_mcp/integrations/figma.py
Normal file
260
tools/dss_mcp/integrations/figma.py
Normal 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)}
|
||||
215
tools/dss_mcp/integrations/jira.py
Normal file
215
tools/dss_mcp/integrations/jira.py
Normal 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)}
|
||||
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}
|
||||
1457
tools/dss_mcp/integrations/translations.py
Normal file
1457
tools/dss_mcp/integrations/translations.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user