akira / modules /mcp_integration.py
akra35567's picture
Upload 55 files
bbbdd24 verified
Raw
History Blame Contribute Delete
16.4 kB
# 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
# ============================================================
@dataclass
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