Files
dss/tools/api/mcp_server.py
Bruno Sarlo d6c25cb4db Simplify code documentation, remove organism terminology
- Remove biological metaphors from docstrings (organism, sensory, genetic, nutrient, etc.)
- Simplify documentation to be minimal and structured for fast model parsing
- Complete SQLite to JSON storage migration (project_manager.py, json_store.py)
- Add Integrations and IntegrationHealth classes to json_store.py
- Add kill_port() function to server.py for port conflict handling
- All 33 tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 11:02:00 -03:00

1456 lines
43 KiB
Python

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