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
This commit is contained in:
0
demo/tools/dss_mcp/integrations/__init__.py
Normal file
0
demo/tools/dss_mcp/integrations/__init__.py
Normal file
264
demo/tools/dss_mcp/integrations/base.py
Normal file
264
demo/tools/dss_mcp/integrations/base.py
Normal file
@@ -0,0 +1,264 @@
|
||||
"""
|
||||
Base Integration Classes
|
||||
|
||||
Provides circuit breaker pattern and base classes for external integrations.
|
||||
"""
|
||||
|
||||
import time
|
||||
import asyncio
|
||||
from typing import Callable, Any, Optional, Dict
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
|
||||
from ..config import mcp_config
|
||||
from storage.database import get_connection
|
||||
|
||||
|
||||
class CircuitState(Enum):
|
||||
"""Circuit breaker states"""
|
||||
CLOSED = "closed" # Normal operation
|
||||
OPEN = "open" # Failing, reject requests
|
||||
HALF_OPEN = "half_open" # Testing if service recovered
|
||||
|
||||
|
||||
@dataclass
|
||||
class CircuitBreakerStats:
|
||||
"""Circuit breaker statistics"""
|
||||
state: CircuitState
|
||||
failure_count: int
|
||||
success_count: int
|
||||
last_failure_time: Optional[float]
|
||||
last_success_time: Optional[float]
|
||||
opened_at: Optional[float]
|
||||
next_retry_time: Optional[float]
|
||||
|
||||
|
||||
class CircuitBreakerOpen(Exception):
|
||||
"""Exception raised when circuit breaker is open"""
|
||||
pass
|
||||
|
||||
|
||||
class CircuitBreaker:
|
||||
"""
|
||||
Circuit Breaker pattern implementation.
|
||||
|
||||
Protects external service calls from cascading failures.
|
||||
Three states: CLOSED (normal), OPEN (failing), HALF_OPEN (testing).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
integration_type: str,
|
||||
failure_threshold: int = None,
|
||||
timeout_seconds: int = None,
|
||||
half_open_max_calls: int = 3
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
integration_type: Type of integration (figma, jira, confluence, etc.)
|
||||
failure_threshold: Number of failures before opening circuit
|
||||
timeout_seconds: Seconds to wait before trying again
|
||||
half_open_max_calls: Max successful calls in half-open before closing
|
||||
"""
|
||||
self.integration_type = integration_type
|
||||
self.failure_threshold = failure_threshold or mcp_config.CIRCUIT_BREAKER_FAILURE_THRESHOLD
|
||||
self.timeout_seconds = timeout_seconds or mcp_config.CIRCUIT_BREAKER_TIMEOUT_SECONDS
|
||||
self.half_open_max_calls = half_open_max_calls
|
||||
|
||||
# In-memory state (could be moved to Redis for distributed setup)
|
||||
self.state = CircuitState.CLOSED
|
||||
self.failure_count = 0
|
||||
self.success_count = 0
|
||||
self.last_failure_time: Optional[float] = None
|
||||
self.last_success_time: Optional[float] = None
|
||||
self.opened_at: Optional[float] = None
|
||||
|
||||
async def call(self, func: Callable, *args, **kwargs) -> Any:
|
||||
"""
|
||||
Call a function through the circuit breaker.
|
||||
|
||||
Args:
|
||||
func: Function to call (can be sync or async)
|
||||
*args, **kwargs: Arguments to pass to func
|
||||
|
||||
Returns:
|
||||
Function result
|
||||
|
||||
Raises:
|
||||
CircuitBreakerOpen: If circuit is open
|
||||
Exception: Original exception from func if it fails
|
||||
"""
|
||||
# Check circuit state
|
||||
if self.state == CircuitState.OPEN:
|
||||
# Check if timeout has elapsed
|
||||
if time.time() - self.opened_at < self.timeout_seconds:
|
||||
await self._record_failure("Circuit breaker is OPEN", db_only=True)
|
||||
raise CircuitBreakerOpen(
|
||||
f"{self.integration_type} service is temporarily unavailable. "
|
||||
f"Retry after {self._seconds_until_retry():.0f}s"
|
||||
)
|
||||
else:
|
||||
# Timeout elapsed, move to HALF_OPEN
|
||||
self.state = CircuitState.HALF_OPEN
|
||||
self.success_count = 0
|
||||
|
||||
# Execute function
|
||||
try:
|
||||
# Handle both sync and async functions
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
result = await func(*args, **kwargs)
|
||||
else:
|
||||
result = func(*args, **kwargs)
|
||||
|
||||
# Success!
|
||||
await self._record_success()
|
||||
|
||||
# If in HALF_OPEN, check if we can close the circuit
|
||||
if self.state == CircuitState.HALF_OPEN:
|
||||
if self.success_count >= self.half_open_max_calls:
|
||||
self.state = CircuitState.CLOSED
|
||||
self.failure_count = 0
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
# Failure
|
||||
await self._record_failure(str(e))
|
||||
|
||||
# Check if we should open the circuit
|
||||
if self.failure_count >= self.failure_threshold:
|
||||
self.state = CircuitState.OPEN
|
||||
self.opened_at = time.time()
|
||||
|
||||
raise
|
||||
|
||||
async def _record_success(self):
|
||||
"""Record successful call"""
|
||||
self.success_count += 1
|
||||
self.last_success_time = time.time()
|
||||
|
||||
# Update database
|
||||
await self._update_health_db(is_healthy=True, error=None)
|
||||
|
||||
async def _record_failure(self, error_message: str, db_only: bool = False):
|
||||
"""Record failed call"""
|
||||
if not db_only:
|
||||
self.failure_count += 1
|
||||
self.last_failure_time = time.time()
|
||||
|
||||
# Update database
|
||||
await self._update_health_db(is_healthy=False, error=error_message)
|
||||
|
||||
async def _update_health_db(self, is_healthy: bool, error: Optional[str]):
|
||||
"""Update integration health in database"""
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
def update_db():
|
||||
try:
|
||||
with get_connection() as conn:
|
||||
circuit_open_until = None
|
||||
if self.state == CircuitState.OPEN and self.opened_at:
|
||||
circuit_open_until = datetime.fromtimestamp(
|
||||
self.opened_at + self.timeout_seconds
|
||||
).isoformat()
|
||||
|
||||
if is_healthy:
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE integration_health
|
||||
SET is_healthy = 1,
|
||||
failure_count = 0,
|
||||
last_success_at = CURRENT_TIMESTAMP,
|
||||
circuit_open_until = NULL,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE integration_type = ?
|
||||
""",
|
||||
(self.integration_type,)
|
||||
)
|
||||
else:
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE integration_health
|
||||
SET is_healthy = 0,
|
||||
failure_count = ?,
|
||||
last_failure_at = CURRENT_TIMESTAMP,
|
||||
circuit_open_until = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE integration_type = ?
|
||||
""",
|
||||
(self.failure_count, circuit_open_until, self.integration_type)
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error updating integration health: {e}")
|
||||
|
||||
await loop.run_in_executor(None, update_db)
|
||||
|
||||
def _seconds_until_retry(self) -> float:
|
||||
"""Get seconds until circuit can be retried"""
|
||||
if self.state != CircuitState.OPEN or not self.opened_at:
|
||||
return 0
|
||||
elapsed = time.time() - self.opened_at
|
||||
remaining = self.timeout_seconds - elapsed
|
||||
return max(0, remaining)
|
||||
|
||||
def get_stats(self) -> CircuitBreakerStats:
|
||||
"""Get current circuit breaker statistics"""
|
||||
next_retry_time = None
|
||||
if self.state == CircuitState.OPEN and self.opened_at:
|
||||
next_retry_time = self.opened_at + self.timeout_seconds
|
||||
|
||||
return CircuitBreakerStats(
|
||||
state=self.state,
|
||||
failure_count=self.failure_count,
|
||||
success_count=self.success_count,
|
||||
last_failure_time=self.last_failure_time,
|
||||
last_success_time=self.last_success_time,
|
||||
opened_at=self.opened_at,
|
||||
next_retry_time=next_retry_time
|
||||
)
|
||||
|
||||
|
||||
class BaseIntegration:
|
||||
"""Base class for all external integrations"""
|
||||
|
||||
def __init__(self, integration_type: str, config: Dict[str, Any]):
|
||||
"""
|
||||
Args:
|
||||
integration_type: Type of integration (figma, jira, etc.)
|
||||
config: Integration configuration (decrypted)
|
||||
"""
|
||||
self.integration_type = integration_type
|
||||
self.config = config
|
||||
self.circuit_breaker = CircuitBreaker(integration_type)
|
||||
|
||||
async def call_api(self, func: Callable, *args, **kwargs) -> Any:
|
||||
"""
|
||||
Call external API through circuit breaker.
|
||||
|
||||
Args:
|
||||
func: API function to call
|
||||
*args, **kwargs: Arguments to pass
|
||||
|
||||
Returns:
|
||||
API response
|
||||
|
||||
Raises:
|
||||
CircuitBreakerOpen: If circuit is open
|
||||
Exception: Original API exception
|
||||
"""
|
||||
return await self.circuit_breaker.call(func, *args, **kwargs)
|
||||
|
||||
def get_health(self) -> Dict[str, Any]:
|
||||
"""Get integration health status"""
|
||||
stats = self.circuit_breaker.get_stats()
|
||||
return {
|
||||
"integration_type": self.integration_type,
|
||||
"state": stats.state.value,
|
||||
"is_healthy": stats.state == CircuitState.CLOSED,
|
||||
"failure_count": stats.failure_count,
|
||||
"success_count": stats.success_count,
|
||||
"last_failure_time": stats.last_failure_time,
|
||||
"last_success_time": stats.last_success_time,
|
||||
"next_retry_time": stats.next_retry_time
|
||||
}
|
||||
262
demo/tools/dss_mcp/integrations/confluence.py
Normal file
262
demo/tools/dss_mcp/integrations/confluence.py
Normal file
@@ -0,0 +1,262 @@
|
||||
"""
|
||||
Confluence Integration for MCP
|
||||
|
||||
Provides Confluence API tools for documentation and knowledge base.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from atlassian import Confluence
|
||||
from mcp import types
|
||||
|
||||
from .base import BaseIntegration
|
||||
|
||||
|
||||
# Confluence MCP Tool Definitions
|
||||
CONFLUENCE_TOOLS = [
|
||||
types.Tool(
|
||||
name="confluence_create_page",
|
||||
description="Create a new Confluence page",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"space_key": {
|
||||
"type": "string",
|
||||
"description": "Confluence space key"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Page title"
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"description": "Page content (HTML or wiki markup)"
|
||||
},
|
||||
"parent_id": {
|
||||
"type": "string",
|
||||
"description": "Optional parent page ID"
|
||||
}
|
||||
},
|
||||
"required": ["space_key", "title", "body"]
|
||||
}
|
||||
),
|
||||
types.Tool(
|
||||
name="confluence_get_page",
|
||||
description="Get Confluence page by ID or title",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"page_id": {
|
||||
"type": "string",
|
||||
"description": "Page ID (use this OR title)"
|
||||
},
|
||||
"space_key": {
|
||||
"type": "string",
|
||||
"description": "Space key (required if using title)"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Page title (use this OR page_id)"
|
||||
},
|
||||
"expand": {
|
||||
"type": "string",
|
||||
"description": "Comma-separated list of expansions (body.storage, version, etc.)",
|
||||
"default": "body.storage,version"
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
types.Tool(
|
||||
name="confluence_update_page",
|
||||
description="Update an existing Confluence page",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"page_id": {
|
||||
"type": "string",
|
||||
"description": "Page ID to update"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "New page title"
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"description": "New page content"
|
||||
}
|
||||
},
|
||||
"required": ["page_id", "title", "body"]
|
||||
}
|
||||
),
|
||||
types.Tool(
|
||||
name="confluence_search",
|
||||
description="Search Confluence pages using CQL",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cql": {
|
||||
"type": "string",
|
||||
"description": "CQL query (e.g., 'space=DSS AND type=page')"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of results",
|
||||
"default": 25
|
||||
}
|
||||
},
|
||||
"required": ["cql"]
|
||||
}
|
||||
),
|
||||
types.Tool(
|
||||
name="confluence_get_space",
|
||||
description="Get Confluence space details",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"space_key": {
|
||||
"type": "string",
|
||||
"description": "Space key"
|
||||
}
|
||||
},
|
||||
"required": ["space_key"]
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class ConfluenceIntegration(BaseIntegration):
|
||||
"""Confluence API integration with circuit breaker"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
"""
|
||||
Initialize Confluence integration.
|
||||
|
||||
Args:
|
||||
config: Must contain 'url', 'username', 'api_token'
|
||||
"""
|
||||
super().__init__("confluence", config)
|
||||
|
||||
url = config.get("url")
|
||||
username = config.get("username")
|
||||
api_token = config.get("api_token")
|
||||
|
||||
if not all([url, username, api_token]):
|
||||
raise ValueError("Confluence configuration incomplete: url, username, api_token required")
|
||||
|
||||
self.confluence = Confluence(
|
||||
url=url,
|
||||
username=username,
|
||||
password=api_token,
|
||||
cloud=True
|
||||
)
|
||||
|
||||
async def create_page(
|
||||
self,
|
||||
space_key: str,
|
||||
title: str,
|
||||
body: str,
|
||||
parent_id: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new page"""
|
||||
def _create():
|
||||
return self.confluence.create_page(
|
||||
space=space_key,
|
||||
title=title,
|
||||
body=body,
|
||||
parent_id=parent_id,
|
||||
representation="storage"
|
||||
)
|
||||
|
||||
return await self.call_api(_create)
|
||||
|
||||
async def get_page(
|
||||
self,
|
||||
page_id: Optional[str] = None,
|
||||
space_key: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
expand: str = "body.storage,version"
|
||||
) -> Dict[str, Any]:
|
||||
"""Get page by ID or title"""
|
||||
def _get():
|
||||
if page_id:
|
||||
return self.confluence.get_page_by_id(
|
||||
page_id=page_id,
|
||||
expand=expand
|
||||
)
|
||||
elif space_key and title:
|
||||
return self.confluence.get_page_by_title(
|
||||
space=space_key,
|
||||
title=title,
|
||||
expand=expand
|
||||
)
|
||||
else:
|
||||
raise ValueError("Must provide either page_id or (space_key + title)")
|
||||
|
||||
return await self.call_api(_get)
|
||||
|
||||
async def update_page(
|
||||
self,
|
||||
page_id: str,
|
||||
title: str,
|
||||
body: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Update an existing page"""
|
||||
def _update():
|
||||
# Get current version
|
||||
page = self.confluence.get_page_by_id(page_id, expand="version")
|
||||
current_version = page["version"]["number"]
|
||||
|
||||
return self.confluence.update_page(
|
||||
page_id=page_id,
|
||||
title=title,
|
||||
body=body,
|
||||
parent_id=None,
|
||||
type="page",
|
||||
representation="storage",
|
||||
minor_edit=False,
|
||||
version_comment="Updated via DSS MCP",
|
||||
version_number=current_version + 1
|
||||
)
|
||||
|
||||
return await self.call_api(_update)
|
||||
|
||||
async def search(self, cql: str, limit: int = 25) -> Dict[str, Any]:
|
||||
"""Search pages using CQL"""
|
||||
def _search():
|
||||
return self.confluence.cql(cql, limit=limit)
|
||||
|
||||
return await self.call_api(_search)
|
||||
|
||||
async def get_space(self, space_key: str) -> Dict[str, Any]:
|
||||
"""Get space details"""
|
||||
def _get():
|
||||
return self.confluence.get_space(space_key)
|
||||
|
||||
return await self.call_api(_get)
|
||||
|
||||
|
||||
class ConfluenceTools:
|
||||
"""MCP tool executor for Confluence integration"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.confluence = ConfluenceIntegration(config)
|
||||
|
||||
async def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Execute Confluence tool"""
|
||||
handlers = {
|
||||
"confluence_create_page": self.confluence.create_page,
|
||||
"confluence_get_page": self.confluence.get_page,
|
||||
"confluence_update_page": self.confluence.update_page,
|
||||
"confluence_search": self.confluence.search,
|
||||
"confluence_get_space": self.confluence.get_space
|
||||
}
|
||||
|
||||
handler = handlers.get(tool_name)
|
||||
if not handler:
|
||||
return {"error": f"Unknown Confluence tool: {tool_name}"}
|
||||
|
||||
try:
|
||||
clean_args = {k: v for k, v in arguments.items() if not k.startswith("_")}
|
||||
result = await handler(**clean_args)
|
||||
return result
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
260
demo/tools/dss_mcp/integrations/figma.py
Normal file
260
demo/tools/dss_mcp/integrations/figma.py
Normal file
@@ -0,0 +1,260 @@
|
||||
"""
|
||||
Figma Integration for MCP
|
||||
|
||||
Provides Figma API tools through circuit breaker pattern.
|
||||
"""
|
||||
|
||||
import httpx
|
||||
from typing import Dict, Any, List, Optional
|
||||
from mcp import types
|
||||
|
||||
from .base import BaseIntegration
|
||||
from ..config import integration_config
|
||||
|
||||
|
||||
# Figma MCP Tool Definitions
|
||||
FIGMA_TOOLS = [
|
||||
types.Tool(
|
||||
name="figma_get_file",
|
||||
description="Get Figma file metadata and structure",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_key": {
|
||||
"type": "string",
|
||||
"description": "Figma file key"
|
||||
}
|
||||
},
|
||||
"required": ["file_key"]
|
||||
}
|
||||
),
|
||||
types.Tool(
|
||||
name="figma_get_styles",
|
||||
description="Get design styles (colors, text, effects) from Figma file",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_key": {
|
||||
"type": "string",
|
||||
"description": "Figma file key"
|
||||
}
|
||||
},
|
||||
"required": ["file_key"]
|
||||
}
|
||||
),
|
||||
types.Tool(
|
||||
name="figma_get_components",
|
||||
description="Get component definitions from Figma file",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_key": {
|
||||
"type": "string",
|
||||
"description": "Figma file key"
|
||||
}
|
||||
},
|
||||
"required": ["file_key"]
|
||||
}
|
||||
),
|
||||
types.Tool(
|
||||
name="figma_extract_tokens",
|
||||
description="Extract design tokens (variables) from Figma file",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_key": {
|
||||
"type": "string",
|
||||
"description": "Figma file key"
|
||||
}
|
||||
},
|
||||
"required": ["file_key"]
|
||||
}
|
||||
),
|
||||
types.Tool(
|
||||
name="figma_get_node",
|
||||
description="Get specific node/component by ID from Figma file",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_key": {
|
||||
"type": "string",
|
||||
"description": "Figma file key"
|
||||
},
|
||||
"node_id": {
|
||||
"type": "string",
|
||||
"description": "Node ID to fetch"
|
||||
}
|
||||
},
|
||||
"required": ["file_key", "node_id"]
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class FigmaIntegration(BaseIntegration):
|
||||
"""Figma API integration with circuit breaker"""
|
||||
|
||||
FIGMA_API_BASE = "https://api.figma.com/v1"
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
"""
|
||||
Initialize Figma integration.
|
||||
|
||||
Args:
|
||||
config: Must contain 'api_token' or use FIGMA_TOKEN from env
|
||||
"""
|
||||
super().__init__("figma", config)
|
||||
self.api_token = config.get("api_token") or integration_config.FIGMA_TOKEN
|
||||
|
||||
if not self.api_token:
|
||||
raise ValueError("Figma API token not configured")
|
||||
|
||||
self.headers = {
|
||||
"X-Figma-Token": self.api_token
|
||||
}
|
||||
|
||||
async def get_file(self, file_key: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get Figma file metadata and structure.
|
||||
|
||||
Args:
|
||||
file_key: Figma file key
|
||||
|
||||
Returns:
|
||||
File data
|
||||
"""
|
||||
async def _fetch():
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{self.FIGMA_API_BASE}/files/{file_key}",
|
||||
headers=self.headers,
|
||||
timeout=30.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
return await self.call_api(_fetch)
|
||||
|
||||
async def get_styles(self, file_key: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get all styles from Figma file.
|
||||
|
||||
Args:
|
||||
file_key: Figma file key
|
||||
|
||||
Returns:
|
||||
Styles data
|
||||
"""
|
||||
async def _fetch():
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{self.FIGMA_API_BASE}/files/{file_key}/styles",
|
||||
headers=self.headers,
|
||||
timeout=30.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
return await self.call_api(_fetch)
|
||||
|
||||
async def get_components(self, file_key: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get all components from Figma file.
|
||||
|
||||
Args:
|
||||
file_key: Figma file key
|
||||
|
||||
Returns:
|
||||
Components data
|
||||
"""
|
||||
async def _fetch():
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{self.FIGMA_API_BASE}/files/{file_key}/components",
|
||||
headers=self.headers,
|
||||
timeout=30.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
return await self.call_api(_fetch)
|
||||
|
||||
async def extract_tokens(self, file_key: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract design tokens (variables) from Figma file.
|
||||
|
||||
Args:
|
||||
file_key: Figma file key
|
||||
|
||||
Returns:
|
||||
Variables/tokens data
|
||||
"""
|
||||
async def _fetch():
|
||||
async with httpx.AsyncClient() as client:
|
||||
# Get local variables
|
||||
response = await client.get(
|
||||
f"{self.FIGMA_API_BASE}/files/{file_key}/variables/local",
|
||||
headers=self.headers,
|
||||
timeout=30.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
return await self.call_api(_fetch)
|
||||
|
||||
async def get_node(self, file_key: str, node_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get specific node from Figma file.
|
||||
|
||||
Args:
|
||||
file_key: Figma file key
|
||||
node_id: Node ID
|
||||
|
||||
Returns:
|
||||
Node data
|
||||
"""
|
||||
async def _fetch():
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{self.FIGMA_API_BASE}/files/{file_key}/nodes",
|
||||
headers=self.headers,
|
||||
params={"ids": node_id},
|
||||
timeout=30.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
return await self.call_api(_fetch)
|
||||
|
||||
|
||||
class FigmaTools:
|
||||
"""MCP tool executor for Figma integration"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
"""
|
||||
Args:
|
||||
config: Figma configuration (with api_token)
|
||||
"""
|
||||
self.figma = FigmaIntegration(config)
|
||||
|
||||
async def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Execute Figma tool"""
|
||||
handlers = {
|
||||
"figma_get_file": self.figma.get_file,
|
||||
"figma_get_styles": self.figma.get_styles,
|
||||
"figma_get_components": self.figma.get_components,
|
||||
"figma_extract_tokens": self.figma.extract_tokens,
|
||||
"figma_get_node": self.figma.get_node
|
||||
}
|
||||
|
||||
handler = handlers.get(tool_name)
|
||||
if not handler:
|
||||
return {"error": f"Unknown Figma tool: {tool_name}"}
|
||||
|
||||
try:
|
||||
# Remove tool-specific prefix from arguments if needed
|
||||
clean_args = {k: v for k, v in arguments.items() if not k.startswith("_")}
|
||||
result = await handler(**clean_args)
|
||||
return result
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
215
demo/tools/dss_mcp/integrations/jira.py
Normal file
215
demo/tools/dss_mcp/integrations/jira.py
Normal file
@@ -0,0 +1,215 @@
|
||||
"""
|
||||
Jira Integration for MCP
|
||||
|
||||
Provides Jira API tools for issue tracking and project management.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from atlassian import Jira
|
||||
from mcp import types
|
||||
|
||||
from .base import BaseIntegration
|
||||
|
||||
|
||||
# Jira MCP Tool Definitions
|
||||
JIRA_TOOLS = [
|
||||
types.Tool(
|
||||
name="jira_create_issue",
|
||||
description="Create a new Jira issue",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_key": {
|
||||
"type": "string",
|
||||
"description": "Jira project key (e.g., 'DSS')"
|
||||
},
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"description": "Issue summary/title"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Issue description"
|
||||
},
|
||||
"issue_type": {
|
||||
"type": "string",
|
||||
"description": "Issue type (Story, Task, Bug, etc.)",
|
||||
"default": "Task"
|
||||
}
|
||||
},
|
||||
"required": ["project_key", "summary"]
|
||||
}
|
||||
),
|
||||
types.Tool(
|
||||
name="jira_get_issue",
|
||||
description="Get Jira issue details by key",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"issue_key": {
|
||||
"type": "string",
|
||||
"description": "Issue key (e.g., 'DSS-123')"
|
||||
}
|
||||
},
|
||||
"required": ["issue_key"]
|
||||
}
|
||||
),
|
||||
types.Tool(
|
||||
name="jira_search_issues",
|
||||
description="Search Jira issues using JQL",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jql": {
|
||||
"type": "string",
|
||||
"description": "JQL query (e.g., 'project=DSS AND status=Open')"
|
||||
},
|
||||
"max_results": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of results",
|
||||
"default": 50
|
||||
}
|
||||
},
|
||||
"required": ["jql"]
|
||||
}
|
||||
),
|
||||
types.Tool(
|
||||
name="jira_update_issue",
|
||||
description="Update a Jira issue",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"issue_key": {
|
||||
"type": "string",
|
||||
"description": "Issue key to update"
|
||||
},
|
||||
"fields": {
|
||||
"type": "object",
|
||||
"description": "Fields to update (summary, description, status, etc.)"
|
||||
}
|
||||
},
|
||||
"required": ["issue_key", "fields"]
|
||||
}
|
||||
),
|
||||
types.Tool(
|
||||
name="jira_add_comment",
|
||||
description="Add a comment to a Jira issue",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"issue_key": {
|
||||
"type": "string",
|
||||
"description": "Issue key"
|
||||
},
|
||||
"comment": {
|
||||
"type": "string",
|
||||
"description": "Comment text"
|
||||
}
|
||||
},
|
||||
"required": ["issue_key", "comment"]
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class JiraIntegration(BaseIntegration):
|
||||
"""Jira API integration with circuit breaker"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
"""
|
||||
Initialize Jira integration.
|
||||
|
||||
Args:
|
||||
config: Must contain 'url', 'username', 'api_token'
|
||||
"""
|
||||
super().__init__("jira", config)
|
||||
|
||||
url = config.get("url")
|
||||
username = config.get("username")
|
||||
api_token = config.get("api_token")
|
||||
|
||||
if not all([url, username, api_token]):
|
||||
raise ValueError("Jira configuration incomplete: url, username, api_token required")
|
||||
|
||||
self.jira = Jira(
|
||||
url=url,
|
||||
username=username,
|
||||
password=api_token,
|
||||
cloud=True
|
||||
)
|
||||
|
||||
async def create_issue(
|
||||
self,
|
||||
project_key: str,
|
||||
summary: str,
|
||||
description: str = "",
|
||||
issue_type: str = "Task"
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new Jira issue"""
|
||||
def _create():
|
||||
fields = {
|
||||
"project": {"key": project_key},
|
||||
"summary": summary,
|
||||
"description": description,
|
||||
"issuetype": {"name": issue_type}
|
||||
}
|
||||
return self.jira.create_issue(fields)
|
||||
|
||||
return await self.call_api(_create)
|
||||
|
||||
async def get_issue(self, issue_key: str) -> Dict[str, Any]:
|
||||
"""Get issue details"""
|
||||
def _get():
|
||||
return self.jira.get_issue(issue_key)
|
||||
|
||||
return await self.call_api(_get)
|
||||
|
||||
async def search_issues(self, jql: str, max_results: int = 50) -> Dict[str, Any]:
|
||||
"""Search issues with JQL"""
|
||||
def _search():
|
||||
return self.jira.jql(jql, limit=max_results)
|
||||
|
||||
return await self.call_api(_search)
|
||||
|
||||
async def update_issue(self, issue_key: str, fields: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Update issue fields"""
|
||||
def _update():
|
||||
self.jira.update_issue_field(issue_key, fields)
|
||||
return {"status": "updated", "issue_key": issue_key}
|
||||
|
||||
return await self.call_api(_update)
|
||||
|
||||
async def add_comment(self, issue_key: str, comment: str) -> Dict[str, Any]:
|
||||
"""Add comment to issue"""
|
||||
def _comment():
|
||||
return self.jira.issue_add_comment(issue_key, comment)
|
||||
|
||||
return await self.call_api(_comment)
|
||||
|
||||
|
||||
class JiraTools:
|
||||
"""MCP tool executor for Jira integration"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.jira = JiraIntegration(config)
|
||||
|
||||
async def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Execute Jira tool"""
|
||||
handlers = {
|
||||
"jira_create_issue": self.jira.create_issue,
|
||||
"jira_get_issue": self.jira.get_issue,
|
||||
"jira_search_issues": self.jira.search_issues,
|
||||
"jira_update_issue": self.jira.update_issue,
|
||||
"jira_add_comment": self.jira.add_comment
|
||||
}
|
||||
|
||||
handler = handlers.get(tool_name)
|
||||
if not handler:
|
||||
return {"error": f"Unknown Jira tool: {tool_name}"}
|
||||
|
||||
try:
|
||||
clean_args = {k: v for k, v in arguments.items() if not k.startswith("_")}
|
||||
result = await handler(**clean_args)
|
||||
return result
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
Reference in New Issue
Block a user