diff --git a/docs/examples/multi_mcp_execution.md b/docs/examples/multi_mcp_execution.md index a49d2b98..31997a09 100644 --- a/docs/examples/multi_mcp_execution.md +++ b/docs/examples/multi_mcp_execution.md @@ -2,8 +2,16 @@ This example demonstrates using a list of MCP servers with an `Agent`. +Start the example servers in separate terminals: + +```bash +python examples/tools/mcp_examples/servers/weather_server.py +python examples/tools/mcp_examples/servers/news_server.py +``` + ```python import os +import json from swarms import Agent # Configure multiple MCP URLs @@ -15,11 +23,12 @@ agent = Agent( max_loops=1, ) -# Example payloads produced by your model -payloads = [ +# Example JSON payloads produced by your model +response = json.dumps([ {"function_name": "get_weather", "server_url": "http://localhost:8000/sse", "payload": {"city": "London"}}, {"function_name": "get_news", "server_url": "http://localhost:9001/sse", "payload": {"topic": "ai"}}, -] +]) + +agent.handle_multiple_mcp_tools(agent.mcp_urls, response) -agent.handle_multiple_mcp_tools(agent.mcp_urls, payloads) ``` diff --git a/docs/swarms/structs/agent.md b/docs/swarms/structs/agent.md index 9178aac8..b024d4ad 100644 --- a/docs/swarms/structs/agent.md +++ b/docs/swarms/structs/agent.md @@ -483,8 +483,16 @@ print(agent.to_toml()) Execute tools from multiple MCP servers by providing a list of URLs via the `mcp_urls` parameter or the `MCP_URLS` environment variable. +Start the example servers: + +```bash +python examples/tools/mcp_examples/servers/weather_server.py +python examples/tools/mcp_examples/servers/news_server.py +``` + ```python import os +import json from swarms import Agent # Using an environment variable for server configuration @@ -497,7 +505,8 @@ agent = Agent( ) # Example MCP payloads returned by your model -mcp_payloads = [ +mcp_response = json.dumps([ + { "function_name": "get_price", "server_url": "http://localhost:8000/sse", @@ -508,9 +517,10 @@ mcp_payloads = [ "server_url": "http://localhost:9001/sse", "payload": {"symbol": "BTC"}, }, -] +]) + +agent.handle_multiple_mcp_tools(agent.mcp_urls, mcp_response) -agent.handle_multiple_mcp_tools(agent.mcp_urls, mcp_payloads) ``` ## Auto Generate Prompt + CPU Execution diff --git a/docs/swarms/structs/agent_mcp.md b/docs/swarms/structs/agent_mcp.md index 63465932..e6c691b8 100644 --- a/docs/swarms/structs/agent_mcp.md +++ b/docs/swarms/structs/agent_mcp.md @@ -145,6 +145,14 @@ payload in the following format: Use `handle_multiple_mcp_tools` to execute each payload across the configured servers. +Example servers can be started with: + +```bash +python examples/tools/mcp_examples/servers/weather_server.py +python examples/tools/mcp_examples/servers/news_server.py +``` + + --- ## Integration Flow diff --git a/examples/tools/mcp_examples/servers/news_server.py b/examples/tools/mcp_examples/servers/news_server.py new file mode 100644 index 00000000..584f25cc --- /dev/null +++ b/examples/tools/mcp_examples/servers/news_server.py @@ -0,0 +1,12 @@ +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP("NewsServer") + +mcp.settings.port = 9001 + +@mcp.tool(name="get_news", description="Return simple news headline") +def get_news(topic: str) -> str: + return f"Latest {topic} news headline" + +if __name__ == "__main__": + mcp.run(transport="sse") diff --git a/examples/tools/mcp_examples/servers/weather_server.py b/examples/tools/mcp_examples/servers/weather_server.py new file mode 100644 index 00000000..93825785 --- /dev/null +++ b/examples/tools/mcp_examples/servers/weather_server.py @@ -0,0 +1,12 @@ +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP("WeatherServer") + +mcp.settings.port = 8000 + +@mcp.tool(name="get_weather", description="Return simple weather info") +def get_weather(city: str) -> str: + return f"Weather in {city}: Sunny 22°C" + +if __name__ == "__main__": + mcp.run(transport="sse") diff --git a/examples/tools/mcp_examples/tests/test_multi_mcp_demo.py b/examples/tools/mcp_examples/tests/test_multi_mcp_demo.py new file mode 100644 index 00000000..c6a66a72 --- /dev/null +++ b/examples/tools/mcp_examples/tests/test_multi_mcp_demo.py @@ -0,0 +1,73 @@ +import os +import json +from swarms import Agent +import io +import sys +from contextlib import redirect_stdout + +print("\n=== Testing Multiple MCP Tool Execution ===\n") + +# Configure multiple MCP URLs +os.environ["MCP_URLS"] = "http://localhost:8000/sse,http://localhost:9001/sse" + +def capture_output(func): + """Capture printed output from a function""" + f = io.StringIO() + with redirect_stdout(f): + func() + return f.getvalue() + +def test_direct_tool_execution(): + """Test directly executing tools on different MCP servers""" + print("Testing direct tool execution...\n") + + agent = Agent( + agent_name="Multi-MCP-Agent", + model_name="gpt-4o-mini", + max_loops=1 + ) + + # Create JSON payloads for multiple tools + payloads = [ + { + "function_name": "get_weather", + "server_url": "http://localhost:8000/sse", + "payload": {"city": "Paris"} + }, + { + "function_name": "get_news", + "server_url": "http://localhost:9001/sse", + "payload": {"topic": "science"} + } + ] + + # Execute the tools and capture output + print("Executing tools on multiple MCP servers...") + output = capture_output( + lambda: agent.handle_multiple_mcp_tools(agent.mcp_urls, json.dumps(payloads)) + ) + + # Extract and display results + print("\nResults from MCP tools:") + print(output) + + print("\nTest complete - Multiple MCP execution successful!") + +def test_agent_configuration(): + """Test different ways to configure agents with multiple MCP URLs""" + print("\n=== Testing Agent MCP Configuration Methods ===\n") + + # Method 1: Configure via environment variables (already set above) + agent1 = Agent(agent_name="Env-Config-Agent") + print(f"Agent1 MCP URLs (from env): {agent1.mcp_urls}") + + # Method 2: Configure via direct parameter + agent2 = Agent( + agent_name="Direct-Config-Agent", + mcp_urls=["http://localhost:8000/sse", "http://localhost:9001/sse"] + ) + print(f"Agent2 MCP URLs (from param): {agent2.mcp_urls}") + +if __name__ == "__main__": + test_agent_configuration() + test_direct_tool_execution() diff --git a/swarms/structs/agent.py b/swarms/structs/agent.py index 56c23533..b0dbc90e 100644 --- a/swarms/structs/agent.py +++ b/swarms/structs/agent.py @@ -1,5 +1,6 @@ import asyncio import json +import re import logging import os import random @@ -74,6 +75,7 @@ from swarms.structs.ma_utils import set_random_models_for_agents from swarms.tools.mcp_client_call import ( execute_tool_call_simple, get_mcp_tools_sync, + execute_mcp_call, ) from swarms.schemas.mcp_schemas import ( MCPConnection, @@ -98,6 +100,25 @@ def parse_done_token(response: str) -> bool: return "" in response +def extract_json_from_response(response: str) -> List[Dict[str, Any]]: + """Extract a JSON list from a model response string.""" + if not response: + return [] + if not isinstance(response, str): + return response if isinstance(response, list) else [] + try: + return json.loads(response) + except json.JSONDecodeError: + match = re.search(r"\[[\s\S]*?\]", response) + if match: + try: + return json.loads(match.group(0)) + except json.JSONDecodeError: + pass + logger.error("Failed to parse MCP payloads from response") + return [] + + # Agent ID generator def agent_id(): """Generate an agent id""" @@ -442,6 +463,8 @@ class Agent: self.sop = sop self.sop_list = sop_list self.tools = tools + # Ensure tool_struct exists even when no tools are provided + self.tool_struct = {} self.system_prompt = system_prompt self.agent_name = agent_name self.agent_description = agent_description @@ -1098,18 +1121,12 @@ class Agent: if exists(self.mcp_urls): try: - payload = ( - json.loads(response) - if isinstance(response, str) - else response + self.handle_multiple_mcp_tools( + self.mcp_urls, + response, + current_loop=loop_count, ) - if isinstance(payload, list): - self.handle_multiple_mcp_tools( - self.mcp_urls, - payload, - current_loop=loop_count, - ) except Exception as e: logger.error( f"Error handling multiple MCP tools: {e}" @@ -2820,12 +2837,14 @@ class Agent: def handle_multiple_mcp_tools( self, mcp_url_list: List[str], - mcp_payloads: List[Dict[str, Any]], + response: Union[str, List[Dict[str, Any]]], current_loop: int = 0, ) -> None: - """Execute a list of MCP tool calls across multiple servers.""" + """Execute multiple MCP tool calls across configured servers.""" + + payloads = extract_json_from_response(response) + for payload in payloads: - for payload in mcp_payloads: function_name = payload.get("function_name") server_url = payload.get("server_url") arguments = payload.get("payload", {}) @@ -2836,25 +2855,31 @@ class Agent: ) continue - try: - tool_response = asyncio.run( - execute_mcp_call( - function_name=function_name, - server_url=server_url, - payload=arguments, + attempt = 0 + while attempt < 3: + try: + tool_response = asyncio.run( + execute_mcp_call( + function_name=function_name, + server_url=server_url, + payload=arguments, + ) ) - ) - self.short_memory.add( - role="Tool Executor", - content=str(tool_response), - ) - self.pretty_print( - str(tool_response), loop_count=current_loop - ) - except Exception as e: # noqa: PERF203 - logger.error( - f"Error executing {function_name} on {server_url}: {e}" - ) + self.short_memory.add( + role="Tool Executor", + content=str(tool_response), + ) + self.pretty_print( + str(tool_response), + loop_count=current_loop, + ) + break + except Exception as e: # noqa: PERF203 + attempt += 1 + logger.error( + f"Error executing {function_name} on {server_url}: {e}" + ) + time.sleep(1) def temp_llm_instance_for_tool_summary(self): return LiteLLM( diff --git a/tests/structs/test_multi_mcp.py b/tests/structs/test_multi_mcp.py index b6e29a41..df9272be 100644 --- a/tests/structs/test_multi_mcp.py +++ b/tests/structs/test_multi_mcp.py @@ -1,5 +1,7 @@ import asyncio -from swarms.structs.agent import Agent +import json +from swarms.structs.agent import Agent, extract_json_from_response + from swarms.structs.agent import execute_mcp_call from unittest.mock import patch @@ -30,9 +32,19 @@ def test_handle_multiple_mcp_tools(): with patch( "swarms.structs.agent.execute_mcp_call", side_effect=fake_exec ): - agent.handle_multiple_mcp_tools(urls, payloads) + agent.handle_multiple_mcp_tools(urls, json.dumps(payloads)) assert called == [ ("tool1", "http://server1", {"a": 1}), ("tool2", "http://server2", {}), ] + + +def test_extract_json_from_response(): + payloads = [ + {"function_name": "foo", "server_url": "http://x", "payload": {"x": 1}} + ] + text = "Random text" + json.dumps(payloads) + " end" + result = extract_json_from_response(text) + assert result == payloads +