Files
dss/demo/tools/dss_mcp/handler.py
Digital Production Factory 276ed71f31 Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm

Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)

Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability

Migration completed: $(date)
🤖 Clean migration with full functionality preserved
2025-12-09 18:45:48 -03:00

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