From c43d7a8680812ed1fc5acfd564986290235a80fb Mon Sep 17 00:00:00 2001 From: Eric Ma Date: Sat, 18 Oct 2025 18:47:30 -0400 Subject: [PATCH] =?UTF-8?q?feat(mcp=5Fintegration)=F0=9F=A4=9D:=20Add=20MC?= =?UTF-8?q?P=20(Model=20Context=20Protocol)=20integration=20to=20ToolBot?= =?UTF-8?q?=20and=20AgentBot=20using=20FastMCP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduce MCPConnectionManager to manage connections to multiple MCP servers with lazy loading and caching. - Add functional adapters in mcp_tools to convert MCP tools into llamabot-compatible tools. - Enhance ToolBot to support MCP servers, discover MCP tools lazily on first call, and integrate them into the tool list. - Extend AgentBot to accept MCP server configurations and pass them to its internal ToolBot instance. - Add detailed MCP integration documentation in AGENTS.md including usage patterns, best practices, and error handling. - Add MCP integration demo notebook showing connection, tool discovery, and usage with ToolBot and AgentBot. --- AGENTS.md | 9 + llamabot/bot/agentbot.py | 4 +- llamabot/bot/toolbot.py | 45 +++- llamabot/components/mcp_client.py | 257 +++++++++++++++++++++++ llamabot/components/mcp_tools.py | 245 ++++++++++++++++++++++ notebooks/mcp-integration-demo.py | 331 ++++++++++++++++++++++++++++++ 6 files changed, 889 insertions(+), 2 deletions(-) create mode 100644 llamabot/components/mcp_client.py create mode 100644 llamabot/components/mcp_tools.py create mode 100644 notebooks/mcp-integration-demo.py diff --git a/AGENTS.md b/AGENTS.md index 8c1f185cc..88b4934a6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -181,6 +181,7 @@ The CLI is built with Typer and organized in `llamabot/cli/`: - **Vector Store**: LanceDB (default), ChromaDB (optional) - **Testing**: pytest, hypothesis, pytest-cov - **Docs**: MkDocs with Material theme +- **MCP Integration**: FastMCP (provides both server and client functionality) ## Common Development Tasks @@ -206,6 +207,14 @@ The CLI is built with Typer and organized in `llamabot/cli/`: 3. Ensure composability with existing components 4. Add comprehensive tests in `tests/components/` +### MCP Integration + +**MCP Dependencies**: This project uses FastMCP for MCP (Model Context Protocol) integration. FastMCP provides both server and client functionality, so we only need the `fastmcp` dependency - do not add the `mcp` package as it's redundant. + +**MCP Client Pattern**: Use `fastmcp.Client` directly instead of creating custom wrappers. The `MCPConnectionManager` class manages multiple server connections with lazy loading. + +**Functional Approach**: MCP tool adapters use functions rather than classes, aligning with the repo's functional programming preference. + ## Security Considerations - **Agent Execution**: All agent-generated code runs in Docker sandbox (`sandbox.py`) diff --git a/llamabot/bot/agentbot.py b/llamabot/bot/agentbot.py index 7a737c068..84b45fda6 100644 --- a/llamabot/bot/agentbot.py +++ b/llamabot/bot/agentbot.py @@ -10,7 +10,7 @@ import json from concurrent.futures import ThreadPoolExecutor, as_completed from datetime import datetime -from typing import Any, Callable, List, Optional, Union +from typing import Any, Callable, List, Optional, Union, Dict from loguru import logger @@ -112,6 +112,7 @@ def __init__( stream_target: str = "none", tools: Optional[list[Callable]] = None, toolbot: Optional[ToolBot] = None, + mcp_servers: Optional[List[Dict[str, Any]]] = None, **completion_kwargs, ): super().__init__( @@ -134,6 +135,7 @@ def __init__( system_prompt=toolbot_sysprompt(globals_dict={}), model_name=model_name, tools=all_tools, + mcp_servers=mcp_servers, **completion_kwargs, ) else: diff --git a/llamabot/bot/toolbot.py b/llamabot/bot/toolbot.py index 019e9c256..3300373a7 100644 --- a/llamabot/bot/toolbot.py +++ b/llamabot/bot/toolbot.py @@ -1,11 +1,13 @@ """ToolBot - A single-turn bot that can execute tools.""" -from typing import Callable, List, Optional, Union +from typing import Callable, List, Optional, Union, Dict, Any from loguru import logger from llamabot.components.tools import today_date, respond_to_user from llamabot.components.chat_memory import ChatMemory from llamabot.components.messages import AIMessage, BaseMessage +from llamabot.components.mcp_client import MCPConnectionManager +from llamabot.components.mcp_tools import discover_all_mcp_tools from llamabot.bot.simplebot import ( SimpleBot, extract_tool_calls, @@ -82,6 +84,7 @@ class ToolBot(SimpleBot): :param model_name: The name of the model to use :param tools: Optional list of additional tools to include :param chat_memory: Chat memory component for context retrieval + :param mcp_servers: Optional list of MCP server configurations :param completion_kwargs: Additional keyword arguments for completion """ @@ -91,6 +94,7 @@ def __init__( model_name: str, tools: Optional[List[Callable]] = None, chat_memory: Optional[ChatMemory] = None, + mcp_servers: Optional[List[Dict[str, Any]]] = None, **completion_kwargs, ): super().__init__( @@ -108,6 +112,42 @@ def __init__( self.name_to_tool_map = {f.__name__: f for f in all_tools} self.chat_memory = chat_memory or ChatMemory() + # Initialize MCP support + self.mcp_servers = mcp_servers or [] + self.mcp_connection_manager = None + self.mcp_tools_discovered = False + + def _discover_mcp_tools(self): + """Discover and add MCP tools to the bot's tool list. + + This method is called lazily on the first tool call to avoid + connection overhead during initialization. + """ + if self.mcp_tools_discovered or not self.mcp_servers: + return + + try: + # Create connection manager + self.mcp_connection_manager = MCPConnectionManager(self.mcp_servers) + + # Discover all MCP tools + mcp_tools = discover_all_mcp_tools(self.mcp_connection_manager) + + if mcp_tools: + # Add MCP tools to the bot's tool list + self.tools.extend([tool.json_schema for tool in mcp_tools]) + self.name_to_tool_map.update( + {tool.__name__: tool for tool in mcp_tools} + ) + + logger.info(f"Added {len(mcp_tools)} MCP tools to ToolBot") + + self.mcp_tools_discovered = True + + except Exception as e: + logger.error(f"Failed to discover MCP tools: {e}") + # Continue without MCP tools - don't fail the bot + def __call__( self, *messages: Union[str, BaseMessage, list[Union[str, BaseMessage]], Callable], @@ -119,6 +159,9 @@ def __call__( """ from llamabot.components.messages import to_basemessage, HumanMessage + # Discover MCP tools on first call + self._discover_mcp_tools() + # Handle callable functions by calling them and converting to strings processed_messages = [] for msg in messages: diff --git a/llamabot/components/mcp_client.py b/llamabot/components/mcp_client.py new file mode 100644 index 000000000..c8b0e19fb --- /dev/null +++ b/llamabot/components/mcp_client.py @@ -0,0 +1,257 @@ +"""MCP Client component for connecting to external MCP servers. + +This module provides functionality to connect ToolBot and AgentBot to external +MCP (Model Context Protocol) servers, enabling them to discover and use tools, +resources, and prompts from these servers. +""" + +import asyncio +import subprocess +from typing import Any, Dict, List, Optional +from loguru import logger + +from fastmcp import Client + + +class MCPConnectionManager: + """Manages connections to multiple MCP servers. + + This class handles the lifecycle of MCP server connections, including + lazy connection, caching, and cleanup. It uses fastmcp.Client internally + to handle the MCP protocol details. + + :param servers: List of MCP server configurations + """ + + def __init__(self, servers: List[Dict[str, Any]]): + # Initialize the connection manager with server configurations. + # servers: List of server configurations, each containing: + # - name: Server identifier + # - command: Command to start the server + # - args: Command arguments (optional) + # - env: Environment variables (optional) + self.servers = servers + self._connections: Dict[str, Client] = {} + self._connected: Dict[str, bool] = {server["name"]: False for server in servers} + + async def get_client(self, server_name: str) -> Optional[Client]: + """Get a connected client for the specified server. + + :param server_name: Name of the server to connect to + :return: Connected Client instance or None if connection failed + """ + if server_name in self._connections and self._connected.get(server_name, False): + return self._connections[server_name] + + # Find server configuration + server_config = None + for server in self.servers: + if server["name"] == server_name: + server_config = server + break + + if not server_config: + logger.error(f"Server configuration not found for: {server_name}") + return None + + try: + # Create client with stdio transport + client = await self._connect_to_server(server_config) + if client: + self._connections[server_name] = client + self._connected[server_name] = True + logger.debug(f"Connected to MCP server: {server_name}") + return client + except Exception as e: + logger.error(f"Failed to connect to MCP server {server_name}: {e}") + self._connected[server_name] = False + + return None + + async def _connect_to_server( + self, server_config: Dict[str, Any] + ) -> Optional[Client]: + """Connect to a specific MCP server. + + :param server_config: Server configuration dictionary + :return: Connected Client instance or None if failed + """ + command = server_config["command"] + args = server_config.get("args", []) + env = server_config.get("env", {}) + + # Start the server process + process = subprocess.Popen( + [command] + args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env={**server_config.get("env", {}), **env}, + ) + + # Create client with stdio transport + client = Client(process.stdin, process.stdout) + + # Initialize the connection + await client.initialize() + + return client + + async def list_tools(self, server_name: str) -> List[Dict[str, Any]]: + """List available tools from a specific server. + + :param server_name: Name of the server + :return: List of tool definitions + """ + client = await self.get_client(server_name) + if not client: + return [] + + try: + tools = await client.list_tools() + return tools + except Exception as e: + logger.error(f"Failed to list tools from {server_name}: {e}") + return [] + + async def call_tool( + self, server_name: str, tool_name: str, arguments: Dict[str, Any] + ) -> Any: + """Call a tool on a specific server. + + :param server_name: Name of the server + :param tool_name: Name of the tool to call + :param arguments: Tool arguments + :return: Tool result + """ + client = await self.get_client(server_name) + if not client: + raise ConnectionError(f"Not connected to server: {server_name}") + + try: + result = await client.call_tool(tool_name, arguments) + return result + except Exception as e: + logger.error(f"Failed to call tool {tool_name} on {server_name}: {e}") + raise + + async def list_resources(self, server_name: str) -> List[Dict[str, Any]]: + """List available resources from a specific server. + + :param server_name: Name of the server + :return: List of resource definitions + """ + client = await self.get_client(server_name) + if not client: + return [] + + try: + resources = await client.list_resources() + return resources + except Exception as e: + logger.error(f"Failed to list resources from {server_name}: {e}") + return [] + + async def read_resource(self, server_name: str, resource_uri: str) -> Any: + """Read a resource from a specific server. + + :param server_name: Name of the server + :param resource_uri: URI of the resource to read + :return: Resource content + """ + client = await self.get_client(server_name) + if not client: + raise ConnectionError(f"Not connected to server: {server_name}") + + try: + content = await client.read_resource(resource_uri) + return content + except Exception as e: + logger.error( + f"Failed to read resource {resource_uri} from {server_name}: {e}" + ) + raise + + async def list_prompts(self, server_name: str) -> List[Dict[str, Any]]: + """List available prompts from a specific server. + + :param server_name: Name of the server + :return: List of prompt definitions + """ + client = await self.get_client(server_name) + if not client: + return [] + + try: + prompts = await client.list_prompts() + return prompts + except Exception as e: + logger.error(f"Failed to list prompts from {server_name}: {e}") + return [] + + async def get_prompt( + self, server_name: str, prompt_name: str, arguments: Dict[str, Any] + ) -> str: + """Get a prompt from a specific server. + + :param server_name: Name of the server + :param prompt_name: Name of the prompt + :param arguments: Prompt arguments + :return: Prompt content + """ + client = await self.get_client(server_name) + if not client: + raise ConnectionError(f"Not connected to server: {server_name}") + + try: + prompt = await client.get_prompt(prompt_name, arguments) + return prompt + except Exception as e: + logger.error(f"Failed to get prompt {prompt_name} from {server_name}: {e}") + raise + + async def close_all(self): + """Close all server connections.""" + for server_name, client in self._connections.items(): + try: + await client.close() + logger.debug(f"Closed connection to {server_name}") + except Exception as e: + logger.error(f"Error closing connection to {server_name}: {e}") + + self._connections.clear() + self._connected.clear() + + def __del__(self): + """Cleanup connections on destruction.""" + if self._connections: + # Schedule cleanup in the event loop + try: + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(self.close_all()) + except RuntimeError: + # No event loop running, can't schedule cleanup + pass + + +def run_async(coro): + """Run an async coroutine in a sync context. + + :param coro: Async coroutine to run + :return: Result of the coroutine + """ + try: + loop = asyncio.get_event_loop() + if loop.is_running(): + # If we're already in an event loop, we need to use a different approach + import concurrent.futures + + with concurrent.futures.ThreadPoolExecutor() as executor: + future = executor.submit(asyncio.run, coro) + return future.result() + else: + return loop.run_until_complete(coro) + except RuntimeError: + # No event loop, create a new one + return asyncio.run(coro) diff --git a/llamabot/components/mcp_tools.py b/llamabot/components/mcp_tools.py new file mode 100644 index 000000000..db6c5a5f7 --- /dev/null +++ b/llamabot/components/mcp_tools.py @@ -0,0 +1,245 @@ +"""MCP Tools adapter for converting MCP tools to llamabot tool format. + +This module provides functional adapters that convert MCP (Model Context Protocol) +tools into the format expected by llamabot's tool system. +""" + +from typing import Any, Callable, Dict, List +from loguru import logger + +from llamabot.components.tools import tool +from llamabot.components.mcp_client import MCPConnectionManager, run_async + + +def mcp_tool_to_llamabot_tool( + mcp_tool: Dict[str, Any], server_name: str, connection_manager: MCPConnectionManager +) -> Callable: + """Convert a single MCP tool to a llamabot-compatible tool. + + :param mcp_tool: MCP tool definition + :param server_name: Name of the MCP server + :param connection_manager: MCP connection manager instance + :return: Callable tool function + """ + tool_name = mcp_tool.get("name", "unknown_tool") + tool_description = mcp_tool.get("description", "MCP tool") + tool_input_schema = mcp_tool.get("inputSchema", {}) + + # Create a unique name for the tool + prefixed_name = f"{server_name}:{tool_name}" + + # Generate docstring from MCP tool description + docstring = f"""{tool_description} + + This is an MCP tool from server '{server_name}'. + + Parameters: + """ + + # Add parameter descriptions from schema + properties = tool_input_schema.get("properties", {}) + required = tool_input_schema.get("required", []) + + for param_name, param_schema in properties.items(): + param_type = param_schema.get("type", "string") + param_desc = param_schema.get("description", "") + is_required = param_name in required + + docstring += f" :param {param_name}: {param_desc} ({param_type})" + if not is_required: + docstring += " (optional)" + docstring += "\n" + + docstring += f" :return: Result from MCP tool '{tool_name}'" + + def mcp_tool_wrapper(**kwargs): + """Wrapper function for MCP tool execution.""" + try: + # Call the MCP tool asynchronously + result = run_async( + connection_manager.call_tool(server_name, tool_name, kwargs) + ) + return result + except Exception as e: + logger.error(f"Error calling MCP tool {prefixed_name}: {e}") + return f"Error: {str(e)}" + + # Set the function metadata + mcp_tool_wrapper.__name__ = prefixed_name + mcp_tool_wrapper.__doc__ = docstring + + # Create the tool decorator with proper schema + @tool + def decorated_tool(**kwargs): + """Decorated MCP tool function.""" + return mcp_tool_wrapper(**kwargs) + + # Update the decorated function's metadata + decorated_tool.__name__ = prefixed_name + decorated_tool.__doc__ = docstring + + return decorated_tool + + +def discover_mcp_tools( + connection_manager: MCPConnectionManager, server_name: str +) -> List[Callable]: + """Discover all tools from an MCP server and convert them to llamabot tools. + + :param connection_manager: MCP connection manager instance + :param server_name: Name of the MCP server + :return: List of converted tool functions + """ + try: + # Get tools from the server + mcp_tools = run_async(connection_manager.list_tools(server_name)) + + if not mcp_tools: + logger.warning(f"No tools found on MCP server: {server_name}") + return [] + + # Convert each MCP tool to a llamabot tool + llamabot_tools = [] + for mcp_tool in mcp_tools: + try: + llamabot_tool = mcp_tool_to_llamabot_tool( + mcp_tool, server_name, connection_manager + ) + llamabot_tools.append(llamabot_tool) + logger.debug( + f"Converted MCP tool: {server_name}:{mcp_tool.get('name', 'unknown')}" + ) + except Exception as e: + logger.error( + f"Failed to convert MCP tool {mcp_tool.get('name', 'unknown')}: {e}" + ) + continue + + logger.info( + f"Discovered {len(llamabot_tools)} tools from MCP server: {server_name}" + ) + return llamabot_tools + + except Exception as e: + logger.error(f"Failed to discover tools from MCP server {server_name}: {e}") + return [] + + +def discover_all_mcp_tools(connection_manager: MCPConnectionManager) -> List[Callable]: + """Discover tools from all configured MCP servers. + + :param connection_manager: MCP connection manager instance + :return: List of all converted tool functions + """ + all_tools = [] + + for server in connection_manager.servers: + server_name = server["name"] + try: + server_tools = discover_mcp_tools(connection_manager, server_name) + all_tools.extend(server_tools) + except Exception as e: + logger.error(f"Failed to discover tools from server {server_name}: {e}") + continue + + logger.info(f"Discovered {len(all_tools)} total MCP tools from all servers") + return all_tools + + +def create_mcp_resource_tool( + server_name: str, resource_uri: str, connection_manager: MCPConnectionManager +) -> Callable: + """Create a tool for reading a specific MCP resource. + + :param server_name: Name of the MCP server + :param resource_uri: URI of the resource + :param connection_manager: MCP connection manager instance + :return: Callable tool function for reading the resource + """ + tool_name = f"{server_name}:read_resource" + + @tool + def read_mcp_resource() -> str: + """Read a resource from an MCP server. + + :return: Content of the MCP resource + """ + try: + content = run_async( + connection_manager.read_resource(server_name, resource_uri) + ) + return str(content) + except Exception as e: + logger.error(f"Error reading MCP resource {resource_uri}: {e}") + return f"Error: {str(e)}" + + read_mcp_resource.__name__ = tool_name + return read_mcp_resource + + +def create_mcp_prompt_tool( + server_name: str, prompt_name: str, connection_manager: MCPConnectionManager +) -> Callable: + """Create a tool for getting a specific MCP prompt. + + :param server_name: Name of the MCP server + :param prompt_name: Name of the prompt + :param connection_manager: MCP connection manager instance + :return: Callable tool function for getting the prompt + """ + tool_name = f"{server_name}:get_prompt_{prompt_name}" + + @tool + def get_mcp_prompt(**kwargs) -> str: + """Get a prompt from an MCP server. + + :param kwargs: Prompt arguments + :return: Prompt content + """ + try: + prompt = run_async( + connection_manager.get_prompt(server_name, prompt_name, kwargs) + ) + return str(prompt) + except Exception as e: + logger.error(f"Error getting MCP prompt {prompt_name}: {e}") + return f"Error: {str(e)}" + + get_mcp_prompt.__name__ = tool_name + return get_mcp_prompt + + +def get_mcp_server_info(connection_manager: MCPConnectionManager) -> Dict[str, Any]: + """Get information about all configured MCP servers. + + :param connection_manager: MCP connection manager instance + :return: Dictionary with server information + """ + server_info = {} + + for server in connection_manager.servers: + server_name = server["name"] + try: + # Try to get basic info about the server + tools = run_async(connection_manager.list_tools(server_name)) + resources = run_async(connection_manager.list_resources(server_name)) + prompts = run_async(connection_manager.list_prompts(server_name)) + + server_info[server_name] = { + "command": server["command"], + "args": server.get("args", []), + "tools_count": len(tools), + "resources_count": len(resources), + "prompts_count": len(prompts), + "connected": connection_manager._connected.get(server_name, False), + } + except Exception as e: + logger.error(f"Failed to get info for server {server_name}: {e}") + server_info[server_name] = { + "command": server["command"], + "args": server.get("args", []), + "error": str(e), + "connected": False, + } + + return server_info diff --git a/notebooks/mcp-integration-demo.py b/notebooks/mcp-integration-demo.py new file mode 100644 index 000000000..0817da00c --- /dev/null +++ b/notebooks/mcp-integration-demo.py @@ -0,0 +1,331 @@ +# marimo +# title: MCP Integration Demo +# description: Demonstrates connecting ToolBot and AgentBot to MCP servers +# author: Eric Ma +# date: 2025-01-18 +# version: 1.0.0 +# tags: [MCP, ToolBot, AgentBot, FastMCP] + +import marimo + +__generated_with = "0.8.0" + +app = marimo.App(width="medium") + + +@app.cell +def __(): + import marimo as mo + import os + import sys + from pathlib import Path + + # Add the llamabot package to the path + sys.path.insert(0, str(Path(__file__).parent.parent)) + + from llamabot.bot.toolbot import ToolBot, toolbot_sysprompt + from llamabot.bot.agentbot import AgentBot + from llamabot.components.mcp_client import MCPConnectionManager + from llamabot.components.mcp_tools import ( + discover_all_mcp_tools, + get_mcp_server_info, + ) + + mo.md( + """ + # MCP Integration Demo + + This notebook demonstrates how to connect ToolBot and AgentBot to external + MCP (Model Context Protocol) servers. + """ + ) + return ( + AgentBot, + MCPConnectionManager, + Path, + ToolBot, + discover_all_mcp_tools, + get_mcp_server_info, + mo, + os, + sys, + toolbot_sysprompt, + ) + + +@app.cell +def __(mo): + mo.md( + """ + ## 1. Basic MCP Connection + + Let's start by connecting to llamabot's own MCP server to demonstrate + the basic connection pattern. + """ + ) + return + + +@app.cell +def __(MCPConnectionManager): + # Configure the llamabot MCP server + llamabot_mcp_server = { + "name": "llamabot_docs", + "command": "uvx", + "args": ["--with", "llamabot[all]", "llamabot", "mcp", "launch"], + "env": {}, + } + + # Create connection manager + connection_manager = MCPConnectionManager([llamabot_mcp_server]) + return connection_manager, llamabot_mcp_server + + +@app.cell +def __(connection_manager, get_mcp_server_info, mo): + # Get server information + server_info = get_mcp_server_info(connection_manager) + mo.display(server_info) + return (server_info,) + + +@app.cell +def __(mo): + mo.md( + """ + ## 2. Tool Discovery + + Now let's discover what tools are available on the MCP server. + """ + ) + return + + +@app.cell +def __(connection_manager, discover_all_mcp_tools, mo): + # Discover tools from the MCP server + mcp_tools = discover_all_mcp_tools(connection_manager) + + mo.md(f"**Discovered {len(mcp_tools)} MCP tools:**") + for tool in mcp_tools: + mo.md( + f"- `{tool.__name__}`: {tool.__doc__.split('.')[0] if tool.__doc__ else 'No description'}" + ) + return (mcp_tools,) + + +@app.cell +def __(mo): + mo.md( + """ + ## 3. ToolBot with MCP Integration + + Now let's create a ToolBot that can use both local tools and MCP tools. + """ + ) + return + + +@app.cell +def __(ToolBot, llamabot_mcp_server, toolbot_sysprompt): + # Create ToolBot with MCP server configuration + toolbot = ToolBot( + system_prompt=toolbot_sysprompt(globals_dict={}), + model_name="ollama_chat/llama3.1:latest", + mcp_servers=[llamabot_mcp_server], + ) + return (toolbot,) + + +@app.cell +def __(mo, toolbot): + # Test the ToolBot with a query that should use MCP tools + mo.md("**Testing ToolBot with MCP integration:**") + + # This will trigger MCP tool discovery + query = "Search for information about ToolBot in the llamabot documentation" + mo.md(f"**Query:** {query}") + + # The bot will discover MCP tools on first call + tool_calls = toolbot(query) + mo.md(f"**Tool calls generated:** {len(tool_calls)}") + + for i, call in enumerate(tool_calls): + mo.md(f"**Tool {i + 1}:** {call.function.name}") + mo.md(f"**Arguments:** {call.function.arguments}") + return call, i, query, tool_calls + + +@app.cell +def __(mo): + mo.md( + """ + ## 4. AgentBot with MCP Integration + + AgentBot can also use MCP tools in its ReAct loop. + """ + ) + return + + +@app.cell +def __(AgentBot, llamabot_mcp_server): + # Create AgentBot with MCP server configuration + agentbot = AgentBot( + model_name="ollama_chat/llama3.1:latest", mcp_servers=[llamabot_mcp_server] + ) + return (agentbot,) + + +@app.cell +def __(agentbot, mo): + mo.md("**Testing AgentBot with MCP integration:**") + + # Test with a complex query that requires multiple steps + complex_query = ( + "Find information about how to use ToolBot and create a simple example" + ) + mo.md(f"**Query:** {complex_query}") + + # AgentBot will use MCP tools in its reasoning loop + try: + result = agentbot(complex_query) + mo.md(f"**Result:** {result.content}") + except Exception as e: + mo.md(f"**Error:** {e}") + return complex_query, e, result + + +@app.cell +def __(mo): + mo.md( + """ + ## 5. Multiple MCP Servers + + You can connect to multiple MCP servers simultaneously. + """ + ) + return + + +@app.cell +def __(mo): + # Example configuration for multiple servers + multiple_servers = [ + { + "name": "llamabot_docs", + "command": "uvx", + "args": ["--with", "llamabot[all]", "llamabot", "mcp", "launch"], + "env": {}, + }, + # Add more servers here as needed + # { + # "name": "filesystem", + # "command": "npx", + # "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"], + # "env": {} + # } + ] + + mo.md("**Multiple server configuration:**") + for server in multiple_servers: + mo.md(f"- **{server['name']}**: {server['command']} {' '.join(server['args'])}") + return multiple_servers, server + + +@app.cell +def __(mo): + mo.md( + """ + ## 6. Best Practices and Error Handling + + ### Best Practices: + + 1. **Lazy Connection**: MCP servers are connected only when first needed + 2. **Error Resilience**: If MCP connection fails, bot continues with local tools + 3. **Namespacing**: MCP tools are prefixed with server name (e.g., `server:tool_name`) + 4. **Resource Management**: Connections are automatically cleaned up + + ### Error Handling: + + The MCP integration includes comprehensive error handling: + - Connection failures don't break the bot + - Tool discovery errors are logged but don't stop execution + - Individual tool call failures are handled gracefully + """ + ) + return + + +@app.cell +def __(mo): + mo.md( + """ + ## 7. Comparison with In-Runtime Tools + + | Feature | In-Runtime Tools | MCP Tools | + |---------|------------------|-----------| + | **Execution** | Same process | External process | + | **Performance** | Fast (no I/O) | Slower (network/process) | + | **Security** | Full access | Sandboxed | + | **Scalability** | Limited | High | + | **Maintenance** | Code changes | Server updates | + | **Discovery** | Static | Dynamic | + + ### When to Use Each: + + - **In-Runtime Tools**: Fast, simple operations, data processing + - **MCP Tools**: External services, file system access, specialized capabilities + """ + ) + return + + +@app.cell +def __(mo): + mo.md( + """ + ## 8. Troubleshooting + + ### Common Issues: + + 1. **Connection Failed**: Check if MCP server is running and accessible + 2. **No Tools Found**: Verify server configuration and permissions + 3. **Tool Execution Error**: Check tool parameters and server logs + 4. **Performance Issues**: Consider connection pooling or caching + + ### Debug Information: + + Enable debug logging to see MCP connection details: + ```python + import logging + logging.getLogger("llamabot.components.mcp_client").setLevel(logging.DEBUG) + ``` + """ + ) + return + + +@app.cell +def __(mo): + mo.md( + """ + ## Conclusion + + This demo shows how MCP integration enhances ToolBot and AgentBot with: + + - **External Tool Access**: Use tools from remote servers + - **Dynamic Discovery**: Automatically find available tools + - **Seamless Integration**: MCP tools work alongside local tools + - **Error Resilience**: Graceful handling of connection issues + + The MCP integration complements the existing tool system, providing + a bridge to external capabilities while maintaining the simplicity + and reliability of the core llamabot functionality. + """ + ) + return + + +if __name__ == "__main__": + app.run()