From 442bb4a3405a94d859750a4ce4b7187b426e869a Mon Sep 17 00:00:00 2001 From: martin legrand Date: Sun, 4 May 2025 18:34:05 +0200 Subject: [PATCH] Feat : MCP agent --- cli.py | 7 +++- prompts/base/mcp_agent.txt | 66 ++++++++++++++++++++++++++++++ prompts/jarvis/mcp_agent.txt | 62 ++++++++++++++++++++++++++++ sources/agents/__init__.py | 3 +- sources/agents/agent.py | 8 ++++ sources/agents/code_agent.py | 4 +- sources/agents/mcp_agent.py | 70 ++++++++++++++++++++++++++++++++ sources/router.py | 9 ++++ sources/tools/BashInterpreter.py | 2 + sources/tools/C_Interpreter.py | 2 + sources/tools/GoInterpreter.py | 2 + sources/tools/JavaInterpreter.py | 2 + sources/tools/PyInterpreter.py | 2 + sources/tools/fileFinder.py | 2 + sources/tools/flightSearch.py | 4 +- sources/tools/mcpFinder.py | 34 ++++++++-------- sources/tools/searxSearch.py | 2 + sources/tools/tools.py | 2 + 18 files changed, 261 insertions(+), 22 deletions(-) create mode 100644 prompts/base/mcp_agent.txt create mode 100644 prompts/jarvis/mcp_agent.txt create mode 100644 sources/agents/mcp_agent.py diff --git a/cli.py b/cli.py index 303c5f1..b0f14c2 100755 --- a/cli.py +++ b/cli.py @@ -7,7 +7,7 @@ import asyncio from sources.llm_provider import Provider from sources.interaction import Interaction -from sources.agents import Agent, CoderAgent, CasualAgent, FileAgent, PlannerAgent, BrowserAgent +from sources.agents import Agent, CoderAgent, CasualAgent, FileAgent, PlannerAgent, BrowserAgent, McpAgent from sources.browser import Browser, create_driver from sources.utility import pretty_print @@ -48,7 +48,10 @@ async def main(): provider=provider, verbose=False, browser=browser), PlannerAgent(name="Planner", prompt_path=f"prompts/{personality_folder}/planner_agent.txt", - provider=provider, verbose=False, browser=browser) + provider=provider, verbose=False, browser=browser), + McpAgent(name="MCP Agent", + prompt_path=f"prompts/{personality_folder}/mcp_agent.txt", + provider=provider, verbose=False), ] interaction = Interaction(agents, diff --git a/prompts/base/mcp_agent.txt b/prompts/base/mcp_agent.txt new file mode 100644 index 0000000..534b2eb --- /dev/null +++ b/prompts/base/mcp_agent.txt @@ -0,0 +1,66 @@ + +You are an agent designed to utilize the MCP protocol to accomplish tasks. + +The MCP provide you with a standard way to use tools and data sources like databases, APIs, or apps (e.g., GitHub, Slack). + +The are thousands of MCPs protocol that can accomplish a variety of tasks, for example: +- get weather information +- get stock data information +- Use software like blender +- Get messages from teams, stack, messenger +- Read and send email + +Anything is possible with MCP. + +To search for MCP a special format: + +- Example 1: + +User: what's the stock market of IBM like today?: + +You: I will search for mcp to find information about IBM stock market. + +```mcp_finder +stock +``` + +You search query must be one or two words at most. + +This will provide you with informations about a specific MCP such as the json of parameters needed to use it. + +For example, you might see: +------- +Name: Search Stock News +Usage name: @Cognitive-Stack/search-stock-news-mcp +Tools: [{'name': 'search-stock-news', 'description': 'Search for stock-related news using Tavily API', 'inputSchema': {'type': 'object', '$schema': 'http://json-schema.org/draft-07/schema#', 'required': ['symbol', 'companyName'], 'properties': {'symbol': {'type': 'string', 'description': "Stock symbol to search for (e.g., 'AAPL')"}, 'companyName': {'type': 'string', 'description': 'Full company name to include in the search'}}, 'additionalProperties': False}}] +------- + +You can then a MCP like so: + +``` +{ + "tool": "", + "inputSchema": {} +} +``` + +For example: + +Now that I know how to use the MCP, I will choose the search-stock-news tool and execute it to find out IBM stock market. + +```Cognitive-Stack/search-stock-news-mcp +{ + "tool": "search-stock-news", + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["symbol"], + "properties": { + "symbol": "IBM" + } + } +} +``` + +If the schema require an information that you don't have ask the users for the information. + diff --git a/prompts/jarvis/mcp_agent.txt b/prompts/jarvis/mcp_agent.txt new file mode 100644 index 0000000..143e521 --- /dev/null +++ b/prompts/jarvis/mcp_agent.txt @@ -0,0 +1,62 @@ + +You are an agent designed to utilize the MCP protocol to accomplish tasks. + +The MCP provide you with a standard way to use tools and data sources like databases, APIs, or apps (e.g., GitHub, Slack). + +The are thousands of MCPs protocol that can accomplish a variety of tasks, for example: +- get weather information +- get stock data information +- Use software like blender +- Get messages from teams, stack, messenger +- Read and send email + +Anything is possible with MCP. + +To search for MCP a special format: + +- Example 1: + +User: what's the stock market of IBM like today?: + +You: I will search for mcp to find information about IBM stock market. + +```mcp_finder +stock +``` + +This will provide you with informations about a specific MCP such as the json of parameters needed to use it. + +For example, you might see: +------- +Name: Search Stock News +Usage name: @Cognitive-Stack/search-stock-news-mcp +Tools: [{'name': 'search-stock-news', 'description': 'Search for stock-related news using Tavily API', 'inputSchema': {'type': 'object', '$schema': 'http://json-schema.org/draft-07/schema#', 'required': ['symbol', 'companyName'], 'properties': {'symbol': {'type': 'string', 'description': "Stock symbol to search for (e.g., 'AAPL')"}, 'companyName': {'type': 'string', 'description': 'Full company name to include in the search'}}, 'additionalProperties': False}}] +------- + +You can then a MCP like so: + +``` +{ + "tool": "", + "inputSchema": {} +} +``` + +For example: + +Now that I know how to use the MCP, I will choose the search-stock-news tool and execute it to find out IBM stock market. + +```Cognitive-Stack/search-stock-news-mcp +{ + "tool": "search-stock-news", + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["symbol"], + "properties": { + "symbol": "IBM" + } + } +} +``` + diff --git a/sources/agents/__init__.py b/sources/agents/__init__.py index 8a11dc0..69718cb 100644 --- a/sources/agents/__init__.py +++ b/sources/agents/__init__.py @@ -5,5 +5,6 @@ from .casual_agent import CasualAgent from .file_agent import FileAgent from .planner_agent import PlannerAgent from .browser_agent import BrowserAgent +from .mcp_agent import McpAgent -__all__ = ["Agent", "CoderAgent", "CasualAgent", "FileAgent", "PlannerAgent", "BrowserAgent"] +__all__ = ["Agent", "CoderAgent", "CasualAgent", "FileAgent", "PlannerAgent", "BrowserAgent", "McpAgent"] diff --git a/sources/agents/agent.py b/sources/agents/agent.py index f3efb41..14c6c77 100644 --- a/sources/agents/agent.py +++ b/sources/agents/agent.py @@ -90,6 +90,12 @@ class Agent(): raise TypeError("Tool must be a callable object (a method)") self.tools[name] = tool + def get_tools_name(self) -> list: + """ + Get the list of tools names. + """ + return list(self.tools.keys()) + def load_prompt(self, file_path: str) -> str: try: with open(file_path, 'r', encoding="utf-8") as f: @@ -235,11 +241,13 @@ class Agent(): answer = "I will execute:\n" + answer # there should always be a text before blocks for the function that display answer self.success = True + self.blocks_result = [] for name, tool in self.tools.items(): feedback = "" blocks, save_path = tool.load_exec_block(answer) if blocks != None: + pretty_print(f"Executing {len(blocks)} {name} blocks...", color="status") for block in blocks: self.show_block(block) output = tool.execute([block]) diff --git a/sources/agents/code_agent.py b/sources/agents/code_agent.py index 10c7854..e291057 100644 --- a/sources/agents/code_agent.py +++ b/sources/agents/code_agent.py @@ -62,14 +62,14 @@ class CoderAgent(Agent): animate_thinking("Executing code...", color="status") self.status_message = "Executing code..." self.logger.info(f"Attempt {attempt + 1}:\n{answer}") - exec_success, _ = self.execute_modules(answer) + exec_success, feedback = self.execute_modules(answer) self.logger.info(f"Execution result: {exec_success}") answer = self.remove_blocks(answer) self.last_answer = answer await asyncio.sleep(0) if exec_success and self.get_last_tool_type() != "bash": break - pretty_print("Execution failure", color="failure") + pretty_print(f"Execution failure:\n{feedback}", color="failure") pretty_print("Correcting code...", color="status") self.status_message = "Correcting code..." attempt += 1 diff --git a/sources/agents/mcp_agent.py b/sources/agents/mcp_agent.py new file mode 100644 index 0000000..e24b04d --- /dev/null +++ b/sources/agents/mcp_agent.py @@ -0,0 +1,70 @@ +import os +import asyncio + +from sources.utility import pretty_print, animate_thinking +from sources.agents.agent import Agent +from sources.tools.mcpFinder import MCP_finder + +# NOTE MCP agent is an active work in progress, not functional yet. + +class McpAgent(Agent): + + def __init__(self, name, prompt_path, provider, verbose=False): + """ + The mcp agent is a special agent for using MCPs. + MCP agent will be disabled if the user does not explicitly set the MCP_FINDER_API_KEY in environment variable. + """ + super().__init__(name, prompt_path, provider, verbose, None) + keys = self.get_api_keys() + self.tools = { + "mcp_finder": MCP_finder(keys["mcp_finder"]), + # add mcp tools here + } + self.role = "mcp" + self.type = "mcp_agent" + self.enabled = True + + def get_api_keys(self) -> dict: + """ + Returns the API keys for the tools. + """ + api_key_mcp_finder = os.getenv("MCP_FINDER_API_KEY") + if not api_key_mcp_finder or api_key_mcp_finder == "": + pretty_print("MCP Finder API key not found. Please set the MCP_FINDER_API_KEY environment variable.", color="failure") + pretty_print("MCP Finder disabled.", color="failure") + self.enabled = False + return { + "mcp_finder": api_key_mcp_finder + } + + def expand_prompt(self, prompt): + """ + Expands the prompt with the tools available. + """ + tools_name = self.get_tools_name() + tools_str = ", ".join(tools_name) + prompt += f""" + You can use the following tools and MCPs: + {tools_str} + """ + return prompt + + async def process(self, prompt, speech_module) -> str: + if self.enabled == False: + return "MCP Agent is disabled." + prompt = self.expand_prompt(prompt) + self.memory.push('user', prompt) + working = True + while working == True: + animate_thinking("Thinking...", color="status") + answer, reasoning = await self.llm_request() + exec_success, _ = self.execute_modules(answer) + answer = self.remove_blocks(answer) + self.last_answer = answer + self.status_message = "Ready" + if len(self.blocks_result) == 0: + working = False + return answer, reasoning + +if __name__ == "__main__": + pass \ No newline at end of file diff --git a/sources/router.py b/sources/router.py index cd53f5b..50e8178 100644 --- a/sources/router.py +++ b/sources/router.py @@ -141,6 +141,9 @@ class AgentRouter: ("Search the web for tips on improving coding skills", "LOW"), ("Write a Python script to count words in a text file", "LOW"), ("Search the web for restaurant", "LOW"), + ("Use a MCP to find the latest stock market data", "LOW"), + ("Use a MCP to send an email to my boss", "LOW"), + ("Could you use a MCP to find the latest news on climate change?", "LOW"), ("Create a simple HTML page with CSS styling", "LOW"), ("Use file.txt and then use it to ...", "HIGH"), ("Yo, what’s good? Find my ‘mixtape.mp3’ real quick", "LOW"), @@ -162,6 +165,7 @@ class AgentRouter: ("Find a public API for book data and create a Flask app to list bestsellers", "HIGH"), ("Organize my desktop files by extension and then write a script to list them", "HIGH"), ("Find the latest research on renewable energy and build a web app to display it", "HIGH"), + ("search online for popular sci-fi movies from 2024 and pick three to watch tonight. Save the list in movie_night.txt", "HIGH"), ("can you find vitess repo, clone it and install by following the readme", "HIGH"), ("Create a JavaScript game using Phaser.js with multiple levels", "HIGH"), ("Search the web for the latest trends in web development and build a sample site", "HIGH"), @@ -330,6 +334,11 @@ class AgentRouter: ("can you make a web app in python that use the flask framework", "code"), ("can you build a web server in go that serve a simple html page", "code"), ("can you find out who Jacky yougouri is ?", "web"), + ("Can you use MCP to find stock market for IBM ?", "mcp"), + ("Can you use MCP to to export my contacts to a csv file?", "mcp"), + ("Can you use a MCP to find write notes to flomo", "mcp"), + ("Can you use a MCP to query my calendar and find the next meeting?", "mcp"), + ("Can you use a mcp to get the distance between Shanghai and Paris?", "mcp"), ("Setup a new flutter project called 'new_flutter_project'", "files"), ("can you create a new project called 'new_project'", "files"), ("can you make a simple web app that display a list of files in my dir", "code"), diff --git a/sources/tools/BashInterpreter.py b/sources/tools/BashInterpreter.py index e6ae113..9d56584 100644 --- a/sources/tools/BashInterpreter.py +++ b/sources/tools/BashInterpreter.py @@ -17,6 +17,8 @@ class BashInterpreter(Tools): def __init__(self): super().__init__() self.tag = "bash" + self.name = "Bash Interpreter" + self.description = "This tool allows the agent to execute bash commands." def language_bash_attempt(self, command: str): """ diff --git a/sources/tools/C_Interpreter.py b/sources/tools/C_Interpreter.py index 2c0cbf9..06084ff 100644 --- a/sources/tools/C_Interpreter.py +++ b/sources/tools/C_Interpreter.py @@ -15,6 +15,8 @@ class CInterpreter(Tools): def __init__(self): super().__init__() self.tag = "c" + self.name = "C Interpreter" + self.description = "This tool allows the agent to execute C code." def execute(self, codes: str, safety=False) -> str: """ diff --git a/sources/tools/GoInterpreter.py b/sources/tools/GoInterpreter.py index ab23f76..68ff282 100644 --- a/sources/tools/GoInterpreter.py +++ b/sources/tools/GoInterpreter.py @@ -15,6 +15,8 @@ class GoInterpreter(Tools): def __init__(self): super().__init__() self.tag = "go" + self.name = "Go Interpreter" + self.description = "This tool allows you to execute Go code." def execute(self, codes: str, safety=False) -> str: """ diff --git a/sources/tools/JavaInterpreter.py b/sources/tools/JavaInterpreter.py index 4658c13..2965c31 100644 --- a/sources/tools/JavaInterpreter.py +++ b/sources/tools/JavaInterpreter.py @@ -15,6 +15,8 @@ class JavaInterpreter(Tools): def __init__(self): super().__init__() self.tag = "java" + self.name = "Java Interpreter" + self.description = "This tool allows you to execute Java code." def execute(self, codes: str, safety=False) -> str: """ diff --git a/sources/tools/PyInterpreter.py b/sources/tools/PyInterpreter.py index 2b4f965..6d39a6b 100644 --- a/sources/tools/PyInterpreter.py +++ b/sources/tools/PyInterpreter.py @@ -16,6 +16,8 @@ class PyInterpreter(Tools): def __init__(self): super().__init__() self.tag = "python" + self.name = "Python Interpreter" + self.description = "This tool allows the agent to execute python code." def execute(self, codes:str, safety = False) -> str: """ diff --git a/sources/tools/fileFinder.py b/sources/tools/fileFinder.py index 2b30667..92518b2 100644 --- a/sources/tools/fileFinder.py +++ b/sources/tools/fileFinder.py @@ -15,6 +15,8 @@ class FileFinder(Tools): def __init__(self): super().__init__() self.tag = "file_finder" + self.name = "File Finder" + self.description = "Finds files in the current directory and returns their information." def read_file(self, file_path: str) -> str: """ diff --git a/sources/tools/flightSearch.py b/sources/tools/flightSearch.py index 3c6f759..87b2e0b 100644 --- a/sources/tools/flightSearch.py +++ b/sources/tools/flightSearch.py @@ -16,6 +16,8 @@ class FlightSearch(Tools): """ super().__init__() self.tag = "flight_search" + self.name = "Flight Search" + self.description = "Search for flight information using a flight number via AviationStack API." self.api_key = None self.api_key = api_key or os.getenv("AVIATIONSTACK_API_KEY") @@ -24,7 +26,7 @@ class FlightSearch(Tools): return "Error: No AviationStack API key provided." for block in blocks: - flight_number = block.strip() + flight_number = block.strip().lower().replace('\n', '') if not flight_number: return "Error: No flight number provided." diff --git a/sources/tools/mcpFinder.py b/sources/tools/mcpFinder.py index 96e07f5..cbca5dc 100644 --- a/sources/tools/mcpFinder.py +++ b/sources/tools/mcpFinder.py @@ -3,11 +3,10 @@ import requests from urllib.parse import urljoin from typing import Dict, Any, Optional -from sources.tools.tools import Tools - if __name__ == "__main__": # if running as a script for individual testing sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +from sources.tools.tools import Tools class MCP_finder(Tools): """ @@ -15,7 +14,9 @@ class MCP_finder(Tools): """ def __init__(self, api_key: str = None): super().__init__() - self.tag = "mcp" + self.tag = "mcp_finder" + self.name = "MCP Finder" + self.description = "Find MCP servers and their tools" self.base_url = "https://registry.smithery.ai" self.headers = { "Authorization": f"Bearer {api_key}", @@ -61,11 +62,7 @@ class MCP_finder(Tools): for mcp in mcps.get("servers", []): name = mcp.get("qualifiedName", "") if query.lower() in name.lower(): - details = { - "name": name, - "description": mcp.get("description", "No description available"), - "params": mcp.get("connections", []) - } + details = self.get_mcp_server_details(name) matching_mcp.append(details) return matching_mcp @@ -79,7 +76,7 @@ class MCP_finder(Tools): try: matching_mcp_infos = self.find_mcp_servers(block_clean) except requests.exceptions.RequestException as e: - output += "Connection failed. Is the API in environement?\n" + output += "Connection failed. Is the API key in environement?\n" continue except Exception as e: output += f"Error: {str(e)}\n" @@ -88,10 +85,12 @@ class MCP_finder(Tools): output += f"Error: No MCP server found for query '{block}'\n" continue for mcp_infos in matching_mcp_infos: - output += f"Name: {mcp_infos['name']}\n" - output += f"Description: {mcp_infos['description']}\n" - output += f"Params: {', '.join(mcp_infos['params'])}\n" - output += "-------\n" + if mcp_infos['tools'] is None: + continue + output += f"Name: {mcp_infos['displayName']}\n" + output += f"Usage name: {mcp_infos['qualifiedName']}\n" + output += f"Tools: {mcp_infos['tools']}" + output += "\n-------\n" return output.strip() def execution_failure_check(self, output: str) -> bool: @@ -107,13 +106,16 @@ class MCP_finder(Tools): Not really needed for this tool (use return of execute() directly) """ if not output: - return "No output generated." - return output.strip() + raise ValueError("No output to interpret.") + return f""" + The following MCPs were found: + {output} + """ if __name__ == "__main__": api_key = os.getenv("MCP_FINDER") tool = MCP_finder(api_key) result = tool.execute([""" -news +stock """], False) print(result) \ No newline at end of file diff --git a/sources/tools/searxSearch.py b/sources/tools/searxSearch.py index 15a4438..4d8fc4f 100644 --- a/sources/tools/searxSearch.py +++ b/sources/tools/searxSearch.py @@ -14,6 +14,8 @@ class searxSearch(Tools): """ super().__init__() self.tag = "web_search" + self.name = "searxSearch" + self.description = "A tool for searching a SearxNG for web search" self.base_url = base_url or os.getenv("SEARXNG_BASE_URL") # Requires a SearxNG base URL self.user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36" self.paywall_keywords = [ diff --git a/sources/tools/tools.py b/sources/tools/tools.py index 80bf25a..555f30c 100644 --- a/sources/tools/tools.py +++ b/sources/tools/tools.py @@ -33,6 +33,8 @@ class Tools(): """ def __init__(self): self.tag = "undefined" + self.name = "undefined" + self.description = "undefined" self.client = None self.messages = [] self.logger = Logger("tools.log")