Build Your Own AI Agent with MCP: A Developer's Guide to Model Context Protocol
Ever wondered how to give your AI assistant access to real-time data, databases, or custom tools? The Model Context Protocol (MCP) is your gateway to building truly intelligent agents that can interact with the world beyond their training data.
What is MCP and Why Should You Care?
If you've been working with AI assistants like Claude, ChatGPT, or local models, you've probably hit the same wall I did: they're incredibly smart, but completely isolated from your actual data and tools.
Imagine if your AI could:
- Query your database in real-time
- Read from your knowledge base
- Execute custom business logic
- Access live APIs and services
That's exactly what the Model Context Protocol (MCP) enables. Think of it as a standardized way for AI models to communicate with external tools and data sources, similar to how REST APIs work for web services.
MCP was developed by Anthropic and is quickly becoming the standard for AI-tool integration. Instead of cramming everything into prompts or fine-tuning models, you can build specialized servers that your AI can call upon when needed.
The Architecture: Servers, Clients, and Protocols
Before we dive into code, let's understand the key components:
π₯οΈ MCP Server
Your custom server that provides tools and data to AI models. It's like a waiter that knows exactly what's available and how to get it.
π€ MCP Client
The bridge between your AI model (OpenAI, Claude, etc.) and your MCP server. It translates between AI function calls and MCP protocol.
π‘ Transport Protocols
MCP supports multiple ways for clients and servers to communicate:
-
stdio
(Standard Input/Output): Great for local development and simple setups -
SSE
(Server-Sent Events): Perfect for web applications and real-time updates - HTTP: For traditional request-response patterns
For this tutorial, we'll focus on stdio
since it's the most straightforward to get started with.
Building Your First MCP Server
Let's build a knowledge base server that can answer questions from a structured dataset. Here's the complete server implementation:
#!/usr/bin/env python3
import asyncio
import json
import os
from mcp.server import Server
import mcp.server.stdio
import mcp.types as types
# Create server instance
server = Server("knowledge-base-server")
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""Define what tools your server provides"""
return [
types.Tool(
name="get_knowledge_base",
description="Retrieve information from the knowledge base",
inputSchema={
"type": "object",
"properties": {},
"required": [],
},
),
]
@server.call_tool()
async def handle_call_tool(name: str, arguments: dict | None):
"""Handle tool execution requests"""
if name == "get_knowledge_base":
try:
kb_path = os.path.join("data", "kb.json")
with open(kb_path, 'r') as kb_file:
kb_data = json.load(kb_file)
# Format the knowledge base nicely
kb_text = "Knowledge Base Contents:\n\n"
for i, item in enumerate(kb_data, 1):
question = item.get("question", "Unknown")
answer = item.get("answer", "Unknown")
kb_text += f"Q{i}: {question}\n"
kb_text += f"A{i}: {answer}\n\n"
return [types.TextContent(type="text", text=kb_text)]
except Exception as e:
return [types.TextContent(
type="text",
text=f"Error accessing knowledge base: {str(e)}"
)]
else:
raise ValueError(f"Unknown tool: {name}")
async def main():
"""Run the server using stdio transport"""
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream)
if __name__ == "__main__":
asyncio.run(main())
Sample Knowledge Base (data/kb.json
)
[
{
"question": "What is MCP?",
"answer": "MCP (Model Context Protocol) is a protocol for AI assistants to communicate with external data sources and tools."
},
{
"question": "What transport protocols are supported?",
"answer": "MCP supports stdio, SSE (Server-Sent Events), and HTTP transport protocols."
},
{
"question": "What is the default transport protocol for MCP?",
"answer": "The default transport protocol for MCP is stdio (standard input/output)."
}
]
Creating the MCP Client
Now let's build a client that connects your AI model to the MCP server:
import asyncio
import json
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from openai import AsyncOpenAI
class MCPOpenAIClient:
def __init__(self, model="gpt-4o"):
self.session = None
self.exit_stack = AsyncExitStack()
self.model = model
self.openai_client = AsyncOpenAI()
async def connect_to_server(self, server_script_path="server.py"):
"""Connect to the MCP server"""
server_params = StdioServerParameters(
command="python",
args=[server_script_path],
)
# Establish connection
stdio_transport = await self.exit_stack.enter_async_context(
stdio_client(server_params)
)
stdio, write = stdio_transport
self.session = await self.exit_stack.enter_async_context(
ClientSession(stdio, write)
)
await self.session.initialize()
print("β
Connected to MCP server!")
async def get_mcp_tools(self):
"""Convert MCP tools to OpenAI function format"""
tools_result = await self.session.list_tools()
return [
{
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.inputSchema,
},
}
for tool in tools_result.tools
]
async def process_query(self, query: str) -> str:
"""Process a query using AI + MCP tools"""
tools = await self.get_mcp_tools()
# First AI call with tools available
response = await self.openai_client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": "You have access to a knowledge base. Use it to answer questions accurately."},
{"role": "user", "content": query}
],
tools=tools,
tool_choice="auto",
)
assistant_message = response.choices[0].message
# Handle tool calls
if assistant_message.tool_calls:
messages = [
{"role": "user", "content": query},
assistant_message,
]
# Execute each tool call
for tool_call in assistant_message.tool_calls:
result = await self.session.call_tool(
tool_call.function.name,
arguments=json.loads(tool_call.function.arguments),
)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result.content[0].text,
})
# Get final response with tool results
final_response = await self.openai_client.chat.completions.create(
model=self.model,
messages=messages,
)
return final_response.choices[0].message.content
return assistant_message.content
Putting It All Together
Here's how to use your new AI agent:
async def main():
client = MCPOpenAIClient()
try:
# Connect to your MCP server
await client.connect_to_server("server.py")
# Test queries
queries = [
"What is MCP?",
"What transport protocols are supported?",
"How does this knowledge base work?"
]
for query in queries:
print(f"\nπ€ Question: {query}")
response = await client.process_query(query)
print(f"π€ Answer: {response}")
finally:
await client.cleanup()
if __name__ == "__main__":
asyncio.run(main())
Understanding Transport Protocols
stdio (What We Used)
- Pros: Simple, great for development, works locally
- Cons: Not suitable for web applications
- Use case: Local AI agents, development, CLI tools
# Server runs as a subprocess, communicates via stdin/stdout
server_params = StdioServerParameters(
command="python",
args=["server.py"],
)
SSE (Server-Sent Events)
- Pros: Web-friendly, real-time updates, HTTP-based
- Cons: More complex setup
- Use case: Web applications, dashboards, real-time systems
# Server runs as web service, client connects via HTTP
server_params = SSEServerParameters(
url="http://localhost:8000/sse",
)
Why This Approach is Powerful
Traditional AI integration often looks like this:
- Fetch data in your application
- Stuff it into prompts
- Send to AI
- Parse response
- Hope for the best
With MCP, your workflow becomes:
- AI identifies what information it needs
- AI calls appropriate tools via MCP
- MCP server provides fresh, structured data
- AI processes and responds
This means:
- β Always fresh data (no stale embeddings)
- β Structured interactions (no prompt injection)
- β Scalable architecture (add tools without retraining)
- β Reusable components (one server, many AI clients)
Tips and Gotchas
π― Best Practices
- Start Simple: Begin with stdio transport and basic tools
- Error Handling: Always wrap MCP calls in try-catch blocks
- Tool Design: Make tools focused and well-documented
- Resource Cleanup: Always clean up connections properly
β οΈ Common Pitfalls
- Blocking Operations: Use async/await throughout your server
- Large Payloads: MCP isn't meant for transferring huge datasets
- Security: Validate all inputs in your tool handlers
- Connection Management: Handle disconnections gracefully
# Good: Proper error handling
try:
result = await self.session.call_tool(tool_name, args)
return result.content[0].text
except Exception as e:
return f"Tool error: {str(e)}"
# Bad: No error handling
result = await self.session.call_tool(tool_name, args)
return result.content[0].text
Project Structure for Success
Here's how I organize my MCP projects:
my-mcp-agent/
βββ server.py # MCP server implementation
βββ client.py # AI client with MCP integration
βββ data/
β βββ kb.json # Knowledge base
β βββ config.yaml # Server configuration
βββ tools/
β βββ __init__.py
β βββ knowledge_base.py # Tool implementations
β βββ calculator.py # More tools
βββ requirements.txt # Dependencies
βββ .env # API keys (don't commit!)
βββ README.md # Documentation
Going Further: Advanced Ideas
Once you've mastered the basics, consider these extensions:
π Multi-Tool Servers
@server.list_tools()
async def handle_list_tools():
return [
types.Tool(name="search_database", ...),
types.Tool(name="send_email", ...),
types.Tool(name="generate_report", ...),
]
π Web-Based Agents
Switch to SSE transport for browser-based AI agents:
# Instead of stdio, use SSE for web apps
from mcp.client.sse import sse_client
async with sse_client(sse_params) as transport:
# Your web-based AI agent
π Tool Chaining
Let your AI call multiple tools in sequence:
# AI can call get_user_data, then send_notification, then log_action
# All in one conversation turn!
Essential Resources
- MCP Specification: The official spec
- Python SDK: Official Python implementation
- MCP Servers Repository: Community examples
- Claude Desktop Integration: Use MCP with Claude directly
Wrapping Up
MCP represents a fundamental shift in how we build AI applications. Instead of cramming everything into prompts or fine-tuning models, we can build modular, reusable tools that any AI can leverage.
The knowledge base server we built today is just the beginning. Imagine servers that:
- Query your databases in real-time
- Interact with APIs and web services
- Execute business logic and workflows
- Provide specialized domain knowledge
The possibilities are endless, and the barrier to entry has never been lower.
What will you build with MCP? Drop a comment below with your ideas β I'd love to see what the community creates!
Connect & Explore More
If you found this tutorial helpful, I'd love to connect! Here are some ways to continue the conversation:
π Connect with me:
- LinkedIn - Let's discuss AI development and collaboration opportunities
- GitHub - Check out more AI projects and contribute to open source
π Explore the project:
- Complete MCP Knowledge Base Server - Fork the full project from this tutorial
- MCP Official Repository - Don't forget to star the official MCP project!
What will you build with MCP? Drop a comment below with your ideas β I'd love to see what the community creates and help troubleshoot any challenges you encounter!
Follow me here on Dev.to for more tutorials on AI agents, developer tools, and cutting-edge tech. More MCP content coming soon! π
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.