Building Intelligent Agents With MCP and LangGraph
MCP is a universal protocol that lets AI agents discover and use tools from any source without custom integration code like USB-C for AI connectivity.
Join the DZone community and get the full member experience.
Join For FreeWe're at an interesting point in AI development. Language models have become very good at understanding and generating text, but they still can't do much independently. They can't check your calendar, pull data from your database, or send that email you've been meaning to write. Whenever we want to give an AI system a new capability, we have to write custom integration code. It’s like having a brilliant assistant who requires a new instruction manual for every single task.
The Model Context Protocol is working to solve this issue, and honestly, it's about time someone stepped up.
What Makes MCP Different
Most AI integrations today are a mess. Each AI application requires its own custom code to connect with every data source or tool it needs. If you're using three different AI tools across five systems, you end up managing fifteen separate integrations. Each one has its own quirks, its own authentication rules, and its own way of failing at 3 AM.
MCP takes a different approach. Instead of creating custom connections between every AI app and every tool, it sets up one universal standard. It's like USB-C replacing all those different cables we used to carry. One protocol means consistent behavior and works everywhere.
What makes it truly useful is that MCP is not just about standardization. It’s designed based on how AI agents actually function. The protocol recognizes that agents need three key things: access to information (resources), the ability to take actions (tools), and reusable patterns for common tasks (prompts). Everything else is just implementation details.
Why This Matters for Agent Development
If you’ve built AI agents before, you know how frustrating it can be. You spend weeks getting one agent to work with a single API. Then, when you need to add another feature, it feels like you’re starting over. Your codebase becomes a tangled mess of integration logic, and every update risks breaking something else.
With MCP, agents can find available tools on their own. Your agent doesn’t need to know in advance what tools are available or how to use them. It figures that out while it’s running. If you need to add a new feature, just start an MCP server, and any MCP-compatible agent can use it right away. There are no code changes or deployment issues.
This works especially well with frameworks like LangGraph. LangGraph is great at managing complex agent workflows. It decides which tools to use, when to use them, and how to combine results. MCP takes care of the universal connection. Together, they allow you to build complex multi-agent systems without getting buried in integration code.
How Agents Leverage Tools Through MCP
Let's discuss how this works in practice. When you build an agent with MCP support, the agent doesn't have fixed tool definitions. Instead, it connects to MCP servers and asks, "What can you do?"
Each MCP server replies with a schema that describes its capabilities. These schemas follow a standard format, allowing the agent to interpret them accurately. The agent can then decide which tools to use based on the user's request, set the right parameters, and execute the tool call without needing custom integration code.
Communication occurs through JSON-RPC, which is a recognized standard for remote procedure calls. Messages are exchanged in a structured format: the agent sends a request, the server processes it, and returns a response. The transport layer manages the communication, whether it’s local stdio for development or HTTP for production.
What’s smart is how this scales. You can run multiple MCP servers, each offering different capabilities. One might handle database queries, another could manage file operations, and a third could handle external API calls. The agent finds and uses all of them through the same standard interface.
Building Your First MCP-Enabled Agent With LangGraph
Let's build something real. We'll create an agent that can analyze code repositories using GitHub's API, with all the tool connectivity handled through MCP.
First, we need an MCP server that exposes GitHub functionality. Here's a practical implementation:
# github_mcp_server.py
import asyncio
import os
from mcp.server import Server
from mcp.types import Tool, TextContent
import requests
class GitHubMCPServer:
def __init__(self):
self.server = Server("github-tools")
self.token = os.getenv("GITHUB_TOKEN")
self.api_base = "https://api.github.com"
self._setup()
def _setup(self):
@self.server.list_tools()
async def list_tools():
return [
Tool(
name="get_repo_info",
description="Fetches repository information including stars, forks, and description",
inputSchema={
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"}
},
"required": ["owner", "repo"]
}
),
Tool(
name="list_pull_requests",
description="Lists open pull requests for a repository",
inputSchema={
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"state": {"type": "string", "enum": ["open", "closed", "all"]}
},
"required": ["owner", "repo"]
}
),
Tool(
name="search_code",
description="Searches for code across GitHub repositories",
inputSchema={
"type": "object",
"properties": {
"query": {"type": "string"},
"language": {"type": "string"}
},
"required": ["query"]
}
)
]
@self.server.call_tool()
async def call_tool(name: str, arguments: dict):
headers = {
"Authorization": f"token {self.token}",
"Accept": "application/vnd.github.v3+json"
}
if name == "get_repo_info":
url = f"{self.api_base}/repos/{arguments['owner']}/{arguments['repo']}"
response = requests.get(url, headers=headers)
data = response.json()
result = {
"name": data.get("name"),
"description": data.get("description"),
"stars": data.get("stargazers_count"),
"forks": data.get("forks_count"),
"language": data.get("language"),
"open_issues": data.get("open_issues_count")
}
return [TextContent(type="text", text=str(result))]
elif name == "list_pull_requests":
url = f"{self.api_base}/repos/{arguments['owner']}/{arguments['repo']}/pulls"
params = {"state": arguments.get("state", "open")}
response = requests.get(url, headers=headers, params=params)
pulls = response.json()
result = [
{
"number": pr["number"],
"title": pr["title"],
"author": pr["user"]["login"],
"state": pr["state"]
}
for pr in pulls[:5]
]
return [TextContent(type="text", text=str(result))]
elif name == "search_code":
url = f"{self.api_base}/search/code"
query = arguments["query"]
if "language" in arguments:
query += f" language:{arguments['language']}"
params = {"q": query, "per_page": 5}
response = requests.get(url, headers=headers, params=params)
results = response.json()
items = [
{
"repository": item["repository"]["full_name"],
"path": item["path"],
"url": item["html_url"]
}
for item in results.get("items", [])
]
return [TextContent(type="text", text=str(items))]
async def run(self):
from mcp.server.stdio import stdio_server
async with stdio_server() as (read, write):
await self.server.run(read, write, self.server.create_initialization_options())
if __name__ == "__main__":
server = GitHubMCPServer()
asyncio.run(server.run())
Now comes the interesting part: connecting this server to a LangGraph agent. LangGraph provides the orchestration framework, while MCP handles the tool connectivity:
# github_agent.py
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode, tools_condition
from langchain_openai import ChatOpenAI
async def build_agent():
# Initialize MCP client pointing to our GitHub server
mcp_client = MultiServerMCPClient({
"github": {
"command": "python",
"args": ["github_mcp_server.py"],
"transport": "stdio"
}
})
# Get tools from MCP server
tools = await mcp_client.get_tools()
# Initialize the language model
model = ChatOpenAI(model="gpt-4", temperature=0)
model_with_tools = model.bind_tools(tools)
# Define agent node
def agent_node(state: MessagesState):
response = model_with_tools.invoke(state["messages"])
return {"messages": [response]}
# Build the graph
workflow = StateGraph(MessagesState)
workflow.add_node("agent", agent_node)
workflow.add_node("tools", ToolNode(tools))
workflow.add_edge(START, "agent")
workflow.add_conditional_edges(
"agent",
tools_condition,
{
"tools": "tools",
"end": END
}
)
workflow.add_edge("tools", "agent")
return workflow.compile()
async def main():
agent = await build_agent()
# Example queries
queries = [
"Get information about the facebook/react repository",
"List the open pull requests for microsoft/vscode",
"Search for authentication code examples in Python"
]
for query in queries:
print(f"\nQuery: {query}")
print("-" * 50)
result = await agent.ainvoke({
"messages": [{"role": "user", "content": query}]
})
final_message = result["messages"][-1]
print(f"Response: {final_message.content}\n")
if __name__ == "__main__":
asyncio.run(main())
What's Happening Under the Hood
When you run this agent, several things happen automatically.
The MCP client starts the GitHub server as a subprocess and establishes communication through stdio. It sends a tools discovery request, and the server responds with schemas for all available tools. LangGraph receives these tool definitions and makes them accessible to the language model.
When you ask for information about the Facebook/React repository, the model assesses the request and identifies that it needs the get_repo_info tool. It generates the necessary parameters: owner: "facebook" and repo: "react." LangGraph handles the execution. The tool call moves through MCP to the GitHub server, which makes the actual API request. Results return through the same path, and the model includes them in its response.
The advantage is that none of this required you to write integration code. The agent discovered the tools dynamically, figured out how to use them from the schemas, and executed them through the standard protocol.
Scaling to Multiple Tools and Complex Workflows
The real power emerges when you start combining multiple MCP servers. Let's extend our agent to also access local files:
async def build_multi_server_agent():
mcp_client = MultiServerMCPClient({
"github": {
"command": "python",
"args": ["github_mcp_server.py"],
"transport": "stdio"
},
"filesystem": {
"command": "python",
"args": ["filesystem_mcp_server.py"],
"transport": "stdio"
},
"database": {
"url": "http://localhost:8000/mcp",
"transport": "streamable_http",
"headers": {"Authorization": "Bearer your-token"}
}
})
tools = await mcp_client.get_tools()
# Rest of agent setup...
Now your agent can reason across multiple domains.
It might fetch code from GitHub, analyze it against local documentation, and update a database all through natural language instructions. The agent decides which tools to use and in what order, while MCP ensures consistent communication with each service.
For more sophisticated workflows, you can add custom nodes to your LangGraph:
def research_node(state: MessagesState):
"""Custom node for research tasks"""
last_message = state["messages"][-1]
# Analyze the request
if "compare" in last_message.content.lower():
# Trigger multiple parallel tool calls
return {"messages": [/* orchestration logic */]}
return {"messages": [last_message]}
workflow.add_node("research", research_node)
workflow.add_edge("agent", "research")
workflow.add_conditional_edges("research", /* routing logic */)
Production Considerations
When you're ready to deploy, a few things matter. First, switch from stdio transport to HTTP for your production servers. Stdio is great for development, but doesn't scale well:
# production_mcp_server.py
from fastapi import FastAPI
from mcp.server.sse import sse_server
app = FastAPI()
@app.post("/mcp")
async def mcp_endpoint():
async with sse_server() as (read, write):
await github_server.run(read, write, init_options)
Authentication becomes critical in production. Don't hardcode tokens, use environment variables, or a proper secrets manager:
mcp_client = MultiServerMCPClient({
"github": {
"url": "https://your-mcp-server.com/mcp",
"transport": "streamable_http",
"headers": {
"Authorization": f"Bearer {os.getenv('MCP_TOKEN')}"
}
}
})
Error handling needs attention, too. MCP servers can fail, network calls can timeout, and APIs can be rate-limited. Wrap your tool executions appropriately:
async def call_tool_with_retry(name: str, arguments: dict, max_retries=3):
for attempt in range(max_retries):
try:
return await server.call_tool(name, arguments)
except TimeoutError:
if attempt == max_retries - 1:
return [TextContent(
type="text",
text=f"Tool {name} timed out after {max_retries} attempts"
)]
await asyncio.sleep(2 ** attempt)
The Bigger Picture
What's interesting about MCP isn't just the technical implementation; it's what becomes possible when everyone uses the same standard. Right now, if you build a useful tool as an MCP server, any MCP-compatible agent can use it. If you build a sophisticated agent, it can immediately access the entire ecosystem of MCP tools.
This network effect is already starting to show. OpenAI integrated MCP support across its products earlier this year. Development tools like VS Code and Cursor have native MCP clients built in. Companies are starting to expose their internal systems as MCP servers, making them instantly accessible to AI agents across the organization.
The protocol continues evolving based on real-world usage. Recent additions include better support for streaming responses, enhanced security primitives for sensitive operations, and improved patterns for handling large contexts. The community around MCP is active, with new servers and clients appearing regularly.
Getting Started
If you want to experiment with this yourself, the setup is straightforward. Install the necessary packages:
pip install mcp langchain-mcp-adapters langgraph langchain-openai
Clone the examples from this article, add your API keys, and start playing. Begin with a simple server exposing one or two tools. Get comfortable with how the protocol works. Then gradually add complexity, more tools, multiple servers, and sophisticated routing logic in your LangGraph workflows.
The documentation for both MCP and LangGraph is solid. The communities are helpful. The tooling is mature enough for production use while still evolving rapidly enough to be interesting.
Where This Goes Next
We're still early in understanding how to build truly capable AI agents. The tooling is getting better, the models are getting smarter, but we're figuring out the patterns as we go. MCP solves one important piece of the puzzle, universal connectivity, which frees us to focus on the harder problems of reasoning, planning, and robust execution.
The agents we can build today are impressive but limited. They work well for structured tasks with clear objectives. As protocols like MCP mature and frameworks like LangGraph evolve, we'll push into more complex territory: agents that maintain long-running contexts, coordinate across organizational boundaries, and handle truly open-ended tasks.
For now, we have the tools to build useful things. Agents that can access your company's data sources, orchestrate complex workflows, and actually take action in the world. That's not theoretical anymore, it's just code you can write this afternoon.
The question isn't whether to adopt these standards, but how quickly you can start leveraging them to build capabilities that weren't practical before. Start small, learn the patterns, and see what becomes possible when your agents can actually reach beyond their training data into the real world.
Opinions expressed by DZone contributors are their own.
Comments