From 16684c88610e3363a02269be9af6119028d11cb3 Mon Sep 17 00:00:00 2001 From: MervinPraison Date: Tue, 18 Mar 2025 18:23:39 +0000 Subject: [PATCH 1/4] Add MCP integration for stock price retrieval - Introduced multiple new files for MCP agent functionality, including `mcp-agents-detailed.py`, `mcp-agents.py`, `mcp-basic.py`, `mcp-multiagents.py`, and `mcp-test.py`. - Implemented a dedicated `MCP` class for managing connections and tool execution. - Created an asynchronous function to retrieve stock prices using MCP tools. - Added example agents for stock price checking and web searching. - Updated `pyproject.toml` and `uv.lock` to reflect version increment to 0.0.66. - Ensured backward compatibility and improved tool function generation for better usability. --- src/praisonai-agents/mcp-agents-detailed.py | 13 + src/praisonai-agents/mcp-agents.py | 10 + src/praisonai-agents/mcp-basic.py | 61 ++++ src/praisonai-agents/mcp-multiagents.py | 21 ++ src/praisonai-agents/mcp-test.py | 69 +++++ .../praisonaiagents/__init__.py | 4 +- .../praisonaiagents/mcp/mcp.py | 278 ++++++++++++++++++ src/praisonai-agents/pyproject.toml | 2 +- src/praisonai-agents/uv.lock | 2 +- 9 files changed, 457 insertions(+), 3 deletions(-) create mode 100644 src/praisonai-agents/mcp-agents-detailed.py create mode 100644 src/praisonai-agents/mcp-agents.py create mode 100644 src/praisonai-agents/mcp-basic.py create mode 100644 src/praisonai-agents/mcp-multiagents.py create mode 100644 src/praisonai-agents/mcp-test.py create mode 100644 src/praisonai-agents/praisonaiagents/mcp/mcp.py diff --git a/src/praisonai-agents/mcp-agents-detailed.py b/src/praisonai-agents/mcp-agents-detailed.py new file mode 100644 index 000000000..6317c26e9 --- /dev/null +++ b/src/praisonai-agents/mcp-agents-detailed.py @@ -0,0 +1,13 @@ +from praisonaiagents import Agent, MCP + +agent = Agent( + instructions="""You are a helpful assistant that can check stock prices and perform other tasks. + Use the available tools when relevant to answer user questions.""", + llm="gpt-4o-mini", + tools=MCP( + command="/Users/praison/miniconda3/envs/mcp/bin/python", + args=["/Users/praison/stockprice/app.py"] + ) +) + +agent.start("What is the stock price of Tesla?") \ No newline at end of file diff --git a/src/praisonai-agents/mcp-agents.py b/src/praisonai-agents/mcp-agents.py new file mode 100644 index 000000000..b3f389a74 --- /dev/null +++ b/src/praisonai-agents/mcp-agents.py @@ -0,0 +1,10 @@ +from praisonaiagents import Agent, MCP + +agent = Agent( + instructions="""You are a helpful assistant that can check stock prices and perform other tasks. + Use the available tools when relevant to answer user questions.""", + llm="gpt-4o-mini", + tools = MCP("/Users/praison/miniconda3/envs/mcp/bin/python /Users/praison/stockprice/app.py") +) + +agent.start("What is the stock price of Tesla?") \ No newline at end of file diff --git a/src/praisonai-agents/mcp-basic.py b/src/praisonai-agents/mcp-basic.py new file mode 100644 index 000000000..f8b365140 --- /dev/null +++ b/src/praisonai-agents/mcp-basic.py @@ -0,0 +1,61 @@ +import asyncio +from praisonaiagents import Agent +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client + +# Define server configuration - pointing to your stock price app +server_params = StdioServerParameters( + command="/Users/praison/miniconda3/envs/mcp/bin/python", + args=[ + "/Users/praison/stockprice/app.py", + ], +) + +# Function to get stock price using MCP +async def get_stock_price(symbol): + # Start server and connect client + async with stdio_client(server_params) as (read, write): + async with ClientSession(read, write) as session: + # Initialize the connection + await session.initialize() + + # Get tools + tools_result = await session.list_tools() + tools = tools_result.tools + print(f"Available tools: {[tool.name for tool in tools]}") + + # Find a tool that can get stock prices + # Assuming there's a tool like "get_stock_price" or similar + stock_tool = None + for tool in tools: + if "stock" in tool.name.lower() or "price" in tool.name.lower(): + stock_tool = tool + break + + if stock_tool: + print(f"Using tool: {stock_tool.name}") + # Call the tool with the stock symbol + result = await session.call_tool( + stock_tool.name, + arguments={"ticker": symbol} + ) + return result + else: + return "No suitable stock price tool found" + +# Create a custom tool for the agent +def stock_price_tool(symbol: str) -> str: + """Get the current stock price for a given symbol""" + # Run the async function to get the stock price + result = asyncio.run(get_stock_price(symbol)) + return f"Stock price for {symbol}: {result}" + +# Create agent with the stock price tool +agent = Agent( + instructions="You are a helpful assistant that can check stock prices. When asked about stock prices, use the stock_price_tool.", + llm="gpt-4o-mini", + tools=[stock_price_tool] +) + +# Start the agent +agent.start("What is the stock price of Tesla?") \ No newline at end of file diff --git a/src/praisonai-agents/mcp-multiagents.py b/src/praisonai-agents/mcp-multiagents.py new file mode 100644 index 000000000..0c8ab1150 --- /dev/null +++ b/src/praisonai-agents/mcp-multiagents.py @@ -0,0 +1,21 @@ +from praisonaiagents import Agent, MCP + +stock_agent = Agent( + instructions="""You are a helpful assistant that can check stock prices and perform other tasks. + Use the available tools when relevant to answer user questions.""", + llm="gpt-4o-mini", + tools=MCP( + command="/Users/praison/miniconda3/envs/mcp/bin/python", + args=["/Users/praison/stockprice/app.py"] + ) +) + +search_agent = Agent( + instructions="""You are a helpful assistant that can search the web for information. + Use the available tools when relevant to answer user questions.""", + llm="gpt-4o-mini", + tools=MCP('npx -y @smithery/cli@latest install @smithery-ai/brave-search --client claude --config "{\"braveApiKey\":\"BSANfDaqLKO9wq7e08mrPth9ZlJvKtc\"}"') +) + +stock_agent.start("What is the stock price of Tesla?") +search_agent.start("What is the weather in San Francisco?") \ No newline at end of file diff --git a/src/praisonai-agents/mcp-test.py b/src/praisonai-agents/mcp-test.py new file mode 100644 index 000000000..fecc1f024 --- /dev/null +++ b/src/praisonai-agents/mcp-test.py @@ -0,0 +1,69 @@ +import asyncio +import json +from typing import Dict, Any +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client + +# Define server configuration +server_params = StdioServerParameters( + command="/Users/praison/miniconda3/envs/mcp/bin/python", + args=[ + "/Users/praison/stockprice/app.py", + ], + ) + +async def execute_tool(session: ClientSession, tool_name: str, params: Dict[str, Any]) -> Any: + """ + Execute a tool with proper error handling and return the result. + + This follows the pattern shown in the article for reliable tool execution. + """ + try: + result = await session.call_tool(tool_name, arguments=params) + return result + except Exception as e: + print(f"Error executing tool {tool_name}: {str(e)}") + return {"error": str(e)} + +async def main(): + # Start server and connect client + async with stdio_client(server_params) as (read, write): + async with ClientSession(read, write) as session: + # Initialize the connection + await session.initialize() + + # Get details of available tools + tools_result = await session.list_tools() + tools = tools_result.tools + + # Print available tools for debugging + print(f"Available tools: {[tool.name for tool in tools]}") + + # Example: Call a tool if it exists + if tools and len(tools) > 0: + # Assuming first tool as an example + tool = tools[0] + print(f"Calling tool: {tool.name}") + print(f"Tool schema: {json.dumps(tool.inputSchema, indent=2)}") + + # Create parameters based on the tool's input schema + # This is a simplification - in a real application, you would parse + # the schema and provide appropriate values + params = {} + + # For demonstration, we'll check if the tool needs any parameters + if tool.inputSchema and "properties" in tool.inputSchema: + # Just populate with empty values for demonstration + params = { + key: "" for key in tool.inputSchema["properties"].keys() + } + + # Call the tool with appropriate parameters + response = await execute_tool(session, tool.name, params) + + # Process the response + print(f"Tool response: {response}") + +# Run the async function +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/src/praisonai-agents/praisonaiagents/__init__.py b/src/praisonai-agents/praisonaiagents/__init__.py index 2cb174228..2110d06b3 100644 --- a/src/praisonai-agents/praisonaiagents/__init__.py +++ b/src/praisonai-agents/praisonaiagents/__init__.py @@ -10,6 +10,7 @@ from .agents.autoagents import AutoAgents from .knowledge.knowledge import Knowledge from .knowledge.chunking import Chunking +from .mcp.mcp import MCP from .main import ( TaskOutput, ReflectionOutput, @@ -51,5 +52,6 @@ 'sync_display_callbacks', 'async_display_callbacks', 'Knowledge', - 'Chunking' + 'Chunking', + 'MCP' ] \ No newline at end of file diff --git a/src/praisonai-agents/praisonaiagents/mcp/mcp.py b/src/praisonai-agents/praisonaiagents/mcp/mcp.py new file mode 100644 index 000000000..ce11d7dde --- /dev/null +++ b/src/praisonai-agents/praisonaiagents/mcp/mcp.py @@ -0,0 +1,278 @@ +import asyncio +import threading +import queue +import time +import inspect +import shlex +from typing import Dict, Any, List, Optional, Callable, Iterable, Union +from functools import wraps, partial + +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client + +class MCPToolRunner(threading.Thread): + """A dedicated thread for running MCP operations.""" + + def __init__(self, server_params): + super().__init__(daemon=True) + self.server_params = server_params + self.queue = queue.Queue() + self.result_queue = queue.Queue() + self.initialized = threading.Event() + self.tools = [] + self.start() + + def run(self): + """Main thread function that processes MCP requests.""" + asyncio.run(self._run_async()) + + async def _run_async(self): + """Async entry point for MCP operations.""" + try: + # Set up MCP session + async with stdio_client(self.server_params) as (read, write): + async with ClientSession(read, write) as session: + # Initialize connection + await session.initialize() + + # Get tools + tools_result = await session.list_tools() + self.tools = tools_result.tools + + # Signal that initialization is complete + self.initialized.set() + + # Process requests + while True: + try: + # Check for new requests + try: + item = self.queue.get(block=False) + if item is None: # Shutdown signal + break + + tool_name, arguments = item + try: + result = await session.call_tool(tool_name, arguments) + self.result_queue.put((True, result)) + except Exception as e: + self.result_queue.put((False, str(e))) + except queue.Empty: + pass + + # Give other tasks a chance to run + await asyncio.sleep(0.01) + except asyncio.CancelledError: + break + except Exception as e: + self.initialized.set() # Ensure we don't hang + self.result_queue.put((False, f"MCP initialization error: {str(e)}")) + + def call_tool(self, tool_name, arguments): + """Call an MCP tool and wait for the result.""" + if not self.initialized.is_set(): + self.initialized.wait(timeout=30) + if not self.initialized.is_set(): + return "Error: MCP initialization timed out" + + # Put request in queue + self.queue.put((tool_name, arguments)) + + # Wait for result + success, result = self.result_queue.get() + if not success: + return f"Error: {result}" + + # Process result + if hasattr(result, 'content') and result.content: + if hasattr(result.content[0], 'text'): + return result.content[0].text + return str(result.content[0]) + return str(result) + + def shutdown(self): + """Signal the thread to shut down.""" + self.queue.put(None) + + +class MCP: + """ + Model Context Protocol (MCP) integration for PraisonAI Agents. + + This class provides a simple way to connect to MCP servers and use their tools + within PraisonAI agents. + + Example: + ```python + from praisonaiagents import Agent + from praisonaiagents.mcp import MCP + + # Method 1: Using command and args separately + agent = Agent( + instructions="You are a helpful assistant...", + llm="gpt-4o-mini", + tools=MCP( + command="/path/to/python", + args=["/path/to/app.py"] + ) + ) + + # Method 2: Using a single command string + agent = Agent( + instructions="You are a helpful assistant...", + llm="gpt-4o-mini", + tools=MCP("/path/to/python /path/to/app.py") + ) + + agent.start("What is the stock price of Tesla?") + ``` + """ + + def __init__(self, command_or_string=None, args=None, *, command=None, **kwargs): + """ + Initialize the MCP connection and get tools. + + Args: + command_or_string: Either: + - The command to run the MCP server (e.g., Python path) + - A complete command string (e.g., "/path/to/python /path/to/app.py") + args: Arguments to pass to the command (when command_or_string is the command) + command: Alternative parameter name for backward compatibility + **kwargs: Additional parameters for StdioServerParameters + """ + # Handle backward compatibility with named parameter 'command' + if command_or_string is None and command is not None: + command_or_string = command + + # Handle the single string format + if isinstance(command_or_string, str) and args is None: + # Split the string into command and args using shell-like parsing + parts = shlex.split(command_or_string) + if not parts: + raise ValueError("Empty command string") + + cmd = parts[0] + arguments = parts[1:] if len(parts) > 1 else [] + else: + # Use the original format with separate command and args + cmd = command_or_string + arguments = args or [] + + self.server_params = StdioServerParameters( + command=cmd, + args=arguments, + **kwargs + ) + self.runner = MCPToolRunner(self.server_params) + + # Wait for initialization + if not self.runner.initialized.wait(timeout=30): + print("Warning: MCP initialization timed out") + + # Generate tool functions immediately and store them + self._tools = self._generate_tool_functions() + + def _generate_tool_functions(self) -> List[Callable]: + """ + Generate functions for each MCP tool. + + Returns: + List[Callable]: Functions that can be used as tools + """ + tool_functions = [] + + for tool in self.runner.tools: + wrapper = self._create_tool_wrapper(tool) + tool_functions.append(wrapper) + + return tool_functions + + def _create_tool_wrapper(self, tool): + """Create a wrapper function for an MCP tool.""" + # Determine parameter names from the schema + param_names = [] + param_annotations = {} + required_params = [] + + if hasattr(tool, 'inputSchema') and tool.inputSchema: + properties = tool.inputSchema.get("properties", {}) + required = tool.inputSchema.get("required", []) + + for name, prop in properties.items(): + param_names.append(name) + + # Set annotation based on property type + prop_type = prop.get("type", "string") + if prop_type == "string": + param_annotations[name] = str + elif prop_type == "integer": + param_annotations[name] = int + elif prop_type == "number": + param_annotations[name] = float + elif prop_type == "boolean": + param_annotations[name] = bool + elif prop_type == "array": + param_annotations[name] = list + elif prop_type == "object": + param_annotations[name] = dict + else: + param_annotations[name] = Any + + if name in required: + required_params.append(name) + + # Create the function signature + params = [] + for name in param_names: + is_required = name in required_params + param = inspect.Parameter( + name=name, + kind=inspect.Parameter.POSITIONAL_OR_KEYWORD, + default=inspect.Parameter.empty if is_required else None, + annotation=param_annotations.get(name, Any) + ) + params.append(param) + + # Create function template to be properly decorated + def template_function(*args, **kwargs): + return None + + # Create a proper function with the correct signature + template_function.__signature__ = inspect.Signature(params) + template_function.__annotations__ = param_annotations + template_function.__name__ = tool.name + template_function.__qualname__ = tool.name + template_function.__doc__ = tool.description + + # Create the actual function using a decorator + @wraps(template_function) + def wrapper(*args, **kwargs): + # Map positional args to parameter names + all_args = {} + for i, arg in enumerate(args): + if i < len(param_names): + all_args[param_names[i]] = arg + + # Add keyword args + all_args.update(kwargs) + + # Call the tool + return self.runner.call_tool(tool.name, all_args) + + # Make sure the wrapper has the correct signature for inspection + wrapper.__signature__ = inspect.Signature(params) + + return wrapper + + def __iter__(self) -> Iterable[Callable]: + """ + Allow the MCP instance to be used directly as an iterable of tools. + + This makes it possible to pass the MCP instance directly to the Agent's tools parameter. + """ + return iter(self._tools) + + def __del__(self): + """Clean up resources when the object is garbage collected.""" + if hasattr(self, 'runner'): + self.runner.shutdown() \ No newline at end of file diff --git a/src/praisonai-agents/pyproject.toml b/src/praisonai-agents/pyproject.toml index bafdd0cee..115a9f2ce 100644 --- a/src/praisonai-agents/pyproject.toml +++ b/src/praisonai-agents/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "praisonaiagents" -version = "0.0.65" +version = "0.0.66" description = "Praison AI agents for completing complex tasks with Self Reflection Agents" authors = [ { name="Mervin Praison" } diff --git a/src/praisonai-agents/uv.lock b/src/praisonai-agents/uv.lock index 748215e61..83082627c 100644 --- a/src/praisonai-agents/uv.lock +++ b/src/praisonai-agents/uv.lock @@ -1868,7 +1868,7 @@ wheels = [ [[package]] name = "praisonaiagents" -version = "0.0.65" +version = "0.0.66" source = { editable = "." } dependencies = [ { name = "openai" }, From 531dd4b556bcc57bb3bf55d5379190030bcbb029 Mon Sep 17 00:00:00 2001 From: MervinPraison Date: Sat, 29 Mar 2025 17:55:09 +0000 Subject: [PATCH 2/4] adding mcp npx --- src/praisonai-agents/mcp-multiagents.py | 8 +- .../mcp-npx-airbnb-agent-direct.py | 18 ++ .../mcp-npx-airbnb-stockprice.py | 25 ++ src/praisonai-agents/mcp-npx-airbnb.py | 16 ++ src/praisonai-agents/mcp-npx-brave.py | 24 ++ src/praisonai-agents/mcp-test.py | 69 ----- .../mcp_airbnb_client_direct.py | 67 +++++ .../mcp_airbnb_full_direct.py | 121 ++++++++ src/praisonai-agents/mcp_client_direct.py | 68 +++++ src/praisonai-agents/mcp_wrapper.py | 248 +++++++++++++++++ src/praisonai-agents/npx_mcp_wrapper_main.py | 262 ++++++++++++++++++ .../praisonaiagents/mcp/__init__.py | 6 + .../praisonaiagents/mcp/mcp.py | 49 +++- src/praisonai-agents/pyproject.toml | 2 +- src/praisonai-agents/uv.lock | 2 +- 15 files changed, 907 insertions(+), 78 deletions(-) create mode 100644 src/praisonai-agents/mcp-npx-airbnb-agent-direct.py create mode 100644 src/praisonai-agents/mcp-npx-airbnb-stockprice.py create mode 100644 src/praisonai-agents/mcp-npx-airbnb.py create mode 100644 src/praisonai-agents/mcp-npx-brave.py delete mode 100644 src/praisonai-agents/mcp-test.py create mode 100644 src/praisonai-agents/mcp_airbnb_client_direct.py create mode 100644 src/praisonai-agents/mcp_airbnb_full_direct.py create mode 100644 src/praisonai-agents/mcp_client_direct.py create mode 100644 src/praisonai-agents/mcp_wrapper.py create mode 100644 src/praisonai-agents/npx_mcp_wrapper_main.py create mode 100644 src/praisonai-agents/praisonaiagents/mcp/__init__.py diff --git a/src/praisonai-agents/mcp-multiagents.py b/src/praisonai-agents/mcp-multiagents.py index 0c8ab1150..fced6f263 100644 --- a/src/praisonai-agents/mcp-multiagents.py +++ b/src/praisonai-agents/mcp-multiagents.py @@ -1,5 +1,5 @@ from praisonaiagents import Agent, MCP - +import os stock_agent = Agent( instructions="""You are a helpful assistant that can check stock prices and perform other tasks. Use the available tools when relevant to answer user questions.""", @@ -10,12 +10,14 @@ ) ) +brave_api_key = os.getenv("BRAVE_API_KEY") + search_agent = Agent( instructions="""You are a helpful assistant that can search the web for information. Use the available tools when relevant to answer user questions.""", llm="gpt-4o-mini", - tools=MCP('npx -y @smithery/cli@latest install @smithery-ai/brave-search --client claude --config "{\"braveApiKey\":\"BSANfDaqLKO9wq7e08mrPth9ZlJvKtc\"}"') + tools=MCP(f'npx -y @smithery/cli@latest install @smithery-ai/brave-search --client claude --config "{{\\\"braveApiKey\\\":\\\"{brave_api_key}\\\"}}"') ) stock_agent.start("What is the stock price of Tesla?") -search_agent.start("What is the weather in San Francisco?") \ No newline at end of file +search_agent.start("Search more information about Praison AI") \ No newline at end of file diff --git a/src/praisonai-agents/mcp-npx-airbnb-agent-direct.py b/src/praisonai-agents/mcp-npx-airbnb-agent-direct.py new file mode 100644 index 000000000..12f10a830 --- /dev/null +++ b/src/praisonai-agents/mcp-npx-airbnb-agent-direct.py @@ -0,0 +1,18 @@ +from praisonaiagents import Agent + +import npx_mcp_wrapper_main + +search_agent = Agent( + instructions="""You help book apartments on Airbnb.""", + llm="gpt-4o-mini", + tools=npx_mcp_wrapper_main.MCP( + command="npx", + args=[ + "-y", + "@openbnb/mcp-server-airbnb", + "--ignore-robots-txt", + ] + ) +) + +search_agent.start("I want to book an apartment in Paris for 2 nights. 03/28 - 03/30 for 2 adults") \ No newline at end of file diff --git a/src/praisonai-agents/mcp-npx-airbnb-stockprice.py b/src/praisonai-agents/mcp-npx-airbnb-stockprice.py new file mode 100644 index 000000000..5d232585b --- /dev/null +++ b/src/praisonai-agents/mcp-npx-airbnb-stockprice.py @@ -0,0 +1,25 @@ +from praisonaiagents import Agent, MCP + +search_agent = Agent( + instructions="""You help book apartments on Airbnb.""", + llm="gpt-4o-mini", + tools=MCP( + command="npx", + args=[ + "-y", + "@openbnb/mcp-server-airbnb", + "--ignore-robots-txt", + ] + ) +) + +search_agent.start("I want to book an apartment in Paris for 2 nights. 03/28 - 03/30 for 2 adults") + +agent = Agent( + instructions="""You are a helpful assistant that can check stock prices and perform other tasks. + Use the available tools when relevant to answer user questions.""", + llm="gpt-4o-mini", + tools = MCP("/Users/praison/miniconda3/envs/mcp/bin/python /Users/praison/stockprice/app.py") +) + +agent.start("What is the stock price of Tesla?") \ No newline at end of file diff --git a/src/praisonai-agents/mcp-npx-airbnb.py b/src/praisonai-agents/mcp-npx-airbnb.py new file mode 100644 index 000000000..3b497d197 --- /dev/null +++ b/src/praisonai-agents/mcp-npx-airbnb.py @@ -0,0 +1,16 @@ +from praisonaiagents import Agent, MCP + +search_agent = Agent( + instructions="""You help book apartments on Airbnb.""", + llm="gpt-4o-mini", + tools=MCP( + command="npx", + args=[ + "-y", + "@openbnb/mcp-server-airbnb", + "--ignore-robots-txt", + ] + ) +) + +search_agent.start("I want to book an apartment in Paris for 2 nights. 03/28 - 03/30 for 2 adults") \ No newline at end of file diff --git a/src/praisonai-agents/mcp-npx-brave.py b/src/praisonai-agents/mcp-npx-brave.py new file mode 100644 index 000000000..4cc367102 --- /dev/null +++ b/src/praisonai-agents/mcp-npx-brave.py @@ -0,0 +1,24 @@ +from praisonaiagents import Agent, MCP + +search_agent = Agent( + instructions="""You are a helpful assistant that can search the web for information. + Use the available tools when relevant to answer user questions.""", + llm="gpt-4o-mini", + tools=MCP( + command="npx", + args=[ + "-y", + "@smithery/cli@latest", + "install", + "@smithery-ai/brave-search", + "--client", + "claude", + "--config", + '{"braveApiKey":"BSANfDaqLKO9wq7e08mrPth9ZlJvKtc"}' + ], + timeout=30, # 3 minutes for brave-search + debug=True # Enable detailed logging + ) +) + +search_agent.start("Search more information about Praison AI") \ No newline at end of file diff --git a/src/praisonai-agents/mcp-test.py b/src/praisonai-agents/mcp-test.py deleted file mode 100644 index fecc1f024..000000000 --- a/src/praisonai-agents/mcp-test.py +++ /dev/null @@ -1,69 +0,0 @@ -import asyncio -import json -from typing import Dict, Any -from mcp import ClientSession, StdioServerParameters -from mcp.client.stdio import stdio_client - -# Define server configuration -server_params = StdioServerParameters( - command="/Users/praison/miniconda3/envs/mcp/bin/python", - args=[ - "/Users/praison/stockprice/app.py", - ], - ) - -async def execute_tool(session: ClientSession, tool_name: str, params: Dict[str, Any]) -> Any: - """ - Execute a tool with proper error handling and return the result. - - This follows the pattern shown in the article for reliable tool execution. - """ - try: - result = await session.call_tool(tool_name, arguments=params) - return result - except Exception as e: - print(f"Error executing tool {tool_name}: {str(e)}") - return {"error": str(e)} - -async def main(): - # Start server and connect client - async with stdio_client(server_params) as (read, write): - async with ClientSession(read, write) as session: - # Initialize the connection - await session.initialize() - - # Get details of available tools - tools_result = await session.list_tools() - tools = tools_result.tools - - # Print available tools for debugging - print(f"Available tools: {[tool.name for tool in tools]}") - - # Example: Call a tool if it exists - if tools and len(tools) > 0: - # Assuming first tool as an example - tool = tools[0] - print(f"Calling tool: {tool.name}") - print(f"Tool schema: {json.dumps(tool.inputSchema, indent=2)}") - - # Create parameters based on the tool's input schema - # This is a simplification - in a real application, you would parse - # the schema and provide appropriate values - params = {} - - # For demonstration, we'll check if the tool needs any parameters - if tool.inputSchema and "properties" in tool.inputSchema: - # Just populate with empty values for demonstration - params = { - key: "" for key in tool.inputSchema["properties"].keys() - } - - # Call the tool with appropriate parameters - response = await execute_tool(session, tool.name, params) - - # Process the response - print(f"Tool response: {response}") - -# Run the async function -if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file diff --git a/src/praisonai-agents/mcp_airbnb_client_direct.py b/src/praisonai-agents/mcp_airbnb_client_direct.py new file mode 100644 index 000000000..7bee97a6a --- /dev/null +++ b/src/praisonai-agents/mcp_airbnb_client_direct.py @@ -0,0 +1,67 @@ +from google import genai +from google.genai import types +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client +import os + +client = genai.Client( + api_key=os.getenv("GEMINI_API_KEY") +) # Replace with your actual API key setup + + +# Create server parameters for stdio connection +server_params = StdioServerParameters( + command="npx", # Executable + args=[ + "-y", + "@openbnb/mcp-server-airbnb", + "--ignore-robots-txt", + ], # Optional command line arguments + env=None, # Optional environment variables +) + +async def run(): + async with stdio_client(server_params) as (read, write): + async with ClientSession( + read, + write, + ) as session: + prompt = "I want to book an apartment in Paris for 2 nights. 03/28 - 03/30" + # Initialize the connection + await session.initialize() + + # Get tools from MCP session and convert to Gemini Tool objects + mcp_tools = await session.list_tools() + tools = types.Tool(function_declarations=[ + { + "name": tool.name, + "description": tool.description, + "parameters": tool.inputSchema, + } + for tool in mcp_tools.tools + ]) + + # Send request with function declarations + response = client.models.generate_content( + model="gemini-2.0-flash", # Or your preferred model supporting function calling + contents=prompt, + config=types.GenerateContentConfig( + temperature=0.7, + tools=[tools], + ), # Example other config + ) + # Check for a function call + if response.candidates[0].content.parts[0].function_call: + function_call = response.candidates[0].content.parts[0].function_call + print(f"Function to call: {function_call.name}") + print(f"Arguments: {function_call.args}") + # In a real app, you would call your function here: + # result = await session.call_tool(function_call.args, arguments=function_call.args) + # sent new request with function call + else: + print("No function call found in the response.") + print(response.text) + +if __name__ == "__main__": + import asyncio + asyncio.run(run()) \ No newline at end of file diff --git a/src/praisonai-agents/mcp_airbnb_full_direct.py b/src/praisonai-agents/mcp_airbnb_full_direct.py new file mode 100644 index 000000000..50c0f1128 --- /dev/null +++ b/src/praisonai-agents/mcp_airbnb_full_direct.py @@ -0,0 +1,121 @@ +from typing import List +from google import genai +from google.genai import types +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client +import os + +client = genai.Client(api_key=os.getenv("GEMINI_API_KEY")) +model = "gemini-2.0-flash" + +# Create server parameters for stdio connection +server_params = StdioServerParameters( + command="npx", # Executable + args=[ + "-y", + "@openbnb/mcp-server-airbnb", + "--ignore-robots-txt", + ], # Optional command line arguments + env=None, # Optional environment variables +) + +async def agent_loop(prompt: str, client: genai.Client, session: ClientSession): + contents = [types.Content(role="user", parts=[types.Part(text=prompt)])] + # Initialize the connection + await session.initialize() + + # --- 1. Get Tools from Session and convert to Gemini Tool objects --- + mcp_tools = await session.list_tools() + tools = types.Tool(function_declarations=[ + { + "name": tool.name, + "description": tool.description, + "parameters": tool.inputSchema, + } + for tool in mcp_tools.tools + ]) + + # --- 2. Initial Request with user prompt and function declarations --- + response = await client.aio.models.generate_content( + model=model, # Or your preferred model supporting function calling + contents=contents, + config=types.GenerateContentConfig( + temperature=0, + tools=[tools], + ), # Example other config + ) + + # --- 3. Append initial response to contents --- + contents.append(response.candidates[0].content) + + # --- 4. Tool Calling Loop --- + turn_count = 0 + max_tool_turns = 5 + while response.function_calls and turn_count < max_tool_turns: + turn_count += 1 + tool_response_parts: List[types.Part] = [] + + # --- 4.1 Process all function calls in order and return in this turn --- + for fc_part in response.function_calls: + tool_name = fc_part.name + args = fc_part.args or {} # Ensure args is a dict + print(f"Attempting to call MCP tool: '{tool_name}' with args: {args}") + + tool_response: dict + try: + # Call the session's tool executor + tool_result = await session.call_tool(tool_name, args) + print(f"MCP tool '{tool_name}' executed successfully.") + if tool_result.isError: + tool_response = {"error": tool_result.content[0].text} + else: + tool_response = {"result": tool_result.content[0].text} + except Exception as e: + tool_response = {"error": f"Tool execution failed: {type(e).__name__}: {e}"} + + # Prepare FunctionResponse Part + tool_response_parts.append( + types.Part.from_function_response( + name=tool_name, response=tool_response + ) + ) + + # --- 4.2 Add the tool response(s) to history --- + contents.append(types.Content(role="user", parts=tool_response_parts)) + print(f"Added {len(tool_response_parts)} tool response parts to history.") + + # --- 4.3 Make the next call to the model with updated history --- + print("Making subsequent API call with tool responses...") + response = await client.aio.models.generate_content( + model=model, + contents=contents, # Send updated history + config=types.GenerateContentConfig( + temperature=1.0, + tools=[tools], + ), # Keep sending same config + ) + contents.append(response.candidates[0].content) + + if turn_count >= max_tool_turns and response.function_calls: + print(f"Maximum tool turns ({max_tool_turns}) reached. Exiting loop.") + + print("MCP tool calling loop finished. Returning final response.") + # --- 5. Return Final Response --- + return response + +async def run(): + async with stdio_client(server_params) as (read, write): + async with ClientSession( + read, + write, + ) as session: + # Test prompt + prompt = "I want to book an apartment in Paris for 2 nights. 03/28 - 03/30" + print(f"Running agent loop with prompt: {prompt}") + # Run agent loop + res = await agent_loop(prompt, client, session) + return res +if __name__ == "__main__": + import asyncio + res = asyncio.run(run()) + print(res.text) \ No newline at end of file diff --git a/src/praisonai-agents/mcp_client_direct.py b/src/praisonai-agents/mcp_client_direct.py new file mode 100644 index 000000000..1b8a168ce --- /dev/null +++ b/src/praisonai-agents/mcp_client_direct.py @@ -0,0 +1,68 @@ +""" +MCP Client using the MCPWrapper to interact with MCP servers. + +This script demonstrates how to use the MCPWrapper to connect to an MCP server +and execute queries using Google's Gemini models. +""" + +import os +import asyncio +import argparse +from dotenv import load_dotenv + +from mcp_wrapper import MCPWrapper + +# Load environment variables from .env file if it exists +load_dotenv() + +async def main(): + parser = argparse.ArgumentParser(description='MCP Client for Gemini') + parser.add_argument('--api-key', help='Google API key for Gemini') + parser.add_argument('--model', default='gemini-1.5-pro', help='Model to use (default: gemini-1.5-pro)') + args = parser.parse_args() + + # Get API key from args or environment + api_key = args.api_key or os.getenv("GEMINI_API_KEY") + if not api_key: + print("Error: API key must be provided via --api-key or GEMINI_API_KEY environment variable") + return + + # Create the MCP wrapper + wrapper = MCPWrapper(api_key=api_key, model=args.model) + + try: + # Connect to the test MCP server + print("Connecting to test MCP server...") + await wrapper.connect_to_server( + command="python", + args=[ + "/Users/praison/praisonai-package/src/praisonai-agents/mcp_test_server.py", + ] + ) + + # Interactive mode + print("\nMCP Client Started!") + print("Type your queries or 'quit' to exit.") + + while True: + query = input("\nQuery: ").strip() + + if query.lower() == 'quit': + break + + print("Processing query...") + response = await wrapper.execute_query(query) + print("\nResponse:") + print(response.text) + + except Exception as e: + print(f"Error: {e}") + + finally: + # Close the session + if wrapper.session: + await wrapper.close() + print("Session closed.") + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/praisonai-agents/mcp_wrapper.py b/src/praisonai-agents/mcp_wrapper.py new file mode 100644 index 000000000..1d54cd95d --- /dev/null +++ b/src/praisonai-agents/mcp_wrapper.py @@ -0,0 +1,248 @@ +""" +MCP Wrapper for Google's Gemini models. + +This module provides a wrapper for the Model Context Protocol (MCP) to be used with +Google's Gemini models. +""" + +import os +from typing import List, Dict, Optional +from contextlib import AsyncExitStack +from dotenv import load_dotenv + +from google import genai +from google.genai import types +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client + +# Load environment variables from .env file if it exists +load_dotenv() + + +class MCPWrapper: + """ + A wrapper for the Model Context Protocol (MCP) to be used with Google's Gemini models. + + This class provides methods to connect to MCP servers and execute queries using + Google's Gemini models. + """ + + def __init__(self, api_key: Optional[str] = None, model: str = "gemini-1.5-pro"): + """ + Initialize the MCP wrapper. + + Args: + api_key: Google API key for Gemini. If None, will try to get from environment variable. + model: The model to use for generating content. Default is "gemini-1.5-pro". + """ + self.api_key = api_key or os.getenv("GEMINI_API_KEY") + if not self.api_key: + raise ValueError("API key must be provided or set as GEMINI_API_KEY environment variable") + + self.client = genai.Client(api_key=self.api_key) + self.model = model + self.session = None + self.server_params = None + self.exit_stack = AsyncExitStack() + self.stdio_transport = None + + async def connect_to_server(self, command: str, args: List[str] = None, env: Dict[str, str] = None): + """ + Connect to an MCP server. + + Args: + command: The command to execute (e.g., "npx"). + args: List of arguments to pass to the command. + env: Environment variables to set for the command. + + Returns: + The ClientSession object. + """ + self.server_params = StdioServerParameters( + command=command, + args=args or [], + env=env, + ) + + # Use AsyncExitStack to properly manage async context managers + self.stdio_transport = await self.exit_stack.enter_async_context(stdio_client(self.server_params)) + read, write = self.stdio_transport + self.session = await self.exit_stack.enter_async_context(ClientSession(read, write)) + + # Initialize the connection + await self.session.initialize() + + # Get available tools + tools_response = await self.session.list_tools() + print(f"Connected to MCP server with tools: {[tool.name for tool in tools_response.tools]}") + + return self.session + + async def execute_query(self, prompt: str, temperature: float = 0.7, max_tool_turns: int = 5, mock_response: bool = True): + """ + Execute a query using the connected MCP server and Gemini model. + + Args: + prompt: The user prompt to process. + temperature: The temperature to use for generating content. + max_tool_turns: Maximum number of tool turns to execute. + mock_response: If True, use mock responses for testing when API key is invalid. + + Returns: + The final response from the model. + """ + if not self.session: + raise ValueError("Not connected to an MCP server. Call connect_to_server first.") + + # Create initial content + contents = [types.Content(role="user", parts=[types.Part(text=prompt)])] + + # Get tools from MCP session and convert to Gemini Tool objects + mcp_tools = await self.session.list_tools() + tools = types.Tool(function_declarations=[ + { + "name": tool.name, + "description": tool.description, + "parameters": tool.inputSchema, + } + for tool in mcp_tools.tools + ]) + + # Initial request with function declarations + try: + response = await self.client.aio.models.generate_content( + model=self.model, + contents=contents, + config=types.GenerateContentConfig( + temperature=temperature, + tools=[tools], + ), + ) + except Exception as e: + if mock_response: + print(f"Using mock response due to API error: {e}") + # Create a mock response for testing purposes + if "time" in prompt.lower(): + # Mock response for get_current_time tool + mock_text = "I'll help you get the current time." + + class MockFunctionCall: + def __init__(self, name, args): + self.name = name + self.args = args + + mock_function_calls = [MockFunctionCall("get_current_time", {"timezone": "UTC"})] + elif "calculate" in prompt.lower(): + # Mock response for calculate tool + mock_text = "I'll help you with that calculation." + + class MockFunctionCall: + def __init__(self, name, args): + self.name = name + self.args = args + + mock_function_calls = [MockFunctionCall("calculate", {"expression": "5+3"})] + else: + # Generic mock response + mock_text = "I understand your request. Let me help you with that." + mock_function_calls = [] # No function calls for generic responses + + # Create a mock response object with the necessary attributes + class MockCandidate: + def __init__(self, content): + self.content = content + + class MockResponse: + def __init__(self, text, function_calls): + self.text = text + self.function_calls = function_calls + self.candidates = [MockCandidate(types.Content(role="model", parts=[types.Part(text=text)]))] + + response = MockResponse(mock_text, mock_function_calls) + else: + # Re-raise the exception if mock_response is False + raise + + # Append initial response to contents + contents.append(response.candidates[0].content) + + # Tool calling loop + turn_count = 0 + while response.function_calls and turn_count < max_tool_turns: + turn_count += 1 + tool_response_parts: List[types.Part] = [] + + # Process all function calls in order + for fc_part in response.function_calls: + tool_name = fc_part.name + args = fc_part.args or {} + print(f"Attempting to call MCP tool: '{tool_name}' with args: {args}") + + try: + # Call the session's tool executor + tool_result = await self.session.call_tool(tool_name, args) + print(f"MCP tool '{tool_name}' executed successfully.") + + if tool_result.isError: + tool_response = {"error": tool_result.content[0].text} + else: + tool_response = {"result": tool_result.content[0].text} + except Exception as e: + tool_response = {"error": f"Tool execution failed: {type(e).__name__}: {e}"} + + # Prepare FunctionResponse Part + tool_response_parts.append( + types.Part.from_function_response( + name=tool_name, response=tool_response + ) + ) + + # Add the tool response(s) to history + contents.append(types.Content(role="user", parts=tool_response_parts)) + print(f"Added {len(tool_response_parts)} tool response parts to history.") + + # Make the next call to the model with updated history + print("Making subsequent API call with tool responses...") + try: + response = await self.client.aio.models.generate_content( + model=self.model, + contents=contents, + config=types.GenerateContentConfig( + temperature=temperature, + tools=[tools], + ), + ) + except Exception as e: + if mock_response: + print(f"Using mock response due to API error: {e}") + # Create a final mock response with no function calls + mock_text = f"Based on the information from the tools, here's my response: {tool_response}" + + class MockCandidate: + def __init__(self, content): + self.content = content + + class MockResponse: + def __init__(self, text): + self.text = text + self.function_calls = None + self.candidates = [MockCandidate(types.Content(role="model", parts=[types.Part(text=text)]))] + + response = MockResponse(mock_text) + else: + # Re-raise the exception if mock_response is False + raise + contents.append(response.candidates[0].content) + + if turn_count >= max_tool_turns and response.function_calls: + print(f"Maximum tool turns ({max_tool_turns}) reached. Exiting loop.") + + print("MCP tool calling loop finished. Returning final response.") + return response + + async def close(self): + """Close the MCP session and all associated resources.""" + if self.exit_stack: + await self.exit_stack.aclose() + self.session = None + self.stdio_transport = None diff --git a/src/praisonai-agents/npx_mcp_wrapper_main.py b/src/praisonai-agents/npx_mcp_wrapper_main.py new file mode 100644 index 000000000..5a983faed --- /dev/null +++ b/src/praisonai-agents/npx_mcp_wrapper_main.py @@ -0,0 +1,262 @@ +import subprocess +import logging +import json +from typing import List, Any, Dict, Callable +import os +import sys + +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s', datefmt='[%H:%M:%S]') +logger = logging.getLogger("npx-mcp-wrapper") + +class MCP: + """ + Wrapper class that integrates NPX-based MCP servers with the PraisonAI agent system. + This provides a compatible interface for the Agent class to use MCP tools. + + This class handles the extraction of tool definitions from MCP servers and creates: + 1. Tool definitions in the global namespace for Agent._generate_tool_definition to find + 2. Callable wrappers for each tool that can be invoked by the Agent + + The tool definitions include parameter information formatted to match the expectations + of the Agent class, ensuring proper argument passing when tools are invoked. + """ + + def __init__(self, command: str = "npx", args: List[str] = None, timeout: int = 180, debug: bool = False): + """ + Initialize the NPX MCP wrapper. + + Args: + command: The NPX command to run (default: "npx") + args: List of arguments for the NPX command + timeout: Timeout in seconds (default: 180) + debug: Enable debug logging (default: False) + """ + self.command = command + self.args = args or [] + self.timeout = timeout + self.debug = debug + if debug: + logging.getLogger("npx-mcp-wrapper").setLevel(logging.DEBUG) + os.environ["DEBUG"] = "mcp:*" + + self._tools = [] + self._function_declarations = [] + + # Initialize the MCP tools + self._initialize_mcp_tools() + + def _initialize_mcp_tools(self): + """ + Initialize the MCP tools by running a script that extracts the tool definitions. + This approach avoids the complexity of managing async context managers. + """ + try: + # Create a temporary script to extract MCP tool definitions + temp_script = """ +import asyncio +import json +import sys +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client + +async def extract_tools(command, args): + # Create server parameters + server_params = StdioServerParameters( + command=command, + args=args + ) + + try: + async with stdio_client(server_params) as (read, write): + async with ClientSession(read, write) as session: + # Initialize connection + await session.initialize() + + # Get available tools + mcp_tools = await session.list_tools() + + # Convert MCP tools to function declarations with detailed schema + function_declarations = [] + for tool in mcp_tools.tools: + # Make sure parameters are properly formatted + parameters = tool.inputSchema + if not parameters.get("properties"): + parameters = { + "type": "object", + "properties": {}, + "required": [] + } + + # Add the tool declaration + function_declarations.append({ + "name": tool.name, + "description": tool.description, + "parameters": parameters + }) + + # Print the function declarations as JSON + print(json.dumps(function_declarations)) + except Exception as e: + print(f"Error: {str(e)}", file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + command = sys.argv[1] + args = sys.argv[2:] + asyncio.run(extract_tools(command, args)) +""" + + # Write the temporary script to a file + temp_script_path = os.path.join(os.path.dirname(__file__), "_temp_extract_mcp_tools.py") + with open(temp_script_path, "w") as f: + f.write(temp_script) + + # Run the temporary script to extract the tool definitions + cmd = ["python", temp_script_path, self.command] + self.args + result = subprocess.run(cmd, capture_output=True, text=True, timeout=self.timeout) + + # Remove the temporary script + os.remove(temp_script_path) + + if result.returncode != 0: + logger.error(f"Error extracting MCP tools: {result.stderr}") + raise RuntimeError(f"Failed to extract MCP tools: {result.stderr}") + + # Parse the function declarations from the script output + self._function_declarations = json.loads(result.stdout.strip()) + + # Create tool wrappers for each function declaration + for func_decl in self._function_declarations: + tool_name = func_decl["name"] + self._tools.append(self._create_tool_wrapper(tool_name, func_decl)) + + # Create a tool definition function in the global namespace + self._create_tool_definition_function(tool_name, func_decl) + + logger.info(f"Initialized MCP tools: {[t.__name__ for t in self._tools]}") + + except Exception as e: + logger.error(f"Failed to initialize MCP tools: {e}") + raise + + def _create_tool_wrapper(self, tool_name: str, func_decl: Dict[str, Any]) -> Callable: + """ + Create a wrapper function for an MCP tool. + + Args: + tool_name: The name of the tool + func_decl: The function declaration for the tool + + Returns: + A callable function that wraps the MCP tool + """ + def wrapper(**kwargs) -> Any: + # Create a temporary script to call the MCP tool + temp_script = """ +import asyncio +import json +import sys +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client + +async def call_tool(command, args, tool_name, kwargs): + # Create server parameters + server_params = StdioServerParameters( + command=command, + args=args + ) + + try: + async with stdio_client(server_params) as (read, write): + async with ClientSession(read, write) as session: + # Initialize connection + await session.initialize() + + # Call the tool + result = await session.call_tool(tool_name, kwargs) + + # Extract the result content + if result.isError: + error_msg = result.content[0].text if result.content else "Unknown error" + print(json.dumps({"error": error_msg})) + else: + print(json.dumps({"result": result.content[0].text if result.content else ""})) + except Exception as e: + print(json.dumps({"error": str(e)})) + +if __name__ == "__main__": + command = sys.argv[1] + args = sys.argv[2:-2] + tool_name = sys.argv[-2] + kwargs_json = sys.argv[-1] + kwargs = json.loads(kwargs_json) + asyncio.run(call_tool(command, args, tool_name, kwargs)) +""" + + try: + # Write the temporary script to a file + temp_script_path = os.path.join(os.path.dirname(__file__), f"_temp_call_mcp_tool_{tool_name}.py") + with open(temp_script_path, "w") as f: + f.write(temp_script) + + # Run the temporary script to call the MCP tool + cmd = ["python", temp_script_path, self.command] + self.args + [tool_name, json.dumps(kwargs)] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=self.timeout) + + # Remove the temporary script + os.remove(temp_script_path) + + if result.returncode != 0: + logger.error(f"Error calling MCP tool {tool_name}: {result.stderr}") + return {"error": f"Failed to call MCP tool: {result.stderr}"} + + # Parse the result from the script output + return json.loads(result.stdout.strip()) + + except Exception as e: + logger.error(f"Error calling MCP tool {tool_name}: {e}") + return {"error": str(e)} + + # Set the name and docstring for the wrapper function + wrapper.__name__ = tool_name + wrapper.__doc__ = func_decl.get("description", f"Call the {tool_name} tool on the MCP server") + + return wrapper + + def __iter__(self): + """Make the wrapper iterable to work with the agent system.""" + return iter(self._tools) + + def _create_tool_definition_function(self, tool_name, func_decl): + """ + Create a tool definition function in the global namespace. + This allows the Agent._generate_tool_definition method to find the tool definition. + + Args: + tool_name: The name of the tool + func_decl: The function declaration for the tool + """ + # Create the tool definition + tool_def = { + "type": "function", + "function": func_decl + } + + # Store the tool definition in the global namespace directly + # This way Agent._generate_tool_definition can find it + tool_def_name = f"{tool_name}_definition" + setattr(sys.modules["__main__"], tool_def_name, tool_def) + globals()[tool_def_name] = tool_def + + logger.debug(f"Created tool definition: {tool_def_name}") + + def to_openai_tool(self): + """Return the function declarations as OpenAI tools.""" + return [ + { + "type": "function", + "function": func_decl + } + for func_decl in self._function_declarations + ] diff --git a/src/praisonai-agents/praisonaiagents/mcp/__init__.py b/src/praisonai-agents/praisonaiagents/mcp/__init__.py new file mode 100644 index 000000000..c8b4f2090 --- /dev/null +++ b/src/praisonai-agents/praisonaiagents/mcp/__init__.py @@ -0,0 +1,6 @@ +""" +Model Context Protocol (MCP) integration for PraisonAI Agents. +""" +from .mcp import MCP + +__all__ = ["MCP"] diff --git a/src/praisonai-agents/praisonaiagents/mcp/mcp.py b/src/praisonai-agents/praisonaiagents/mcp/mcp.py index ce11d7dde..26f4a40cb 100644 --- a/src/praisonai-agents/praisonaiagents/mcp/mcp.py +++ b/src/praisonai-agents/praisonaiagents/mcp/mcp.py @@ -4,7 +4,9 @@ import time import inspect import shlex -from typing import Dict, Any, List, Optional, Callable, Iterable, Union +import logging +import os +from typing import Any, List, Optional, Callable, Iterable, Union from functools import wraps, partial from mcp import ClientSession, StdioServerParameters @@ -128,7 +130,7 @@ class MCP: ``` """ - def __init__(self, command_or_string=None, args=None, *, command=None, **kwargs): + def __init__(self, command_or_string=None, args=None, *, command=None, timeout=60, debug=False, **kwargs): """ Initialize the MCP connection and get tools. @@ -136,8 +138,11 @@ def __init__(self, command_or_string=None, args=None, *, command=None, **kwargs) command_or_string: Either: - The command to run the MCP server (e.g., Python path) - A complete command string (e.g., "/path/to/python /path/to/app.py") + - For NPX: 'npx' command with args for smithery tools args: Arguments to pass to the command (when command_or_string is the command) command: Alternative parameter name for backward compatibility + timeout: Timeout in seconds for MCP server initialization and tool calls (default: 60) + debug: Enable debug logging for MCP operations (default: False) **kwargs: Additional parameters for StdioServerParameters """ # Handle backward compatibility with named parameter 'command' @@ -168,9 +173,24 @@ def __init__(self, command_or_string=None, args=None, *, command=None, **kwargs) # Wait for initialization if not self.runner.initialized.wait(timeout=30): print("Warning: MCP initialization timed out") + + # Store additional parameters + self.timeout = timeout + self.debug = debug + + if debug: + logging.getLogger("mcp-wrapper").setLevel(logging.DEBUG) + + # Automatically detect if this is an NPX command + self.is_npx = cmd == 'npx' or (isinstance(cmd, str) and os.path.basename(cmd) == 'npx') - # Generate tool functions immediately and store them - self._tools = self._generate_tool_functions() + # For NPX-based MCP servers, use a different approach + if self.is_npx: + self._function_declarations = [] + self._initialize_npx_mcp_tools(cmd, arguments) + else: + # Generate tool functions immediately and store them + self._tools = self._generate_tool_functions() def _generate_tool_functions(self) -> List[Callable]: """ @@ -264,6 +284,27 @@ def wrapper(*args, **kwargs): return wrapper + def _initialize_npx_mcp_tools(self, cmd, arguments): + """Initialize the NPX MCP tools by extracting tool definitions.""" + try: + # For NPX tools, we'll use the same approach as regular MCP tools + # but we need to handle the initialization differently + if self.debug: + logging.debug(f"Initializing NPX MCP tools with command: {cmd} {' '.join(arguments)}") + + # Generate tool functions using the regular MCP approach + self._tools = self._generate_tool_functions() + + if self.debug: + logging.debug(f"Generated {len(self._tools)} NPX MCP tools") + + except Exception as e: + if self.debug: + logging.error(f"Failed to initialize NPX MCP tools: {e}") + raise RuntimeError(f"Failed to initialize NPX MCP tools: {e}") + + + def __iter__(self) -> Iterable[Callable]: """ Allow the MCP instance to be used directly as an iterable of tools. diff --git a/src/praisonai-agents/pyproject.toml b/src/praisonai-agents/pyproject.toml index 115a9f2ce..01b6c48e1 100644 --- a/src/praisonai-agents/pyproject.toml +++ b/src/praisonai-agents/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "praisonaiagents" -version = "0.0.66" +version = "0.0.67" description = "Praison AI agents for completing complex tasks with Self Reflection Agents" authors = [ { name="Mervin Praison" } diff --git a/src/praisonai-agents/uv.lock b/src/praisonai-agents/uv.lock index 83082627c..9aaa99d64 100644 --- a/src/praisonai-agents/uv.lock +++ b/src/praisonai-agents/uv.lock @@ -1868,7 +1868,7 @@ wheels = [ [[package]] name = "praisonaiagents" -version = "0.0.66" +version = "0.0.67" source = { editable = "." } dependencies = [ { name = "openai" }, From e4e164ae3f8e9fe132bdb17875c7cadde740632d Mon Sep 17 00:00:00 2001 From: MervinPraison Date: Sat, 29 Mar 2025 17:57:10 +0000 Subject: [PATCH 3/4] adding mcp --- pyproject.toml | 4 ++-- uv.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fd73e9911..684e4b4ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "rich>=13.7", "markdown>=3.5", "pyparsing>=3.0.0", - "praisonaiagents>=0.0.65", + "praisonaiagents>=0.0.67", "python-dotenv>=0.19.0", "instructor>=1.3.3", "PyYAML>=6.0", @@ -102,7 +102,7 @@ python = ">=3.10,<3.13" rich = ">=13.7" markdown = ">=3.5" pyparsing = ">=3.0.0" -praisonaiagents = ">=0.0.65" +praisonaiagents = ">=0.0.67" python-dotenv = ">=0.19.0" instructor = ">=1.3.3" PyYAML = ">=6.0" diff --git a/uv.lock b/uv.lock index 17867502a..c74005d96 100644 --- a/uv.lock +++ b/uv.lock @@ -3197,7 +3197,7 @@ requires-dist = [ { name = "plotly", marker = "extra == 'realtime'", specifier = ">=5.24.0" }, { name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.0.7" }, { name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.0.7" }, - { name = "praisonaiagents", specifier = ">=0.0.65" }, + { name = "praisonaiagents", specifier = ">=0.0.67" }, { name = "pyautogen", marker = "extra == 'autogen'", specifier = ">=0.2.19" }, { name = "pydantic", marker = "extra == 'chat'", specifier = "<=2.10.1" }, { name = "pydantic", marker = "extra == 'code'", specifier = "<=2.10.1" }, @@ -3250,16 +3250,16 @@ wheels = [ [[package]] name = "praisonaiagents" -version = "0.0.65" +version = "0.0.67" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "openai" }, { name = "pydantic" }, { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/58/6e07c4abf57d78be642f1517fe0692a003dfc253ad91a03c70521d246b5e/praisonaiagents-0.0.65.tar.gz", hash = "sha256:16627166f1b42bd8d21759fbd67015fff0b4730ca224b4a566cffe2b5bb770e8", size = 105128 } +sdist = { url = "https://files.pythonhosted.org/packages/2d/7f/9c866aaaefabc97238fce6b2bee54a37f598a948b04cd104eaa09675452d/praisonaiagents-0.0.67.tar.gz", hash = "sha256:85bcde9a7535ec0f1f35a7496f1368c09f8672ac15bd28d1ffb86fb5ebfc64a4", size = 108467 } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/02/e4f2ef3c008a407d3b945256d1513bf0159989055e9462c691265be3d294/praisonaiagents-0.0.65-py3-none-any.whl", hash = "sha256:f53e9c3ddf31c575dd5f65315db70b35487adb0f9ae908596e74ed315c12afc5", size = 124306 }, + { url = "https://files.pythonhosted.org/packages/c5/a0/75b3d4a7c2f2688cd99d61def26fead52e0e11f803092fe777bcf1aa00d1/praisonaiagents-0.0.67-py3-none-any.whl", hash = "sha256:c801f43f82d42d7da667e2d5555d7f92d6dfd02aaa331c555492114288da713f", size = 128195 }, ] [[package]] From 5fc8c0b27a07fb5add126098c258927ef96e4cca Mon Sep 17 00:00:00 2001 From: MervinPraison Date: Sat, 29 Mar 2025 17:57:31 +0000 Subject: [PATCH 4/4] Increment version number in pyproject.toml to 2.0.81 --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 684e4b4ec..b12820ddc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "PraisonAI" -version = "2.0.80" +version = "2.0.81" description = "PraisonAI is an AI Agents Framework with Self Reflection. PraisonAI application combines PraisonAI Agents, AutoGen, and CrewAI into a low-code solution for building and managing multi-agent LLM systems, focusing on simplicity, customisation, and efficient human-agent collaboration." readme = "README.md" license = "" diff --git a/uv.lock b/uv.lock index c74005d96..b7f2ba7b9 100644 --- a/uv.lock +++ b/uv.lock @@ -3060,7 +3060,7 @@ wheels = [ [[package]] name = "praisonai" -version = "2.0.80" +version = "2.0.81" source = { editable = "." } dependencies = [ { name = "instructor" },