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
429 lines
14 KiB
Python
429 lines
14 KiB
Python
"""
|
|
Unified MCP Handler
|
|
|
|
Central handler for all MCP tool execution. Used by:
|
|
- Direct API calls (/api/mcp/tools/{name}/execute)
|
|
- Claude chat (inline tool execution)
|
|
- SSE streaming connections
|
|
|
|
This module ensures all MCP requests go through a single code path
|
|
for consistent logging, error handling, and security.
|
|
"""
|
|
|
|
import json
|
|
import asyncio
|
|
from typing import Dict, Any, List, Optional, Tuple
|
|
from datetime import datetime
|
|
from dataclasses import dataclass, asdict
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from storage.database import get_connection
|
|
from .config import mcp_config, integration_config
|
|
from .context.project_context import get_context_manager, ProjectContext
|
|
from .tools.project_tools import PROJECT_TOOLS, ProjectTools
|
|
from .integrations.figma import FIGMA_TOOLS, FigmaTools
|
|
from .integrations.jira import JIRA_TOOLS, JiraTools
|
|
from .integrations.confluence import CONFLUENCE_TOOLS, ConfluenceTools
|
|
from .integrations.base import CircuitBreakerOpen
|
|
|
|
|
|
@dataclass
|
|
class ToolResult:
|
|
"""Result of a tool execution"""
|
|
tool_name: str
|
|
success: bool
|
|
result: Any
|
|
error: Optional[str] = None
|
|
duration_ms: int = 0
|
|
timestamp: str = None
|
|
|
|
def __post_init__(self):
|
|
if not self.timestamp:
|
|
self.timestamp = datetime.now().isoformat()
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return asdict(self)
|
|
|
|
|
|
@dataclass
|
|
class MCPContext:
|
|
"""Context for MCP operations"""
|
|
project_id: str
|
|
user_id: Optional[int] = None
|
|
session_id: Optional[str] = None
|
|
|
|
|
|
class MCPHandler:
|
|
"""
|
|
Unified MCP tool handler.
|
|
|
|
Provides:
|
|
- Tool discovery (list all available tools)
|
|
- Tool execution with proper context
|
|
- Integration management
|
|
- Logging and metrics
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.context_manager = get_context_manager()
|
|
self._tool_registry: Dict[str, Dict[str, Any]] = {}
|
|
self._initialize_tools()
|
|
|
|
def _initialize_tools(self):
|
|
"""Initialize tool registry with all available tools"""
|
|
# Register base project tools
|
|
for tool in PROJECT_TOOLS:
|
|
self._tool_registry[tool.name] = {
|
|
"tool": tool,
|
|
"category": "project",
|
|
"requires_integration": False
|
|
}
|
|
|
|
# Register Figma tools
|
|
for tool in FIGMA_TOOLS:
|
|
self._tool_registry[tool.name] = {
|
|
"tool": tool,
|
|
"category": "figma",
|
|
"requires_integration": True,
|
|
"integration_type": "figma"
|
|
}
|
|
|
|
# Register Jira tools
|
|
for tool in JIRA_TOOLS:
|
|
self._tool_registry[tool.name] = {
|
|
"tool": tool,
|
|
"category": "jira",
|
|
"requires_integration": True,
|
|
"integration_type": "jira"
|
|
}
|
|
|
|
# Register Confluence tools
|
|
for tool in CONFLUENCE_TOOLS:
|
|
self._tool_registry[tool.name] = {
|
|
"tool": tool,
|
|
"category": "confluence",
|
|
"requires_integration": True,
|
|
"integration_type": "confluence"
|
|
}
|
|
|
|
def list_tools(self, include_details: bool = False) -> Dict[str, Any]:
|
|
"""
|
|
List all available MCP tools.
|
|
|
|
Args:
|
|
include_details: Include full tool schemas
|
|
|
|
Returns:
|
|
Tool listing by category
|
|
"""
|
|
tools_by_category = {}
|
|
|
|
for name, info in self._tool_registry.items():
|
|
category = info["category"]
|
|
if category not in tools_by_category:
|
|
tools_by_category[category] = []
|
|
|
|
tool_info = {
|
|
"name": name,
|
|
"description": info["tool"].description,
|
|
"requires_integration": info.get("requires_integration", False)
|
|
}
|
|
|
|
if include_details:
|
|
tool_info["input_schema"] = info["tool"].inputSchema
|
|
|
|
tools_by_category[category].append(tool_info)
|
|
|
|
return {
|
|
"tools": tools_by_category,
|
|
"total_count": len(self._tool_registry)
|
|
}
|
|
|
|
def get_tool_info(self, tool_name: str) -> Optional[Dict[str, Any]]:
|
|
"""Get information about a specific tool"""
|
|
if tool_name not in self._tool_registry:
|
|
return None
|
|
|
|
info = self._tool_registry[tool_name]
|
|
return {
|
|
"name": tool_name,
|
|
"description": info["tool"].description,
|
|
"category": info["category"],
|
|
"input_schema": info["tool"].inputSchema,
|
|
"requires_integration": info.get("requires_integration", False),
|
|
"integration_type": info.get("integration_type")
|
|
}
|
|
|
|
async def execute_tool(
|
|
self,
|
|
tool_name: str,
|
|
arguments: Dict[str, Any],
|
|
context: MCPContext
|
|
) -> ToolResult:
|
|
"""
|
|
Execute an MCP tool.
|
|
|
|
Args:
|
|
tool_name: Name of the tool to execute
|
|
arguments: Tool arguments
|
|
context: MCP context (project_id, user_id)
|
|
|
|
Returns:
|
|
ToolResult with success/failure and data
|
|
"""
|
|
start_time = datetime.now()
|
|
|
|
# Check if tool exists
|
|
if tool_name not in self._tool_registry:
|
|
return ToolResult(
|
|
tool_name=tool_name,
|
|
success=False,
|
|
result=None,
|
|
error=f"Unknown tool: {tool_name}"
|
|
)
|
|
|
|
tool_info = self._tool_registry[tool_name]
|
|
category = tool_info["category"]
|
|
|
|
try:
|
|
# Execute based on category
|
|
if category == "project":
|
|
result = await self._execute_project_tool(tool_name, arguments, context)
|
|
elif category == "figma":
|
|
result = await self._execute_figma_tool(tool_name, arguments, context)
|
|
elif category == "jira":
|
|
result = await self._execute_jira_tool(tool_name, arguments, context)
|
|
elif category == "confluence":
|
|
result = await self._execute_confluence_tool(tool_name, arguments, context)
|
|
else:
|
|
result = {"error": f"Unknown tool category: {category}"}
|
|
|
|
# Check for error in result
|
|
success = "error" not in result
|
|
error = result.get("error") if not success else None
|
|
|
|
# Calculate duration
|
|
duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
|
|
|
|
# Log execution
|
|
await self._log_tool_usage(
|
|
tool_name=tool_name,
|
|
category=category,
|
|
project_id=context.project_id,
|
|
user_id=context.user_id,
|
|
success=success,
|
|
duration_ms=duration_ms,
|
|
error=error
|
|
)
|
|
|
|
return ToolResult(
|
|
tool_name=tool_name,
|
|
success=success,
|
|
result=result if success else None,
|
|
error=error,
|
|
duration_ms=duration_ms
|
|
)
|
|
|
|
except CircuitBreakerOpen as e:
|
|
duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
|
|
return ToolResult(
|
|
tool_name=tool_name,
|
|
success=False,
|
|
result=None,
|
|
error=str(e),
|
|
duration_ms=duration_ms
|
|
)
|
|
except Exception as e:
|
|
duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
|
|
await self._log_tool_usage(
|
|
tool_name=tool_name,
|
|
category=category,
|
|
project_id=context.project_id,
|
|
user_id=context.user_id,
|
|
success=False,
|
|
duration_ms=duration_ms,
|
|
error=str(e)
|
|
)
|
|
return ToolResult(
|
|
tool_name=tool_name,
|
|
success=False,
|
|
result=None,
|
|
error=str(e),
|
|
duration_ms=duration_ms
|
|
)
|
|
|
|
async def _execute_project_tool(
|
|
self,
|
|
tool_name: str,
|
|
arguments: Dict[str, Any],
|
|
context: MCPContext
|
|
) -> Dict[str, Any]:
|
|
"""Execute a project tool"""
|
|
# Ensure project_id is set
|
|
if "project_id" not in arguments:
|
|
arguments["project_id"] = context.project_id
|
|
|
|
project_tools = ProjectTools(context.user_id)
|
|
return await project_tools.execute_tool(tool_name, arguments)
|
|
|
|
async def _execute_figma_tool(
|
|
self,
|
|
tool_name: str,
|
|
arguments: Dict[str, Any],
|
|
context: MCPContext
|
|
) -> Dict[str, Any]:
|
|
"""Execute a Figma tool"""
|
|
# Get Figma config
|
|
config = await self._get_integration_config("figma", context)
|
|
if not config:
|
|
# Try global config
|
|
if integration_config.FIGMA_TOKEN:
|
|
config = {"api_token": integration_config.FIGMA_TOKEN}
|
|
else:
|
|
return {"error": "Figma not configured. Please add Figma API token."}
|
|
|
|
figma_tools = FigmaTools(config)
|
|
return await figma_tools.execute_tool(tool_name, arguments)
|
|
|
|
async def _execute_jira_tool(
|
|
self,
|
|
tool_name: str,
|
|
arguments: Dict[str, Any],
|
|
context: MCPContext
|
|
) -> Dict[str, Any]:
|
|
"""Execute a Jira tool"""
|
|
config = await self._get_integration_config("jira", context)
|
|
if not config:
|
|
return {"error": "Jira not configured. Please configure Jira integration."}
|
|
|
|
jira_tools = JiraTools(config)
|
|
return await jira_tools.execute_tool(tool_name, arguments)
|
|
|
|
async def _execute_confluence_tool(
|
|
self,
|
|
tool_name: str,
|
|
arguments: Dict[str, Any],
|
|
context: MCPContext
|
|
) -> Dict[str, Any]:
|
|
"""Execute a Confluence tool"""
|
|
config = await self._get_integration_config("confluence", context)
|
|
if not config:
|
|
return {"error": "Confluence not configured. Please configure Confluence integration."}
|
|
|
|
confluence_tools = ConfluenceTools(config)
|
|
return await confluence_tools.execute_tool(tool_name, arguments)
|
|
|
|
async def _get_integration_config(
|
|
self,
|
|
integration_type: str,
|
|
context: MCPContext
|
|
) -> Optional[Dict[str, Any]]:
|
|
"""Get decrypted integration config for user/project"""
|
|
if not context.user_id or not context.project_id:
|
|
return None
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
def get_config():
|
|
try:
|
|
with get_connection() as conn:
|
|
row = conn.execute(
|
|
"""
|
|
SELECT config FROM project_integrations
|
|
WHERE project_id = ? AND user_id = ? AND integration_type = ? AND enabled = 1
|
|
""",
|
|
(context.project_id, context.user_id, integration_type)
|
|
).fetchone()
|
|
|
|
if not row:
|
|
return None
|
|
|
|
encrypted_config = row["config"]
|
|
|
|
# Decrypt
|
|
cipher = mcp_config.get_cipher()
|
|
if cipher:
|
|
try:
|
|
decrypted = cipher.decrypt(encrypted_config.encode()).decode()
|
|
return json.loads(decrypted)
|
|
except:
|
|
pass
|
|
|
|
# Try parsing as plain JSON
|
|
try:
|
|
return json.loads(encrypted_config)
|
|
except:
|
|
return None
|
|
except:
|
|
return None
|
|
|
|
return await loop.run_in_executor(None, get_config)
|
|
|
|
async def _log_tool_usage(
|
|
self,
|
|
tool_name: str,
|
|
category: str,
|
|
project_id: str,
|
|
user_id: Optional[int],
|
|
success: bool,
|
|
duration_ms: int,
|
|
error: Optional[str] = None
|
|
):
|
|
"""Log tool execution to database"""
|
|
loop = asyncio.get_event_loop()
|
|
|
|
def log():
|
|
try:
|
|
with get_connection() as conn:
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO mcp_tool_usage
|
|
(project_id, user_id, tool_name, tool_category, duration_ms, success, error_message)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(project_id, user_id, tool_name, category, duration_ms, success, error)
|
|
)
|
|
except:
|
|
pass # Don't fail on logging errors
|
|
|
|
await loop.run_in_executor(None, log)
|
|
|
|
async def get_project_context(
|
|
self,
|
|
project_id: str,
|
|
user_id: Optional[int] = None
|
|
) -> Optional[ProjectContext]:
|
|
"""Get project context for Claude system prompt"""
|
|
return await self.context_manager.get_context(project_id, user_id)
|
|
|
|
def get_tools_for_claude(self) -> List[Dict[str, Any]]:
|
|
"""
|
|
Get tools formatted for Claude's tool_use feature.
|
|
|
|
Returns:
|
|
List of tools in Anthropic's tool format
|
|
"""
|
|
tools = []
|
|
for name, info in self._tool_registry.items():
|
|
tools.append({
|
|
"name": name,
|
|
"description": info["tool"].description,
|
|
"input_schema": info["tool"].inputSchema
|
|
})
|
|
return tools
|
|
|
|
|
|
# Singleton instance
|
|
_mcp_handler: Optional[MCPHandler] = None
|
|
|
|
|
|
def get_mcp_handler() -> MCPHandler:
|
|
"""Get singleton MCP handler instance"""
|
|
global _mcp_handler
|
|
if _mcp_handler is None:
|
|
_mcp_handler = MCPHandler()
|
|
return _mcp_handler
|