├── .gitignore ├── requirements.txt ├── Image └── AgenticRAGMCPServer.gif ├── .env.sample ├── config.py ├── README.md ├── server.py └── mcp-client.py /.gitignore: -------------------------------------------------------------------------------- 1 | .venv/ 2 | .env 3 | __pycache__/ -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mcp[cli]==1.6.0 2 | openai==1.75.0 3 | python-dotenv 4 | ipykernel 5 | httpx 6 | -------------------------------------------------------------------------------- /Image/AgenticRAGMCPServer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashishpatel26/Agentic-RAG-with-MCP-Server/main/Image/AgenticRAGMCPServer.gif -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | # OpenAI API Key 2 | OPENAI_API_KEY= your api key here 3 | 4 | # MCP Server URL 5 | MCP_SERVER_URL=http://0.0.0.0:8050 6 | 7 | # OpenAI Model Name 8 | OPENAI_MODEL_NAME=gpt-4o-mini 9 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | def load_config(): 5 | """Load configuration from environment variables""" 6 | # Make sure environment variables are loaded 7 | load_dotenv() 8 | 9 | # MongoDB Configuration 10 | mongodb_username = os.getenv('MONGODB_USERNAME') 11 | mongodb_password = os.getenv('MONGODB_PASSWORD') 12 | mongodb_uri = os.getenv('MONGODB_URI') or f"mongodb+srv://{mongodb_username}:{mongodb_password}@ircw.371s5.mongodb.net/?retryWrites=true&w=majority&appName=IRCW" 13 | 14 | # Default MongoDB Collections Configuration 15 | default_mongodb_collections = [ 16 | {"db": "newsfirst_data", "collection": "articles"}, 17 | {"db": "newswire_data", "collection": "articles"}, 18 | {"db": "adaderana_data", "collection": "articles"} 19 | ] 20 | 21 | # Pinecone Configuration 22 | pinecone_api_key = os.getenv('PINECONE_API_KEY') 23 | pinecone_index_name = os.getenv('PINECONE_INDEX_NAME', 'news-articles-index') 24 | pinecone_cloud = os.getenv('PINECONE_CLOUD', 'aws') 25 | pinecone_region = os.getenv('PINECONE_REGION', 'us-east-1') 26 | 27 | # OpenAI Configuration 28 | openai_api_key = os.getenv('OPENAI_API_KEY') 29 | 30 | # Return configuration as a dictionary 31 | return { 32 | "MONGODB_USERNAME": mongodb_username, 33 | "MONGODB_PASSWORD": mongodb_password, 34 | "MONGODB_URI": mongodb_uri, 35 | "DEFAULT_MONGODB_COLLECTIONS": default_mongodb_collections, 36 | "PINECONE_API_KEY": pinecone_api_key, 37 | "PINECONE_INDEX_NAME": pinecone_index_name, 38 | "PINECONE_CLOUD": pinecone_cloud, 39 | "PINECONE_REGION": pinecone_region, 40 | "OPENAI_API_KEY": openai_api_key 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Agentic RAG with MCP Server [![Agentic-RAG-MCPServer - AgenticRag](https://img.shields.io/badge/Agentic--RAG--MCPServer-AgenticRag-blueviolet)](https://github.com/ashishpatel26/Agentic-RAG-with-MCP-Server) 2 | 3 | --- 4 | 5 | ## ✨ Overview 6 | 7 | ![](Image/AgenticRAGMCPServer.gif) 8 | 9 | **Agentic RAG with MCP Server** is a powerful project that brings together an MCP (Model Context Protocol) server and client for building **Agentic RAG** (Retrieval-Augmented Generation) applications. 10 | 11 | This setup empowers your RAG system with advanced tools such as: 12 | 13 | * 🕵️‍♂️ **Entity Extraction** 14 | * 🔍 **Query Refinement** 15 | * ✅ **Relevance Checking** 16 | 17 | The server hosts these intelligent tools, while the client shows how to seamlessly connect and utilize them. 18 | 19 | --- 20 | 21 | ## 🖥️ Server — `server.py` 22 | 23 | Powered by the `FastMCP` class from the `mcp` library, the server exposes these handy tools: 24 | 25 | | Tool Name | Description | Icon | 26 | | ----------------------- | ----------------------------------------------------------------------------------------- | ---- | 27 | | `get_time_with_prefix` | Returns the **current date & time** | ⏰ | 28 | | `extract_entities_tool` | Uses **OpenAI** to extract entities from a query — enhancing document retrieval relevance | 🧠 | 29 | | `refine_query_tool` | Improves the quality of user queries with **OpenAI-powered refinement** | ✨ | 30 | | `check_relevance` | Filters out irrelevant content by checking chunk relevance with an LLM | ✅ | 31 | 32 | --- 33 | 34 | ## 🤝 Client — `mcp-client.py` 35 | 36 | The client demonstrates how to connect and interact with the MCP server: 37 | 38 | * Establish a connection with `ClientSession` from the `mcp` library 39 | * List all available server tools 40 | * Call any tool with custom arguments 41 | * Process queries leveraging **OpenAI or Gemini** and MCP tools in tandem 42 | 43 | --- 44 | 45 | ## ⚙️ Requirements 46 | 47 | * Python 3.9 or higher 48 | * `openai` Python package 49 | * `mcp` library 50 | * `python-dotenv` for environment variable management 51 | 52 | --- 53 | 54 | ## 🛠️ Installation Guide 55 | 56 | ```bash 57 | # Step 1: Clone the repository 58 | git clone https://github.com/ashishpatel26/Agentic-RAG-with-MCP-Server.git 59 | 60 | # Step 2: Navigate into the project directory 61 | cd Agentic-RAG-with-MCP-Serve 62 | 63 | # Step 3: Install dependencies 64 | pip install -r requirements.txt 65 | ``` 66 | 67 | --- 68 | 69 | ## 🔐 Configuration 70 | 71 | 1. Create a `.env` file (use `.env.sample` as a template) 72 | 2. Set your OpenAI model in `.env`: 73 | 74 | ```env 75 | OPENAI_MODEL_NAME="your-model-name-here" 76 | GEMINI_API_KEY="your-model-name-here" 77 | ``` 78 | 79 | --- 80 | 81 | ## 🚀 How to Use 82 | 83 | 1. **Start the MCP server:** 84 | 85 | ```bash 86 | python server.py 87 | ``` 88 | 89 | 2. **Run the MCP client:** 90 | 91 | ```bash 92 | python mcp-client.py 93 | ``` 94 | 95 | --- 96 | 97 | ## 📜 License 98 | 99 | This project is licensed under the [MIT License](LICENSE). 100 | 101 | --- 102 | 103 | ***Thanks for Reading 🙏*** 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import json 4 | import sys 5 | import openai 6 | import config 7 | from dotenv import load_dotenv 8 | from mcp.server.fastmcp import FastMCP 9 | 10 | # Load environment variables 11 | load_dotenv() 12 | 13 | # Create an MCP server 14 | mcp = FastMCP( 15 | name="Knowledge Base", 16 | host="0.0.0.0", # only used for SSE transport (localhost) 17 | port=8050, # only used for SSE transport (set this to any port) 18 | ) 19 | 20 | @mcp.tool() 21 | def get_time_with_prefix(): 22 | """Get the current date and time.""" 23 | return str(datetime.datetime.now()) 24 | 25 | @mcp.tool() 26 | def extract_entities_tool(query: str) -> str: 27 | """Extract entities from a given text query using OpenAI.""" 28 | try: 29 | prompt = ( 30 | "You are an entity extraction specialist. Analyze the following query and identify any named entities " 31 | "present, categorizing them by type. Focus on entities that would be important for searching " 32 | "news articles about Sri Lanka. Use the following categories:\n\n" 33 | "PERSON: Real people or fictional characters\n" 34 | "LOCATION: Countries, cities, states, mountains, bodies of water, etc.\n" 35 | "ORGANIZATION: Companies, agencies, institutions, etc.\n" 36 | "EVENT: Named events such as festivals, wars, sports events, etc.\n" 37 | "DATE: Absolute or relative dates or periods\n" 38 | "FACILITY: Buildings, airports, highways, bridges, etc.\n" 39 | "PRODUCT: Objects, vehicles, foods, etc. (Not services)\n" 40 | "WORK_OF_ART: Titles of books, songs, etc.\n\n" 41 | "Return your answer as a JSON object with entity types as keys and arrays of extracted entities as values. " 42 | "Only include categories where entities were found. If no entities are found, return an empty JSON object {}." 43 | ) 44 | 45 | completion = openai.chat.completions.create( 46 | model=os.getenv("OPENAI_MODEL_NAME"), 47 | messages=[ 48 | {"role": "system", "content": prompt}, 49 | {"role": "user", "content": query} 50 | ] 51 | ) 52 | return completion.choices[0].message.content 53 | except Exception as e: 54 | return json.dumps({"error": str(e)}) 55 | 56 | @mcp.tool() 57 | def refine_query_tool(original_query: str) -> str: 58 | """Refine a given text query using OpenAI.""" 59 | try: 60 | 61 | prompt = ( 62 | "You are a search query optimization agent. Your task is to refine and improve search queries " 63 | "about Sri Lanka news to make them more effective for vector search. Enhance specificity, add context, " 64 | "and clarify ambiguities. Keep the query concise. Return ONLY the refined query without explanations, " 65 | "and do not add any year or number randomly to the qyery" 66 | ) 67 | 68 | response = openai.chat.completions.create( 69 | model=os.getenv("OPENAI_MODEL_NAME"), 70 | messages=[ 71 | {"role": "system", "content": prompt}, 72 | {"role": "user", "content": f"Original query: {original_query}"} 73 | ], 74 | max_tokens=100, 75 | temperature=0.3 76 | ) 77 | 78 | refined_query = response.choices[0].message.content.strip() 79 | 80 | # If the refinement drastically changed the query, revert to original 81 | if len(refined_query) > len(original_query) * 3: 82 | return original_query 83 | 84 | return refined_query 85 | 86 | except Exception as e: 87 | return json.dumps({"error": str(e)}) 88 | 89 | @mcp.tool() 90 | def check_relevance(question: str, text_chunk: str) -> float: 91 | """ 92 | Check the relevance of a text chunk to a given question using an LLM. 93 | Returns a relevance score between 0 and 1. 94 | """ 95 | try: 96 | 97 | # Truncate content for API efficiency (can be adjusted based on token limits) 98 | content_preview = text_chunk[:1000] + "..." if len(text_chunk) > 1000 else text_chunk 99 | 100 | prompt = f""" 101 | Task: Evaluate if this article is relevant to the user's query. 102 | 103 | User Query: "{question}" 104 | 105 | Article Content Preview: 106 | "{content_preview}" 107 | 108 | Provide a relevance score from 0 to 1, where: 109 | - 0.0-0.3: Not relevant at all 110 | - 0.4-0.6: Somewhat relevant but missing key aspects 111 | - 0.7-1.0: Highly relevant to the query 112 | 113 | First provide your reasoning, then on a new line, output only 'SCORE: X.X' (a number between 0 and 1). 114 | """ 115 | 116 | response = openai.chat.completions.create( 117 | model=os.getenv("OPENAI_MODEL_NAME"), # Can be adjusted based on requirements 118 | messages=[ 119 | {"role": "system", "content": "You are an expert news curator that accurately evaluates if articles are relevant to user queries."}, 120 | {"role": "user", "content": prompt} 121 | ], 122 | max_tokens=300 123 | ) 124 | 125 | result_text = response.choices[0].message.content 126 | 127 | # Parse the score from the format "SCORE: X.X" 128 | try: 129 | # Find the score line 130 | score_line = [line for line in result_text.split('\n') if 'SCORE:' in line] 131 | if score_line: 132 | # Extract just the number 133 | score_text = score_line[0].split('SCORE:')[1].strip() 134 | relevance_score = float(score_text) 135 | else: 136 | # Fallback if format isn't followed 137 | relevance_score = 0.5 138 | 139 | except Exception as e: 140 | print(f"Error parsing relevance score: {e}") 141 | relevance_score = 0.5 142 | 143 | return float(relevance_score) 144 | except Exception as e: 145 | return json.dumps({"error": str(e)}) 146 | 147 | # Run the server 148 | if __name__ == "__main__": 149 | mcp.run(transport="sse") 150 | -------------------------------------------------------------------------------- /mcp-client.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | import json 4 | from contextlib import AsyncExitStack 5 | from typing import Any, Dict, List 6 | 7 | import nest_asyncio 8 | from dotenv import load_dotenv 9 | from mcp import ClientSession, StdioServerParameters 10 | from mcp.client.stdio import stdio_client 11 | from mcp.client.sse import sse_client 12 | import asyncio 13 | from openai import AsyncOpenAI 14 | 15 | # Apply nest_asyncio to allow nested event loops (needed for Jupyter/IPython) 16 | nest_asyncio.apply() 17 | 18 | # Load environment variables 19 | load_dotenv() 20 | 21 | # Global variables to store session state 22 | session = None 23 | exit_stack = AsyncExitStack() 24 | openai_client = AsyncOpenAI() 25 | stdio = None 26 | write = None 27 | 28 | 29 | async def connect_to_server(server_script_path: str = "server.py"): 30 | """Connect to an MCP server. 31 | 32 | Args: 33 | server_script_path: Path to the server script. 34 | """ 35 | global session, stdio, write, exit_stack 36 | 37 | # Server configuration 38 | server_params = StdioServerParameters( 39 | command="python", 40 | args=[server_script_path], 41 | ) 42 | 43 | # Connect to the server 44 | stdio_transport = await exit_stack.enter_async_context(stdio_client(server_params)) 45 | stdio, write = stdio_transport 46 | session = await exit_stack.enter_async_context(ClientSession(stdio, write)) 47 | 48 | # Initialize the connection 49 | await session.initialize() 50 | 51 | # List available tools 52 | tools_result = await session.list_tools() 53 | print("\nConnected to server with tools:") 54 | for tool in tools_result.tools: 55 | print(f" - {tool.name}: {tool.description}") 56 | 57 | 58 | async def get_mcp_tools() -> List[Dict[str, Any]]: 59 | """Get available tools from the MCP server in OpenAI format. 60 | 61 | Returns: 62 | A list of tools in OpenAI format. 63 | """ 64 | global session 65 | 66 | tools_result = await session.list_tools() 67 | return [ 68 | { 69 | "type": "function", 70 | "function": { 71 | "name": tool.name, 72 | "description": tool.description, 73 | "parameters": tool.inputSchema, 74 | }, 75 | } 76 | for tool in tools_result.tools 77 | ] 78 | 79 | 80 | async def process_query(query: str) -> str: 81 | """Process a query using OpenAI and available MCP tools. 82 | 83 | Args: 84 | query: The user query. 85 | 86 | Returns: 87 | The response from OpenAI. 88 | """ 89 | global session, openai_client 90 | 91 | # Get available tools 92 | tools = await get_mcp_tools() 93 | 94 | 95 | # Initial OpenAI API call 96 | response = await openai_client.chat.completions.create( 97 | model=os.getenv("OPENAI_MODEL_NAME"), 98 | messages=[{"role": "user", "content": query}], 99 | tools=tools, 100 | tool_choice="auto", 101 | ) 102 | 103 | # Get assistant's response 104 | assistant_message = response.choices[0].message 105 | 106 | # Initialize conversation with user query and assistant response 107 | messages = [ 108 | {"role": "user", "content": query}, 109 | assistant_message, 110 | ] 111 | 112 | # Handle tool calls if present 113 | if assistant_message.tool_calls: 114 | # Process each tool call 115 | for tool_call in assistant_message.tool_calls: 116 | # Execute tool call 117 | result = await session.call_tool( 118 | tool_call.function.name, 119 | arguments=json.loads(tool_call.function.arguments), 120 | ) 121 | 122 | # Add tool response to conversation 123 | messages.append( 124 | { 125 | "role": "tool", 126 | "tool_call_id": tool_call.id, 127 | "content": result.content[0].text, 128 | } 129 | ) 130 | 131 | # Get final response from OpenAI with tool results 132 | final_response = await openai_client.chat.completions.create( 133 | model=os.getenv("OPENAI_MODEL_NAME"), 134 | messages=messages, 135 | tools=tools, 136 | tool_choice="none", # Don't allow more tool calls 137 | ) 138 | 139 | return final_response.choices[0].message.content 140 | 141 | # No tool calls, just return the direct response 142 | return assistant_message.content 143 | 144 | 145 | async def cleanup(): 146 | """Clean up resources.""" 147 | global exit_stack 148 | await exit_stack.aclose() 149 | 150 | 151 | async def main(): 152 | """Main entry point for the client.""" 153 | #await connect_to_server("server.py") 154 | async with sse_client("http://localhost:8050/sse") as (read_stream, write_stream): 155 | global session 156 | async with ClientSession(read_stream, write_stream) as session: 157 | # Initialize the connection 158 | await session.initialize() 159 | 160 | # List available tools 161 | tools_result = await session.list_tools() 162 | print("\nConnected to server with tools:") 163 | for tool in tools_result.tools: 164 | print(f" - {tool.name}: {tool.description}") 165 | 166 | # Example: Ask about company vacation policy 167 | query = "What is the current time?" 168 | print(f"\nQuery: {query}") 169 | 170 | response = await process_query(query) 171 | print(f"\nResponse: {response}") 172 | 173 | # Example: Extract entities from a query 174 | entity_query = "What events happened in Colombo, Sri Lanka on January 1st, 2023?" 175 | print(f"\nEntity Query: {entity_query}") 176 | entity_response = await session.call_tool( 177 | "extract_entities_tool", arguments={"query": entity_query} 178 | ) 179 | print(f"\nEntity Response: {entity_response.content[0].text}") 180 | 181 | # Example: Refine a query 182 | refine_query = "What are the latest news about the president of Sri Lanka?" 183 | print(f"\nRefine Query: {refine_query}") 184 | refine_query_response = await session.call_tool( 185 | "refine_query_tool", arguments={"original_query": refine_query} 186 | ) 187 | print(f"\nRefine Query Response: {refine_query_response.content[0].text}") 188 | 189 | # Example: Check relevance of a document to a query 190 | document = "This is a news article about the American tax implimentation." 191 | query = "Sri Lankan politics" 192 | print(f"\nRelevance Query: Document: '{document}', Query: '{query}'") 193 | relevance_response = await session.call_tool( 194 | "check_relevance", arguments={"text_chunk": document, "question": query} 195 | ) 196 | print(f"\nRelevance Response: {relevance_response.content[0].text}") 197 | 198 | await cleanup() 199 | 200 | 201 | if __name__ == "__main__": 202 | asyncio.run(main()) 203 | --------------------------------------------------------------------------------