Files
dss/tools/api/mcp_server.py
Bruno Sarlo 069f5482d8 Replace SQLite with JSON file storage
- Remove database.py (SQLite) from tools/storage/ and dss-mvp1/
- Add json_store.py with full JSON-based storage layer
- Update 16 files to use new json_store imports
- Storage now mirrors DSS canonical structure:
  .dss/data/
  ├── _system/    (config, cache, activity)
  ├── projects/   (per-project: tokens, components, styles)
  └── teams/      (team definitions)
- Remove Docker files (not needed)
- Update DSS_CORE.json to v1.1.0

Philosophy: "Eat our own food" - storage structure matches DSS design

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

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

1491 lines
45 KiB
Python

#!/usr/bin/env python3
"""
🧠 DSS MCP SERVER - Design System Organism Neural Interface
The MCP server is how AI agents interface with the DSS organism through
the Model Context Protocol. It exposes the organism's neural pathways (API)
as tools that Claude and other AI agents can use to:
- Perceive the organism's current state (health checks)
- Direct the organism's sensory organs (Figma perception)
- Control token ingestion and circulation (nutrient management)
- Analyze the organism's codebase (code intelligence)
- Generate Storybook documentation (organism presentation)
- Diagnose and fix health issues (debugging and fixing)
Think of MCP tools as the organism's neural interface - the way external
intelligence (AI agents) can communicate with and direct the organism.
The organism responds to 32 different MCP tools (neural commands):
- Status & Discovery (4 tools)
- Token Ingestion (7 tools)
- Analysis (11 tools)
- Storybook Generation (5 tools)
- Utilities (5 tools)
When an AI agent calls a tool, it's sending a command through the organism's
nervous system to activate specific organs and functions.
"""
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:
"""
🏥 ORGANISM STATUS CHECK - Get complete vital signs report
Returns the DSS organism's vital signs:
- Is the organism conscious and responsive?
- What is its awareness level (statistics)?
- Can the organism perceive Figma (sensory organs working)?
- What version of the organism is this?
"""
stats = get_stats()
figma_configured = bool(FIGMA_TOKEN)
return json.dumps({
"organism_status": "🟢 Alive and conscious" if FIGMA_TOKEN else "🟡 Alive but blind",
"sensory_organs": {
"figma_eyes": "👁️ Perceiving" if figma_configured else "👁️ Closed"
},
"mode": "living in reality" if figma_configured else "living in imagination (mock mode)",
"organism_statistics": stats,
"version": "0.8.0",
"message": "The organism is ready to work" if figma_configured else "Configure Figma token to unlock visual perception"
}, indent=2)
@mcp.tool()
async def list_projects() -> str:
"""
🏥 ORGANISM CONSCIOUSNESS - View all design system organisms
Lists all living design system organisms that have been born
(created) and are under observation.
"""
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:
"""
🧬 ORGANISM GENESIS - Birth of a new design system organism
Creates a new design system organism - a living, breathing instance
that will ingest tokens, circulate nutrients, and grow over time.
Args:
name: The organism's name (identity)
description: The organism's purpose and characteristics
figma_file_key: Link to the organism's visual genetic blueprint (Figma)
Returns: The newly born organism's vital information
"""
project = Projects.create(name, description, figma_file_key)
return json.dumps({"success": True, "organism_born": 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)