Source code for isek.tools.fastmcp_toolkit

# isek/tools/fastmcp_toolkit.py

import asyncio
from typing import Any, Dict, List, Optional
from fastmcp import Client
from isek.tools.toolkit import Toolkit
from isek.utils.log import log


[docs] class FastMCPToolkit(Toolkit): """ FastMCP-based toolkit for MCP server integration. Provides simplified access to MCP tools with automatic discovery and registration. """ def __init__( self, server_source: str, name: str = "fastmcp", timeout: float = 30.0, auto_register: bool = True, debug: bool = False, auth_token: Optional[str] = None, ): """Initialize FastMCP toolkit. Args: server_source: MCP server source (URL, file path, or FastMCP instance) name: Toolkit name timeout: Connection timeout in seconds auto_register: Whether to automatically discover and register tools debug: Enable debug output auth_token: Authentication token for the MCP server """ self.server_source = server_source self.timeout = timeout self.auth_token = auth_token self.client = None super().__init__( name=name, tools=[], auto_register=False, debug=debug, ) if auto_register: # Initialize connection and discover tools self._initialize_connection() def _initialize_connection(self): """Initialize MCP connection and discover tools.""" try: # Create FastMCP client client_kwargs = { "transport": self.server_source, "timeout": self.timeout, } if self.auth_token: client_kwargs["auth"] = self.auth_token self.client = Client(**client_kwargs) # Discover and register tools self._discover_tools() except Exception as e: log.error(f"[FastMCPToolkit] Failed to initialize connection: {e}") if self.debug: raise def _discover_tools(self): """Discover tools from MCP server and register them.""" if not self.client: return try: # Get tools list tools = self._run_async(self.client.list_tools()) if self.debug: log.debug(f"[FastMCPToolkit] Discovered {len(tools)} tools") # Register each tool for tool in tools: self._register_mcp_tool(tool.name) except Exception as e: log.error(f"[FastMCPToolkit] Tool discovery failed: {e}") def _register_mcp_tool(self, tool_name: str): """Register a single MCP tool as a local function. Args: tool_name: Name of the MCP tool to register """ def tool_wrapper(**kwargs: Any) -> Any: """Wrapper function that calls MCP tool.""" try: # Parameter mapping for common tools mapped_kwargs = self._map_parameters(tool_name, kwargs) # Call the tool result = self._run_async( self.client.call_tool(tool_name, mapped_kwargs) ) return self._extract_text(result) except Exception as e: log.error(f"[FastMCPToolkit] Tool call failed for {tool_name}: {e}") return f"[Error] {e}" # Set function attributes tool_wrapper.__name__ = tool_name.replace(".", "_") tool_wrapper.__doc__ = f"MCP tool: {tool_name}" # Register with toolkit self.register(tool_wrapper, name=tool_wrapper.__name__) if self.debug: log.debug(f"[FastMCPToolkit] Registered tool: {tool_name}") def _map_parameters(self, tool_name: str, kwargs: Dict[str, Any]) -> Dict[str, Any]: """Map common parameter names to MCP tool specific parameter names. Args: tool_name: Name of the MCP tool kwargs: Original parameters Returns: Mapped parameters """ mapped = kwargs.copy() # GitHub Copilot MCP parameter mappings if tool_name == "search_repositories": # Map 'q' to 'query' for GitHub search if "q" in mapped and "query" not in mapped: mapped["query"] = mapped.pop("q") return mapped def _run_async(self, coro): """Run async coroutine in new event loop.""" loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: async def run_with_client(): async with self.client: return await coro result = loop.run_until_complete(run_with_client()) return result finally: loop.close() def _extract_text(self, result) -> str: """Extract text content from MCP result.""" if not result or len(result) == 0: return str(result) first_result = result[0] # Try to get text content safely try: if hasattr(first_result, "text") and first_result.text is not None: return first_result.text except (AttributeError, TypeError): pass return str(first_result)
[docs] def health_check(self) -> bool: """Check if MCP connection is healthy. Returns: True if connection is working, False otherwise """ if not self.client: return False try: self._run_async(self.client.ping()) return True except Exception as e: log.error(f"[FastMCPToolkit] Health check failed: {e}") return False
[docs] def list_available_tools(self) -> List[str]: """Get list of available MCP tools. Returns: List of tool names """ if not self.client: return [] try: tools = self._run_async(self.client.list_tools()) return [tool.name for tool in tools] except Exception as e: log.error(f"[FastMCPToolkit] Failed to get tools list: {e}") return []
[docs] def call_tool(self, tool_name: str, **kwargs) -> Any: """Call a specific MCP tool. Args: tool_name: Name of the tool to call **kwargs: Arguments to pass to the tool Returns: Tool execution result """ if not self.client: return "[Error] No MCP client available" try: # Parameter mapping for common tools mapped_kwargs = self._map_parameters(tool_name, kwargs) # Call the tool result = self._run_async(self.client.call_tool(tool_name, mapped_kwargs)) return self._extract_text(result) except Exception as e: log.error(f"[FastMCPToolkit] Tool call failed for {tool_name}: {e}") return f"[Error] {e}"
# Convenience function
[docs] def create_fastmcp_toolkit( server_source: str, auth_token: Optional[str] = None, debug: bool = False ) -> FastMCPToolkit: """Create a FastMCP toolkit instance.""" return FastMCPToolkit( server_source=server_source, auth_token=auth_token, debug=debug )