# Building an AI Agent with RAG and External Tools via MCP
Modern AI agents go beyond text generation—they interact with external APIs, manage state, and make decisions based on context. In this article, we'll build an agent that combines RAG for working with internal documentation and Model Context Protocol (MCP) for accessing external services: weather and GitHub Issues. The architecture is built on LangGraph and FastMCP, allowing the system to scale without writing custom adapters.
Architectural Components: ReAct, RAG, and MCP
The AI agent in our case operates on the ReAct cycle: The model analyzes the query → selects a tool → gets the result → repeats the cycle until the final answer. This differs from linear chains, where the sequence of steps is rigidly predefined. LangGraph is used to support decision-making—it provides explicit state management and transition graphs.
RAG here acts not as a separate module, but as one of the agent's tools. When a user asks about internal documentation, the agent calls search_documentation instead of relying on the model's parametric memory. This reduces the risk of hallucinations and increases response accuracy.
Model Context Protocol (MCP) standardizes the connection of external tools. Instead of manually writing wrappers for each API, we run an MCP server that provides tools via JSON-RPC. The agent connects to the server via a client, and all tools become available automatically. This is especially useful in production, where the number of integrations grows quickly.
Tech Stack and Choice Rationale
The choice of technologies is dictated by requirements for scalability, async support, and MCP compatibility:
- Python 3.11+: support for modern async/await, stable ecosystem.
- LangGraph + LangChain: LangGraph provides control over state and the ReAct cycle, LangChain—abstractions for models and tools.
- FastMCP: Python library for quickly creating MCP servers via decorators. Version 2.0 is actively developed and includes client capabilities.
- ChromaDB: lightweight embeddable vector DB, ideal for prototypes.
- OpenAI text-embedding-3-small: optimal price/performance ratio for embeddings.
- Claude or GPT-4: any model with function calling support.
Implementing the MCP Server for External Tools
The first step is creating an MCP server that provides two tools: getting the weather and creating a GitHub Issue. The server is built on FastMCP and runs as a subprocess with stdio transport.
# tools_server.py
import os
import httpx
from fastmcp import FastMCP
mcp = FastMCP("ExternalTools")
@mcp.tool()
async def get_weather(city: str) -> str:
async with httpx.AsyncClient() as client:
geo_response = await client.get(
"https://geocoding-api.open-meteo.com/v1/search",
params={"name": city, "count": 1, "language": "en", "format": "json"},
timeout=10.0
)
geo_data = geo_response.json()
if not geo_data.get("results"):
return f"Gorod '{city}' not found"
location = geo_data["results"][0]
lat, lon = location["latitude"], location["longitude"]
weather_response = await client.get(
"https://api.open-meteo.com/v1/forecast",
params={"latitude": lat, "longitude": lon, "current_weather": True},
timeout=10.0
)
current = weather_response.json()["current_weather"]
return f"Pogoda in {location['name']}: {current['temperature']}°C, veter {current['windspeed']} km/ch"
@mcp.tool()
async def create_github_issue(repo: str, title: str, body: str, labels: list[str] | None = None) -> str:
github_token = os.environ.get("GITHUB_TOKEN")
if not github_token:
return "Error: GITHUB_TOKEN not ustanovlen"
url = f"https://api.github.com/repos/{repo}/issues"
headers = {"Authorization": f"Bearer {github_token}", "Accept": "application/vnd.github+json"}
payload = {"title": title, "body": body}
if labels: payload["labels"] = labels
async with httpx.AsyncClient() as client:
response = await client.post(url, json=payload, headers=headers)
if response.status_code == 201:
return f"Issue sozdan: {response.json()['html_url']}"
else:
return f"Error: {response.status_code}"
if __name__ == "__main__":
mcp.run(transport="stdio")
Key features of the implementation:
- The
@mcp.tool()decorator automatically generates the JSON schema for parameters based on type annotations and docstring. - An async HTTP client is used for non-blocking requests.
- Error handling is built into each tool's logic—the agent receives human-readable messages instead of exceptions.
- Stdio transport allows the client to run the server as a child process and communicate via stdin/stdout.
Setting Up the RAG Module as an Agent Tool
The RAG system indexes PDF documents and provides a search function via a LangChain-compatible tool. Importantly, this tool is registered in the same pool as the MCP tools, allowing the agent to select it alongside external APIs.
# rag_tool.py
import os
from pathlib import Path
from langchain_openai import OpenAIEmbeddings
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain.tools import tool
CHROMA_PATH = "./chroma_db"
DATA_PATH = "./data"
@tool
def search_documentation(query: str) -> str:
"""Ischet relevantnye fragmenty in documentation by request."""
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma(persist_directory=CHROMA_PATH, embedding_function=embeddings)
docs = vectorstore.similarity_search(query, k=3)
return "\n\n".join([f"Withtranitsa {d.metadata.get('page', 'N/A')}: {d.page_content[:500]}..." for d in docs])
# Function initsializatsii database (vyzyvaetsya when starte agenta)
def initialize_vector_store(force_reload=False):
if os.path.exists(CHROMA_PATH) and not force_reload:
return
documents = []
for pdf_file in Path(DATA_PATH).glob("*.pdf"):
loader = PyPDFLoader(str(pdf_file))
documents.extend(loader.load())
if not documents:
return
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
chunks = splitter.split_documents(documents)
Chroma.from_documents(chunks, OpenAIEmbeddings(model="text-embedding-3-small"), persist_directory=CHROMA_PATH)
Integrating the MCP Client and Assembling the Agent in LangGraph
The final stage is assembling the agent that combines MCP tools and RAG. We use langchain-mcp-adapters to convert MCP tools to LangChain format, then register them along with the RAG tool in the graph.
# agent.py
from langgraph.graph import StateGraph, END
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage, AIMessage
from langchain_mcp_adapters import McpToolNode
from rag_tool import search_documentation
import subprocess
import asyncio
# Run MCP-server how podprotsessa
mcp_process = subprocess.Popen(
["python", "tools_server.py"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
# Withbudynek MCP-client and adaptera narzędzieov
mcp_node = McpToolNode(
server_command=["python", "tools_server.py"],
transport="stdio"
)
# Receiving list narzędzieov from MCP-server
mcp_tools = asyncio.run(mcp_node.get_tools())
all_tools = [*mcp_tools, search_documentation]
# Initialization modeli and graph
model = ChatAnthropic(model="claude-3-5-sonnet-20240620").bind_tools(all_tools)
# Withstoyanie graph
class AgentState(TypedDict):
messages: Annotated[list, operator.add]
# Uzly graph
def call_model(state):
response = model.invoke(state["messages"])
return {"messages": [response]}
def should_continue(state):
last_message = state["messages"][-1]
return "continue" if last_message.tool_calls else "end"
# Postroenie graph
workflow = StateGraph(AgentState)
workflow.add_node("agent", call_model)
workflow.add_node("tools", mcp_node)
workflow.set_entry_point("agent")
workflow.add_conditional_edges("agent", should_continue, {"continue": "tools", "end": END})
workflow.add_edge("tools", "agent")
app = workflow.compile()
# Example run
result = app.invoke({"messages": [HumanMessage(content="Uznay pogodu in Londone and sozday issue in repo/my-project with etim rezultatom")]})
print(result["messages"][-1].content)
What is important:
- MCP eliminates the need to write custom adapters for each new tool—just add a
@mcp.tool()decorator on the server. - RAG integrates as a regular tool, allowing the agent to choose between searching documentation and calling an external API.
- LangGraph provides transparent state management and ReAct loop control, which is critical for complex multi-step tasks.
- Asynchronous tool implementation prevents blocking the agent's main thread.
- Stdio transport simplifies deployment—the server and agent can run in the same container or on the same machine without network dependencies.
— Editorial Team
No comments yet.