Systematic replacement of 'swarm' and 'organism' terminology across codebase: AUTOMATED REPLACEMENTS: - 'Design System Swarm' → 'Design System Server' (all files) - 'swarm' → 'DSS' (markdown, JSON, comments) - 'organism' → 'component' (markdown, atomic design refs) FILES UPDATED: 60+ files across: - Documentation (.md files) - Configuration (.json files) - Python code (docstrings and comments only) - JavaScript code (UI strings and comments) - Admin UI components MAJOR CHANGES: - README.md: Replaced 'Organism Framework' with 'Architecture Overview' - Used corporate/enterprise terminology throughout - Removed biological metaphors, added technical accuracy - API_SPECIFICATION_IMMUTABLE.md: Terminology updates - dss-claude-plugin/.mcp.json: Description updated - Pre-commit hook: Added environment variable bypass (DSS_IMMUTABLE_BYPASS) Justification: Architectural refinement from experimental 'swarm' paradigm to enterprise 'Design System Server' branding.
427 lines
12 KiB
Python
427 lines
12 KiB
Python
"""
|
|
DSS MCP Server
|
|
|
|
SSE-based Model Context Protocol server for Claude.
|
|
Provides project-isolated context and tools with user-scoped integrations.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
import structlog
|
|
from typing import Optional, Dict, Any
|
|
from fastapi import FastAPI, Query, HTTPException
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from sse_starlette.sse import EventSourceResponse
|
|
from mcp.server import Server
|
|
from mcp import types
|
|
|
|
from .config import mcp_config, validate_config
|
|
from .context.project_context import get_context_manager
|
|
from .tools.project_tools import PROJECT_TOOLS, ProjectTools
|
|
from .tools.workflow_tools import WORKFLOW_TOOLS, WorkflowTools
|
|
from .tools.debug_tools import DEBUG_TOOLS, DebugTools
|
|
from .integrations.storybook import STORYBOOK_TOOLS
|
|
from .integrations.translations import TRANSLATION_TOOLS
|
|
from .plugin_registry import PluginRegistry
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=mcp_config.LOG_LEVEL,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
)
|
|
logger = structlog.get_logger()
|
|
|
|
# FastAPI app for SSE endpoints
|
|
app = FastAPI(
|
|
title="DSS MCP Server",
|
|
description="Model Context Protocol server for Design System Server",
|
|
version="0.8.0"
|
|
)
|
|
|
|
# CORS configuration
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"], # TODO: Configure based on environment
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# MCP Server instance
|
|
mcp_server = Server("dss-mcp")
|
|
|
|
# Initialize Plugin Registry
|
|
plugin_registry = PluginRegistry()
|
|
plugin_registry.load_plugins()
|
|
|
|
# Store active sessions
|
|
_active_sessions: Dict[str, Dict[str, Any]] = {}
|
|
|
|
|
|
def get_session_key(project_id: str, user_id: Optional[int] = None) -> str:
|
|
"""Generate session key for caching"""
|
|
return f"{project_id}:{user_id or 'anonymous'}"
|
|
|
|
|
|
@app.on_event("startup")
|
|
async def startup():
|
|
"""Startup tasks"""
|
|
logger.info("Starting DSS MCP Server")
|
|
|
|
# Validate configuration
|
|
warnings = validate_config()
|
|
if warnings:
|
|
for warning in warnings:
|
|
logger.warning(warning)
|
|
|
|
logger.info(
|
|
"DSS MCP Server started",
|
|
host=mcp_config.HOST,
|
|
port=mcp_config.PORT
|
|
)
|
|
|
|
|
|
@app.on_event("shutdown")
|
|
async def shutdown():
|
|
"""Cleanup on shutdown"""
|
|
logger.info("Shutting down DSS MCP Server")
|
|
|
|
|
|
@app.get("/health")
|
|
async def health_check():
|
|
"""Health check endpoint"""
|
|
context_manager = get_context_manager()
|
|
return {
|
|
"status": "healthy",
|
|
"server": "dss-mcp",
|
|
"version": "0.8.0",
|
|
"cache_size": len(context_manager._cache),
|
|
"active_sessions": len(_active_sessions)
|
|
}
|
|
|
|
|
|
@app.get("/sse")
|
|
async def sse_endpoint(
|
|
project_id: str = Query(..., description="Project ID for context isolation"),
|
|
user_id: Optional[int] = Query(None, description="User ID for user-scoped integrations")
|
|
):
|
|
"""
|
|
Server-Sent Events endpoint for MCP communication.
|
|
|
|
This endpoint maintains a persistent connection with the client
|
|
and streams MCP protocol messages.
|
|
"""
|
|
session_key = get_session_key(project_id, user_id)
|
|
|
|
logger.info(
|
|
"SSE connection established",
|
|
project_id=project_id,
|
|
user_id=user_id,
|
|
session_key=session_key
|
|
)
|
|
|
|
# Load project context
|
|
context_manager = get_context_manager()
|
|
try:
|
|
project_context = await context_manager.get_context(project_id, user_id)
|
|
if not project_context:
|
|
raise HTTPException(status_code=404, detail=f"Project not found: {project_id}")
|
|
except Exception as e:
|
|
logger.error("Failed to load project context", error=str(e))
|
|
raise HTTPException(status_code=500, detail=f"Failed to load project: {str(e)}")
|
|
|
|
# Create project tools instance
|
|
project_tools = ProjectTools(user_id)
|
|
|
|
# Track session
|
|
_active_sessions[session_key] = {
|
|
"project_id": project_id,
|
|
"user_id": user_id,
|
|
"connected_at": asyncio.get_event_loop().time(),
|
|
"project_tools": project_tools
|
|
}
|
|
|
|
async def event_generator():
|
|
"""Generate SSE events for MCP communication"""
|
|
try:
|
|
# Send initial connection confirmation
|
|
yield {
|
|
"event": "connected",
|
|
"data": json.dumps({
|
|
"project_id": project_id,
|
|
"project_name": project_context.name,
|
|
"available_tools": len(PROJECT_TOOLS),
|
|
"integrations_enabled": list(project_context.integrations.keys())
|
|
})
|
|
}
|
|
|
|
# Keep connection alive
|
|
while True:
|
|
await asyncio.sleep(30) # Heartbeat every 30 seconds
|
|
yield {
|
|
"event": "heartbeat",
|
|
"data": json.dumps({"timestamp": asyncio.get_event_loop().time()})
|
|
}
|
|
|
|
except asyncio.CancelledError:
|
|
logger.info("SSE connection closed", session_key=session_key)
|
|
finally:
|
|
# Cleanup session
|
|
if session_key in _active_sessions:
|
|
del _active_sessions[session_key]
|
|
|
|
return EventSourceResponse(event_generator())
|
|
|
|
|
|
# MCP Protocol Handlers
|
|
@mcp_server.list_tools()
|
|
async def list_tools() -> list[types.Tool]:
|
|
"""
|
|
List all available tools.
|
|
|
|
Tools are dynamically determined based on:
|
|
- Base DSS project tools (always available)
|
|
- Workflow orchestration tools
|
|
- Debug tools
|
|
- Storybook integration tools
|
|
- Dynamically loaded plugins
|
|
- User's enabled integrations (Figma, Jira, Confluence, etc.)
|
|
"""
|
|
# Start with base project tools
|
|
tools = PROJECT_TOOLS.copy()
|
|
|
|
# Add workflow orchestration tools
|
|
tools.extend(WORKFLOW_TOOLS)
|
|
|
|
# Add debug tools
|
|
tools.extend(DEBUG_TOOLS)
|
|
|
|
# Add Storybook integration tools
|
|
tools.extend(STORYBOOK_TOOLS)
|
|
|
|
# Add Translation tools
|
|
tools.extend(TRANSLATION_TOOLS)
|
|
|
|
# Add plugin tools
|
|
tools.extend(plugin_registry.get_all_tools())
|
|
|
|
# TODO: Add integration-specific tools based on user's enabled integrations
|
|
# This will be implemented in Phase 3
|
|
|
|
logger.debug("Listed tools", tool_count=len(tools), plugin_count=len(plugin_registry.plugins))
|
|
return tools
|
|
|
|
|
|
@mcp_server.call_tool()
|
|
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
|
"""
|
|
Execute a tool by name.
|
|
|
|
Args:
|
|
name: Tool name
|
|
arguments: Tool arguments (must include project_id)
|
|
|
|
Returns:
|
|
Tool execution results
|
|
"""
|
|
logger.info("Tool called", tool_name=name, arguments=arguments)
|
|
|
|
project_id = arguments.get("project_id")
|
|
if not project_id:
|
|
return [
|
|
types.TextContent(
|
|
type="text",
|
|
text=json.dumps({"error": "project_id is required"})
|
|
)
|
|
]
|
|
|
|
# Find active session for this project
|
|
# For now, use first matching session (can be enhanced with session management)
|
|
session_key = None
|
|
project_tools = None
|
|
|
|
for key, session in _active_sessions.items():
|
|
if session["project_id"] == project_id:
|
|
session_key = key
|
|
project_tools = session["project_tools"]
|
|
break
|
|
|
|
if not project_tools:
|
|
# Create temporary tools instance
|
|
project_tools = ProjectTools()
|
|
|
|
# Check if this is a workflow tool
|
|
workflow_tool_names = [tool.name for tool in WORKFLOW_TOOLS]
|
|
debug_tool_names = [tool.name for tool in DEBUG_TOOLS]
|
|
storybook_tool_names = [tool.name for tool in STORYBOOK_TOOLS]
|
|
translation_tool_names = [tool.name for tool in TRANSLATION_TOOLS]
|
|
|
|
# Execute tool
|
|
try:
|
|
if name in workflow_tool_names:
|
|
# Handle workflow orchestration tools
|
|
from .audit import AuditLog
|
|
audit_log = AuditLog()
|
|
workflow_tools = WorkflowTools(audit_log)
|
|
result = await workflow_tools.handle_tool_call(name, arguments)
|
|
elif name in debug_tool_names:
|
|
# Handle debug tools
|
|
debug_tools = DebugTools()
|
|
result = await debug_tools.execute_tool(name, arguments)
|
|
elif name in storybook_tool_names:
|
|
# Handle Storybook tools
|
|
from .integrations.storybook import StorybookTools
|
|
storybook_tools = StorybookTools()
|
|
result = await storybook_tools.execute_tool(name, arguments)
|
|
elif name in translation_tool_names:
|
|
# Handle Translation tools
|
|
from .integrations.translations import TranslationTools
|
|
translation_tools = TranslationTools()
|
|
result = await translation_tools.execute_tool(name, arguments)
|
|
elif name in plugin_registry.handlers:
|
|
# Handle plugin tools
|
|
result = await plugin_registry.execute_tool(name, arguments)
|
|
# Plugin tools return MCP content objects directly, not dicts
|
|
if isinstance(result, list):
|
|
return result
|
|
else:
|
|
# Handle regular project tools
|
|
result = await project_tools.execute_tool(name, arguments)
|
|
|
|
return [
|
|
types.TextContent(
|
|
type="text",
|
|
text=json.dumps(result, indent=2)
|
|
)
|
|
]
|
|
except Exception as e:
|
|
logger.error("Tool execution failed", tool_name=name, error=str(e))
|
|
return [
|
|
types.TextContent(
|
|
type="text",
|
|
text=json.dumps({"error": str(e)})
|
|
)
|
|
]
|
|
|
|
|
|
@mcp_server.list_resources()
|
|
async def list_resources() -> list[types.Resource]:
|
|
"""
|
|
List available resources.
|
|
|
|
Resources provide static or dynamic content that Claude can access.
|
|
Examples: project documentation, component specs, design system guidelines.
|
|
"""
|
|
# TODO: Implement resources based on project context
|
|
# For now, return empty list
|
|
return []
|
|
|
|
|
|
@mcp_server.read_resource()
|
|
async def read_resource(uri: str) -> str:
|
|
"""
|
|
Read a specific resource by URI.
|
|
|
|
Args:
|
|
uri: Resource URI (e.g., "dss://project-id/components/Button")
|
|
|
|
Returns:
|
|
Resource content
|
|
"""
|
|
# TODO: Implement resource reading
|
|
# For now, return not implemented
|
|
return json.dumps({"error": "Resource reading not yet implemented"})
|
|
|
|
|
|
@mcp_server.list_prompts()
|
|
async def list_prompts() -> list[types.Prompt]:
|
|
"""
|
|
List available prompt templates.
|
|
|
|
Prompts provide pre-configured conversation starters for Claude.
|
|
"""
|
|
# TODO: Add DSS-specific prompt templates
|
|
# Examples: "Analyze component consistency", "Review token usage", etc.
|
|
return []
|
|
|
|
|
|
@mcp_server.get_prompt()
|
|
async def get_prompt(name: str, arguments: dict) -> types.GetPromptResult:
|
|
"""
|
|
Get a specific prompt template.
|
|
|
|
Args:
|
|
name: Prompt name
|
|
arguments: Prompt arguments
|
|
|
|
Returns:
|
|
Prompt content
|
|
"""
|
|
# TODO: Implement prompt templates
|
|
return types.GetPromptResult(
|
|
description="Prompt not found",
|
|
messages=[]
|
|
)
|
|
|
|
|
|
# API endpoint to call MCP tools directly (for testing/debugging)
|
|
@app.post("/api/tools/{tool_name}")
|
|
async def call_tool_api(tool_name: str, arguments: Dict[str, Any]):
|
|
"""
|
|
Direct API endpoint to call MCP tools.
|
|
|
|
Useful for testing tools without MCP client.
|
|
"""
|
|
project_tools = ProjectTools()
|
|
result = await project_tools.execute_tool(tool_name, arguments)
|
|
return result
|
|
|
|
|
|
# API endpoint to list active sessions
|
|
@app.get("/api/sessions")
|
|
async def list_sessions():
|
|
"""List all active SSE sessions"""
|
|
return {
|
|
"active_sessions": len(_active_sessions),
|
|
"sessions": [
|
|
{
|
|
"project_id": session["project_id"],
|
|
"user_id": session["user_id"],
|
|
"connected_at": session["connected_at"]
|
|
}
|
|
for session in _active_sessions.values()
|
|
]
|
|
}
|
|
|
|
|
|
# API endpoint to clear context cache
|
|
@app.post("/api/cache/clear")
|
|
async def clear_cache(project_id: Optional[str] = None):
|
|
"""Clear context cache for a project or all projects"""
|
|
context_manager = get_context_manager()
|
|
context_manager.clear_cache(project_id)
|
|
|
|
return {
|
|
"status": "cache_cleared",
|
|
"project_id": project_id or "all"
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
|
|
logger.info(
|
|
"Starting DSS MCP Server",
|
|
host=mcp_config.HOST,
|
|
port=mcp_config.PORT
|
|
)
|
|
|
|
uvicorn.run(
|
|
"server:app",
|
|
host=mcp_config.HOST,
|
|
port=mcp_config.PORT,
|
|
reload=True,
|
|
log_level=mcp_config.LOG_LEVEL.lower()
|
|
)
|