Spaces:
Sleeping
Sleeping
| # type: ignore | |
| """ | |
| ================================================================================ | |
| MCP INTEGRATION MODULE - ANTHROPIC MODEL CONTEXT PROTOCOL | |
| ================================================================================ | |
| Integrates MCP (Model Context Protocol) as optional skill provider for AKIRA. | |
| MCP expands available tools and resources without breaking existing architecture. | |
| Philosophy: | |
| - MCP is OPTIONAL (fallback to current skills if unavailable) | |
| - MCP tools augment existing skills, don't replace them | |
| - Zero impact on current behavior if MCP is disabled | |
| - Lazy loading - only initialize if requested | |
| Supported MCP Resources: | |
| - filesystem_read: Safe file reading (whitelist) | |
| - filesystem_write: Safe file writing (whitelist) | |
| - web_search_advanced: Enhanced web search | |
| - database_query: Query AKIRA memory DB | |
| - skill_execute: Execute existing AKIRA skills | |
| - system_info: System statistics | |
| ================================================================================ | |
| """ | |
| import os | |
| import json | |
| import logging | |
| import asyncio | |
| from typing import Dict, Any, List, Optional, Callable | |
| from dataclasses import dataclass, field | |
| from datetime import datetime | |
| try: | |
| from loguru import logger | |
| except ImportError: | |
| logger = logging.getLogger(__name__) | |
| # ============================================================ | |
| # MCP RESOURCE DEFINITIONS | |
| # ============================================================ | |
| class MCPResourceSchema: | |
| """Schema for an MCP resource (tool).""" | |
| name: str | |
| description: str | |
| parameters: Dict[str, Any] | |
| handler: Optional[Callable] = None | |
| enabled: bool = True | |
| category: str = "general" # filesystem, web, database, skill, system | |
| # ============================================================ | |
| # MCP RESOURCE CATALOG | |
| # ============================================================ | |
| class MCPResourceCatalog: | |
| """ | |
| Central catalog of available MCP resources. | |
| Defines all tools that MCP can expose to Claude. | |
| """ | |
| def __init__(self): | |
| self.logger = logger | |
| self.resources: Dict[str, MCPResourceSchema] = {} | |
| self._initialize_default_resources() | |
| def _initialize_default_resources(self): | |
| """Initialize built-in MCP resources.""" | |
| # FILESYSTEM_READ | |
| self.resources["filesystem_read"] = MCPResourceSchema( | |
| name="filesystem_read", | |
| description="Read content from a file on the system (whitelist only)", | |
| parameters={ | |
| "type": "object", | |
| "properties": { | |
| "path": { | |
| "type": "string", | |
| "description": "File path to read (must be whitelisted)" | |
| }, | |
| "encoding": { | |
| "type": "string", | |
| "description": "File encoding (default: utf-8)", | |
| "default": "utf-8" | |
| } | |
| }, | |
| "required": ["path"] | |
| }, | |
| category="filesystem" | |
| ) | |
| # FILESYSTEM_WRITE | |
| self.resources["filesystem_write"] = MCPResourceSchema( | |
| name="filesystem_write", | |
| description="Write content to a file (whitelisted directories only)", | |
| parameters={ | |
| "type": "object", | |
| "properties": { | |
| "path": { | |
| "type": "string", | |
| "description": "File path to write to (must be whitelisted)" | |
| }, | |
| "content": { | |
| "type": "string", | |
| "description": "Content to write" | |
| } | |
| }, | |
| "required": ["path", "content"] | |
| }, | |
| category="filesystem" | |
| ) | |
| # WEB_SEARCH_ADVANCED | |
| self.resources["web_search_advanced"] = MCPResourceSchema( | |
| name="web_search_advanced", | |
| description="Perform advanced web search with filters and ranking", | |
| parameters={ | |
| "type": "object", | |
| "properties": { | |
| "query": { | |
| "type": "string", | |
| "description": "Search query" | |
| }, | |
| "max_results": { | |
| "type": "integer", | |
| "description": "Maximum results to return (default: 5)", | |
| "default": 5 | |
| }, | |
| "filter": { | |
| "type": "string", | |
| "description": "Optional filter (news, academic, recent)", | |
| "enum": ["news", "academic", "recent", "none"] | |
| } | |
| }, | |
| "required": ["query"] | |
| }, | |
| category="web" | |
| ) | |
| # DATABASE_QUERY | |
| self.resources["database_query"] = MCPResourceSchema( | |
| name="database_query", | |
| description="Query AKIRA's memory database for facts and patterns", | |
| parameters={ | |
| "type": "object", | |
| "properties": { | |
| "user_id": { | |
| "type": "string", | |
| "description": "User ID to query" | |
| }, | |
| "query_type": { | |
| "type": "string", | |
| "description": "Type of query (facts, patterns, emotions, preferences)", | |
| "enum": ["facts", "patterns", "emotions", "preferences"] | |
| }, | |
| "limit": { | |
| "type": "integer", | |
| "description": "Number of results (default: 10)", | |
| "default": 10 | |
| } | |
| }, | |
| "required": ["user_id", "query_type"] | |
| }, | |
| category="database" | |
| ) | |
| # SYSTEM_INFO | |
| self.resources["system_info"] = MCPResourceSchema( | |
| name="system_info", | |
| description="Get current system information (time, date, stats)", | |
| parameters={ | |
| "type": "object", | |
| "properties": { | |
| "info_type": { | |
| "type": "string", | |
| "description": "What info to retrieve", | |
| "enum": ["time", "date", "datetime", "uptime", "memory", "all"] | |
| } | |
| }, | |
| "required": ["info_type"] | |
| }, | |
| category="system" | |
| ) | |
| # SKILL_EXECUTE | |
| self.resources["skill_execute"] = MCPResourceSchema( | |
| name="skill_execute", | |
| description="Execute an existing AKIRA skill", | |
| parameters={ | |
| "type": "object", | |
| "properties": { | |
| "skill_name": { | |
| "type": "string", | |
| "description": "Name of the skill to execute" | |
| }, | |
| "arguments": { | |
| "type": "object", | |
| "description": "Arguments to pass to the skill" | |
| } | |
| }, | |
| "required": ["skill_name"] | |
| }, | |
| category="skill" | |
| ) | |
| self.logger.info(f"✅ MCP Catalog initialized with {len(self.resources)} resources") | |
| def get_resource(self, name: str) -> Optional[MCPResourceSchema]: | |
| """Get a specific resource schema.""" | |
| return self.resources.get(name) | |
| def list_resources(self, category: Optional[str] = None) -> List[MCPResourceSchema]: | |
| """List all available resources, optionally filtered by category.""" | |
| if category: | |
| return [r for r in self.resources.values() if r.category == category] | |
| return list(self.resources.values()) | |
| def to_claude_tools_format(self) -> List[Dict[str, Any]]: | |
| """ | |
| Convert resources to Claude Tool Use format. | |
| Returns list of tool definitions compatible with Claude API. | |
| """ | |
| tools = [] | |
| for resource in self.resources.values(): | |
| if not resource.enabled: | |
| continue | |
| tool = { | |
| "name": resource.name, | |
| "description": resource.description, | |
| "input_schema": { | |
| "type": "object", | |
| "properties": resource.parameters.get("properties", {}), | |
| "required": resource.parameters.get("required", []) | |
| } | |
| } | |
| tools.append(tool) | |
| return tools | |
| # ============================================================ | |
| # MCP SERVER CLIENT | |
| # ============================================================ | |
| class MCPServerClient: | |
| """ | |
| Client for communicating with MCP server(s). | |
| Handles resource invocation, caching, and error handling. | |
| """ | |
| def __init__(self, catalog: Optional[MCPResourceCatalog] = None): | |
| self.logger = logger | |
| self.catalog = catalog or MCPResourceCatalog() | |
| self.is_available = False | |
| self._detection_attempted = False | |
| self._default_handlers = self._setup_default_handlers() | |
| self._detect_mcp_server() | |
| def _setup_default_handlers(self) -> Dict[str, Callable]: | |
| """Setup default handlers for resources.""" | |
| return { | |
| "system_info": self._handle_system_info, | |
| "filesystem_read": self._handle_filesystem_read, | |
| "filesystem_write": self._handle_filesystem_write, | |
| "skill_execute": self._handle_skill_execute, | |
| } | |
| def _detect_mcp_server(self): | |
| """Detect if MCP server is available.""" | |
| try: | |
| # Check for common MCP server indicators | |
| # For now, assume available if module can be imported | |
| import anthropic | |
| self.is_available = True | |
| self.logger.info("✅ MCP Server detected (anthropic module available)") | |
| except ImportError: | |
| self.is_available = False | |
| self.logger.warning("⚠️ Anthropic SDK not available - MCP disabled") | |
| except Exception as e: | |
| self.is_available = False | |
| self.logger.warning(f"⚠️ MCP detection failed: {e}") | |
| self._detection_attempted = True | |
| def _handle_system_info(self, info_type: str = "all") -> Dict[str, Any]: | |
| """Handle system_info resource.""" | |
| import psutil | |
| from datetime import datetime | |
| result = {} | |
| if info_type in ["time", "all"]: | |
| result["time"] = datetime.now().strftime("%H:%M:%S") | |
| if info_type in ["date", "all"]: | |
| result["date"] = datetime.now().strftime("%Y-%m-%d") | |
| if info_type in ["datetime", "all"]: | |
| result["datetime"] = datetime.now().isoformat() | |
| if info_type in ["uptime", "all"]: | |
| try: | |
| uptime = psutil.boot_time() | |
| result["uptime_seconds"] = int(datetime.now().timestamp() - uptime) | |
| except: | |
| pass | |
| if info_type in ["memory", "all"]: | |
| try: | |
| memory = psutil.virtual_memory() | |
| result["memory_percent"] = memory.percent | |
| result["memory_available_gb"] = memory.available / (1024**3) | |
| except: | |
| pass | |
| return result | |
| def _handle_filesystem_read(self, path: str, encoding: str = "utf-8") -> Dict[str, Any]: | |
| """Handle filesystem_read resource (safe paths only).""" | |
| # Whitelist of safe directories | |
| safe_dirs = [ | |
| "/akira/data", | |
| "/akira/logs", | |
| os.path.expanduser("~/.akira"), | |
| ] | |
| # Check if path is in whitelist | |
| path_allowed = any(path.startswith(safe_dir) for safe_dir in safe_dirs) | |
| if not path_allowed: | |
| return {"error": "Path not whitelisted for reading"} | |
| try: | |
| with open(path, 'r', encoding=encoding) as f: | |
| content = f.read() | |
| return {"success": True, "content": content} | |
| except Exception as e: | |
| return {"error": str(e)} | |
| def _handle_filesystem_write(self, path: str, content: str) -> Dict[str, Any]: | |
| """Handle filesystem_write resource (safe paths only).""" | |
| # Whitelist of safe directories | |
| safe_dirs = [ | |
| "/akira/data", | |
| os.path.expanduser("~/.akira"), | |
| ] | |
| # Check if path is in whitelist | |
| path_allowed = any(path.startswith(safe_dir) for safe_dir in safe_dirs) | |
| if not path_allowed: | |
| return {"error": "Path not whitelisted for writing"} | |
| try: | |
| os.makedirs(os.path.dirname(path), exist_ok=True) | |
| with open(path, 'w', encoding='utf-8') as f: | |
| f.write(content) | |
| return {"success": True, "message": f"Written to {path}"} | |
| except Exception as e: | |
| return {"error": str(e)} | |
| def _handle_skill_execute(self, skill_name: str, arguments: Dict[str, Any] = None) -> Dict[str, Any]: | |
| """ | |
| ✅ NOVO: Handle skill_execute resource | |
| Executa qualquer skill registrada no AKIRA registry | |
| """ | |
| try: | |
| from modules.skills_registry import registry | |
| if arguments is None: | |
| arguments = {} | |
| self.logger.info(f"🛠️ [MCP] Executando skill: {skill_name} com args: {arguments}") | |
| result = registry.execute(skill_name, arguments) | |
| return { | |
| "success": True, | |
| "skill": skill_name, | |
| "result": result, | |
| "timestamp": datetime.now().isoformat() | |
| } | |
| except ImportError: | |
| return { | |
| "success": False, | |
| "error": "Skills registry not available", | |
| "skill": skill_name | |
| } | |
| except Exception as e: | |
| self.logger.error(f"❌ [MCP] Erro ao executar skill {skill_name}: {e}") | |
| return { | |
| "success": False, | |
| "error": str(e), | |
| "skill": skill_name | |
| } | |
| async def invoke_resource(self, resource_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]: | |
| """ | |
| Invoke an MCP resource. | |
| Tries handler first, then falls back to error. | |
| """ | |
| if not self.is_available: | |
| return {"error": "MCP server not available"} | |
| handler = self._default_handlers.get(resource_name) | |
| if handler: | |
| try: | |
| result = handler(**arguments) | |
| return result | |
| except Exception as e: | |
| return {"error": str(e)} | |
| return {"error": f"Resource '{resource_name}' handler not found"} | |
| def get_available_tools(self) -> List[Dict[str, Any]]: | |
| """Get all available tools in Claude format.""" | |
| if not self.is_available: | |
| return [] | |
| return self.catalog.to_claude_tools_format() | |
| # ============================================================ | |
| # SINGLETON INSTANCE | |
| # ============================================================ | |
| _MCP_CATALOG = None | |
| _MCP_CLIENT = None | |
| def get_mcp_catalog() -> MCPResourceCatalog: | |
| """Get singleton MCP catalog.""" | |
| global _MCP_CATALOG | |
| if _MCP_CATALOG is None: | |
| _MCP_CATALOG = MCPResourceCatalog() | |
| return _MCP_CATALOG | |
| def get_mcp_client() -> MCPServerClient: | |
| """Get singleton MCP client.""" | |
| global _MCP_CLIENT | |
| if _MCP_CLIENT is None: | |
| _MCP_CLIENT = MCPServerClient(get_mcp_catalog()) | |
| return _MCP_CLIENT | |