What You'll Build
In this comprehensive tutorial, you'll create three progressively advanced AI agents using the Model Context Protocol (MCP):
๐ Hello World Agent (15 min)
Basic MCP server with greeting functionality. Perfect introduction to MCP concepts and architecture.
๐ค๏ธ Weather Intelligence Agent (30 min)
Real-world agent with API integration, error handling, and caching for weather data.
๐ Production-Ready Agent (60 min)
Enterprise-grade agent with advanced features, validation, monitoring, and deployment strategies.
Prerequisites & Environment Setup
Before we begin building, ensure you have the proper development environment configured:
System Requirements
- Python: 3.10 or higher (recommended: 3.11+)
- Memory: 4GB RAM minimum (8GB recommended)
- Storage: 2GB available space
- Operating System: Windows, macOS, or Linux
- Development Tools: VS Code with Python extension (optional but recommended)
Quick Environment Setup
๐ฆ Step 1: Install Modern Package Manager
We'll use uv
for fast Python package management:
curl -LsSf https://astral.sh/uv/install.sh | sh
๐๏ธ Step 2: Create Project Structure
Set up your development workspace:
uv init my-first-mcp-agent
cd my-first-mcp-agent
uv venv
source .venv/bin/activate
uv add "mcp[cli]" httpx pydantic redis python-dotenv
โ
Verification
Test your installation with: python -c "import mcp; print('MCP installed successfully!')"
Understanding MCP Architecture
Before diving into code, let's understand the core MCP concepts that power AI agent development:
MCP Agent Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Claude Desktop โ
โ (or other MCP client) โ
โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโ
โ JSON-RPC 2.0
โ stdio/HTTP/WebSocket
โโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโ
โ Your MCP Server โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Tools Registry โ โ
โ โ โข API integrations โ โ
โ โ โข Data processing โ โ
โ โ โข Business logic โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Resources Registry โ โ
โ โ โข File access โ โ
โ โ โข Database connections โ โ
โ โ โข External services โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ ๏ธ Tools
Functions that Claude can call to perform actions like API calls, calculations, or data processing.
๐ Resources
Data sources that Claude can read from, such as files, databases, or web content.
๐ Protocol
JSON-RPC 2.0 communication protocol enabling standardized AI-agent interaction.
Step 1: Hello World MCP Server (15 minutes)
Let's start with the simplest possible MCP server to understand core concepts:
๐ฏ Goal: Create Basic MCP Server
Build a minimal working MCP server with a greeting tool and server information resource.
from mcp.server.fastmcp import FastMCP
import asyncio
mcp = FastMCP("Hello World AI Agent")
@mcp.tool()
def greet_user(name: str) -> str:
"""Greet a user by name with a friendly message"""
return f"Hello, {name}! ๐ I'm your first MCP agent, ready to help!"
@mcp.tool()
def get_server_stats() -> dict:
"""Provide basic server statistics and information"""
return {
"server_name": "Hello World AI Agent",
"version": "1.0.0",
"status": "active",
"capabilities": ["greeting", "information"]
}
@mcp.resource("info://server")
def server_info() -> str:
"""Provide comprehensive server information"""
return """
Hello World MCP Server - Your First AI Agent!
This server demonstrates basic MCP concepts:
- Tool registration and execution
- Resource provision
- JSON-RPC communication protocol
Available tools:
- greet_user(name): Personal greeting functionality
- get_server_stats(): Server status and capabilities
Ready to build more advanced agents!
"""
if __name__ == "__main__":
asyncio.run(mcp.run())
Testing Your First Agent
๐งช Test the Server
Verify your MCP server works correctly:
# Run the server directly
python hello_mcp.py
# Test with MCP CLI tools (in another terminal)
mcp test hello_mcp.py --tool greet_user --args '{"name": "Developer"}'
โ
Expected Output
You should see: Hello, Developer! ๐ I'm your first MCP agent, ready to help!
Step 2: Weather Intelligence Agent (30 minutes)
Now let's build a practical agent that integrates with external APIs and handles real-world data:
๐ฏ Advanced Features
- External API integration (OpenWeatherMap)
- Comprehensive error handling
- Input validation with Pydantic
- Async/await patterns
- Resource-based data access
โ ๏ธ API Key Required
You'll need a free OpenWeatherMap API key. Sign up at openweathermap.org/api and replace your-api-key-here
in the code.
from mcp.server.fastmcp import FastMCP
import httpx
import asyncio
import os
from typing import Dict, Any, Optional
from pydantic import BaseModel, validator
from datetime import datetime
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class WeatherQuery(BaseModel):
city: str
country: str = "US"
units: str = "metric"
@validator('city')
def validate_city(cls, v):
if not v or len(v) > 100:
raise ValueError('City name is required and must be under 100 characters')
return v.strip()
mcp = FastMCP("Weather Intelligence Agent")
API_KEY = os.getenv("OPENWEATHER_API_KEY", "your-api-key-here")
BASE_URL = "https://api.openweathermap.org/data/2.5"
@mcp.tool()
async def get_current_weather(city: str, country: str = "US") -> Dict[str, Any]:
"""Get current weather conditions for a specified city
Args:
city: Name of the city (e.g., 'San Francisco')
country: Country code (e.g., 'US', 'UK', 'CA')
Returns:
Dict containing weather data or error information
"""
try:
query = WeatherQuery(city=city, country=country)
url = f"{BASE_URL}/weather"
params = {
"q": f"{query.city},{query.country}",
"appid": API_KEY,
"units": query.units
}
async with httpx.AsyncClient() as client:
response = await client.get(
url,
params=params,
timeout=10.0
)
if response.status_code == 200:
data = response.json()
logger.info(f"Successfully retrieved weather for {city}, {country}")
return {
"success": True,
"city": data["name"],
"country": data["sys"]["country"],
"temperature": data["main"]["temp"],
"feels_like": data["main"]["feels_like"],
"description": data["weather"][0]["description"].title(),
"humidity": data["main"]["humidity"],
"pressure": data["main"]["pressure"],
"wind_speed": data.get("wind", {}).get("speed", 0),
"timestamp": datetime.now().isoformat()
}
else:
logger.error(f"API request failed: {response.status_code}")
return {
"success": False,
"error": f"Weather API returned status {response.status_code}",
"message": "Unable to fetch weather data. Please check city name and try again."
}
except ValueError as e:
logger.error(f"Validation error: {e}")
return {
"success": False,
"error": "validation_error",
"message": str(e)
}
except httpx.TimeoutException:
logger.error(f"Timeout fetching weather for {city}")
return {
"success": False,
"error": "timeout",
"message": "Request timed out. Please try again later."
}
except Exception as e:
logger.exception(f"Unexpected error fetching weather: {e}")
return {
"success": False,
"error": "internal_error",
"message": "An internal error occurred. Please try again."
}
@mcp.tool()
async def get_weather_forecast(city: str, days: int = 5) -> Dict[str, Any]:
"""Get weather forecast for the next few days
Args:
city: Name of the city
days: Number of days to forecast (1-5)
Returns:
Dict containing forecast data
"""
try:
if days < 1 or days > 5:
return {
"success": False,
"error": "validation_error",
"message": "Days must be between 1 and 5"
}
url = f"{BASE_URL}/forecast"
params = {
"q": city,
"appid": API_KEY,
"units": "metric",
"cnt": days * 8
}
async with httpx.AsyncClient() as client:
response = await client.get(url, params=params, timeout=10.0)
if response.status_code == 200:
data = response.json()
return {
"success": True,
"city": data["city"]["name"],
"forecast_count": len(data["list"]),
"forecasts": [
{
"datetime": item["dt_txt"],
"temperature": item["main"]["temp"],
"description": item["weather"][0]["description"].title(),
"humidity": item["main"]["humidity"]
} for item in data["list"][:days*2]
]
}
else:
return {
"success": False,
"error": f"API error: {response.status_code}"
}
except Exception as e:
logger.exception(f"Error fetching forecast: {e}")
return {
"success": False,
"error": "internal_error",
"message": str(e)
}
@mcp.resource("weather://cities/{city}")
async def city_weather_info(city: str) -> str:
"""Get comprehensive weather information for a city as a formatted resource"""
weather_data = await get_current_weather(city)
if weather_data["success"]:
return f"""
Weather Report for {weather_data['city']}, {weather_data['country']}
Generated: {weather_data['timestamp']}
๐ก๏ธ Temperature: {weather_data['temperature']}ยฐC (feels like {weather_data['feels_like']}ยฐC)
๐ค๏ธ Conditions: {weather_data['description']}
๐ง Humidity: {weather_data['humidity']}%
๐๏ธ Pressure: {weather_data['pressure']} hPa
๐จ Wind Speed: {weather_data['wind_speed']} m/s
This data is provided by OpenWeatherMap API and updated in real-time.
"""
else:
return f"Weather data unavailable for {city}. Error: {weather_data.get('message', 'Unknown error')}"
if __name__ == "__main__":
print("๐ค๏ธ Starting Weather Intelligence Agent...")
print("Make sure to set your OPENWEATHER_API_KEY environment variable!")
asyncio.run(mcp.run())
Testing Your Weather Agent
๐ง Setup API Key
Set your environment variable before testing:
# Set API key (replace with your actual key)
export OPENWEATHER_API_KEY="your_actual_api_key_here"
# Test the weather agent
python weather_agent.py
# In another terminal, test specific functionality
mcp test weather_agent.py --tool get_current_weather --args '{"city": "San Francisco", "country": "US"}'
Step 3: Production-Ready Features (45 minutes)
Let's enhance our weather agent with enterprise-grade features for production deployment:
๐ Advanced Error Handling
Comprehensive error categorization, rate limiting, and recovery strategies
โก Performance Optimization
Redis caching, connection pooling, and response time optimization
๐ Monitoring & Logging
Structured logging, metrics collection, and health checks
๐ก๏ธ Security & Validation
Input sanitization, rate limiting, and secure API key management
from mcp.server.fastmcp import FastMCP
import httpx
import asyncio
import redis.asyncio as redis
import json
import os
from typing import Dict, Any, Optional
from pydantic import BaseModel, validator
from datetime import datetime, timedelta
from enum import Enum
import logging
import time
from functools import wraps
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s',
handlers=[
logging.FileHandler('weather_agent.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
class ErrorCode(Enum):
VALIDATION_ERROR = "VALIDATION_ERROR"
API_ERROR = "API_ERROR"
RATE_LIMIT = "RATE_LIMIT"
CACHE_ERROR = "CACHE_ERROR"
NETWORK_ERROR = "NETWORK_ERROR"
INTERNAL_ERROR = "INTERNAL_ERROR"
class WeatherQuery(BaseModel):
city: str
country: str = "US"
units: str = "metric"
@validator('city')
def validate_city(cls, v):
if not v or len(v.strip()) == 0:
raise ValueError('City name is required')
if len(v) > 100:
raise ValueError('City name must be under 100 characters')
sanitized = ''.join(c for c in v if c.isalnum() or c in ' -')
return sanitized.strip()
@validator('units')
def validate_units(cls, v):
valid_units = ['metric', 'imperial', 'kelvin']
if v not in valid_units:
raise ValueError(f'Units must be one of {valid_units}')
return v
class ProductionWeatherAgent:
def __init__(self):
self.mcp = FastMCP("Production Weather Intelligence Agent")
self.api_key = os.getenv("OPENWEATHER_API_KEY")
self.redis_client: Optional[redis.Redis] = None
self.http_client: Optional[httpx.AsyncClient] = None
self.request_count = 0
self.start_time = time.time()
self.rate_limit_requests = 100
self.rate_limit_window = 3600
self._register_tools()
async def initialize(self):
"""Initialize async components"""
try:
redis_url = os.getenv("REDIS_URL", "redis://localhost:6379")
self.redis_client = redis.from_url(redis_url, decode_responses=True)
await self.redis_client.ping()
logger.info("Redis connection established")
except Exception as e:
logger.warning(f"Redis connection failed: {e}. Continuing without caching.")
self.redis_client = None
timeout = httpx.Timeout(10.0, connect=5.0)
limits = httpx.Limits(max_keepalive_connections=5, max_connections=10)
self.http_client = httpx.AsyncClient(timeout=timeout, limits=limits)
def rate_limit(self, func):
"""Decorator for rate limiting"""
@wraps(func)
async def wrapper(*args, **kwargs):
if await self._check_rate_limit():
return {
"success": False,
"error_code": ErrorCode.RATE_LIMIT.value,
"message": "Rate limit exceeded. Please try again later."
}
return await func(*args, **kwargs)
return wrapper
async def _check_rate_limit(self) -> bool:
"""Check if rate limit is exceeded"""
current_time = time.time()
if current_time - self.start_time > self.rate_limit_window:
self.request_count = 0
self.start_time = current_time
self.request_count += 1
return self.request_count > self.rate_limit_requests
async def _get_cached_data(self, cache_key: str) -> Optional[dict]:
"""Retrieve data from cache"""
if not self.redis_client:
return None
try:
cached_data = await self.redis_client.get(cache_key)
if cached_data:
return json.loads(cached_data)
except Exception as e:
logger.error(f"Cache retrieval error: {e}")
return None
async def _set_cached_data(self, cache_key: str, data: dict, ttl: int = 600):
"""Store data in cache"""
if not self.redis_client:
return
try:
await self.redis_client.setex(cache_key, ttl, json.dumps(data))
except Exception as e:
logger.error(f"Cache storage error: {e}")
def _register_tools(self):
"""Register all MCP tools"""
@self.mcp.tool()
@self.rate_limit
async def get_current_weather_cached(city: str, country: str = "US") -> Dict[str, Any]:
"""Get current weather with intelligent caching"""
return await self._get_weather_with_cache(city, country)
@self.mcp.tool()
async def get_agent_health() -> Dict[str, Any]:
"""Get agent health and performance metrics"""
return {
"status": "healthy",
"uptime_seconds": time.time() - self.start_time,
"requests_processed": self.request_count,
"cache_available": self.redis_client is not None,
"api_key_configured": bool(self.api_key and self.api_key != "your-api-key-here"),
"timestamp": datetime.now().isoformat()
}
async def _get_weather_with_cache(self, city: str, country: str) -> Dict[str, Any]:
"""Get weather data with caching logic"""
try:
query = WeatherQuery(city=city, country=country)
cache_key = f"weather:{query.city}:{query.country}"
cached_result = await self._get_cached_data(cache_key)
if cached_result:
logger.info(f"Cache hit for {city}, {country}")
cached_result["cache_hit"] = True
return cached_result
weather_data = await self._fetch_weather_api(query)
if weather_data["success"]:
weather_data["cache_hit"] = False
await self._set_cached_data(cache_key, weather_data, 600)
return weather_data
except ValueError as e:
logger.error(f"Validation error: {e}")
return {
"success": False,
"error_code": ErrorCode.VALIDATION_ERROR.value,
"message": str(e)
}
except Exception as e:
logger.exception(f"Unexpected error: {e}")
return {
"success": False,
"error_code": ErrorCode.INTERNAL_ERROR.value,
"message": "Internal server error"
}
async def _fetch_weather_api(self, query: WeatherQuery) -> Dict[str, Any]:
"""Fetch weather data from OpenWeatherMap API"""
if not self.api_key or self.api_key == "your-api-key-here":
return {
"success": False,
"error_code": ErrorCode.API_ERROR.value,
"message": "API key not configured"
}
url = "https://api.openweathermap.org/data/2.5/weather"
params = {
"q": f"{query.city},{query.country}",
"appid": self.api_key,
"units": query.units
}
try:
response = await self.http_client.get(url, params=params)
if response.status_code == 200:
data = response.json()
logger.info(f"API success for {query.city}, {query.country}")
return {
"success": True,
"city": data["name"],
"country": data["sys"]["country"],
"temperature": data["main"]["temp"],
"feels_like": data["main"]["feels_like"],
"description": data["weather"][0]["description"].title(),
"humidity": data["main"]["humidity"],
"pressure": data["main"]["pressure"],
"wind_speed": data.get("wind", {}).get("speed", 0),
"timestamp": datetime.now().isoformat()
}
else:
logger.error(f"API error {response.status_code}: {response.text}")
return {
"success": False,
"error_code": ErrorCode.API_ERROR.value,
"message": f"Weather API returned status {response.status_code}"
}
except httpx.TimeoutException:
logger.error(f"Timeout fetching weather for {query.city}")
return {
"success": False,
"error_code": ErrorCode.NETWORK_ERROR.value,
"message": "Request timed out"
}
except Exception as e:
logger.exception(f"Network error: {e}")
return {
"success": False,
"error_code": ErrorCode.NETWORK_ERROR.value,
"message": "Network error occurred"
}
async def run(self):
"""Run the production weather agent"""
await self.initialize()
await self.mcp.run()
async def cleanup(self):
"""Cleanup resources"""
if self.http_client:
await self.http_client.aclose()
if self.redis_client:
await self.redis_client.close()
async def main():
agent = ProductionWeatherAgent()
try:
print("๐ Starting Production Weather Intelligence Agent...")
await agent.run()
finally:
await agent.cleanup()
if __name__ == "__main__":
asyncio.run(main())
Integration with Claude Desktop
Now let's configure your MCP server to work with Claude Desktop for seamless AI interactions:
๐ง Configuration Setup
Add your MCP server to Claude Desktop's configuration:
{
"mcpServers": {
"weather-agent": {
"command": "/absolute/path/to/python",
"args": ["/absolute/path/to/production_weather_agent.py"],
"env": {
"OPENWEATHER_API_KEY": "your_actual_api_key_here",
"REDIS_URL": "redis://localhost:6379"
}
}
}
}
โ
Testing Integration
- Save the configuration to
~/.claude/claude_desktop_config.json
- Restart Claude Desktop application
- Test with: "What's the weather in San Francisco?"
- Verify caching with: "Check the weather in San Francisco again"
Common Pitfalls & Solutions
Learn from common mistakes to build robust MCP agents:
โ Pitfall #1: Blocking Operations
Problem: Using synchronous operations that block the event loop
def bad_api_call():
response = requests.get("https://api.example.com")
return response.json()
async def good_api_call():
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com")
return response.json()
โ ๏ธ Pitfall #2: Poor Error Messages
Problem: Generic errors that don't help users understand what went wrong
return {"error": "Something went wrong"}
return {
"success": False,
"error_code": "INVALID_CITY",
"message": "City 'Atlantis' not found. Please check spelling.",
"suggestions": ["Atlanta", "Atlantic City"],
"help_url": "https://docs.example.com/city-lookup"
}
โ
Best Practice: Comprehensive Validation
Solution: Use Pydantic models for robust input validation
from pydantic import BaseModel, validator
class SafeInput(BaseModel):
command: str
@validator('command')
def validate_command(cls, v):
allowed_commands = ['list', 'search', 'info']
if v not in allowed_commands:
raise ValueError(f'Command must be one of {allowed_commands}')
return v
@mcp.tool()
def safe_tool(command: str):
validated = SafeInput(command=command)
return execute_safe_command(validated.command)
Advanced Patterns & Next Steps
Take your MCP development to the next level with these advanced patterns:
Multi-Tool Coordination
@mcp.tool()
async def comprehensive_weather_report(city: str) -> Dict[str, Any]:
"""Generate comprehensive weather report using multiple data sources"""
current_weather = await get_current_weather_cached(city)
forecast = await get_weather_forecast(city, days=3)
report = {
"city": city,
"current": current_weather,
"forecast": forecast,
"analysis": generate_weather_insights(current_weather, forecast),
"recommendations": generate_activity_recommendations(current_weather)
}
return report
Dynamic Tool Registration
def register_weather_tools(mcp_server, config):
"""Dynamically register tools based on available APIs"""
if config.get("openweather_api_key"):
mcp_server.register_tool("current_weather", get_current_weather)
mcp_server.register_tool("forecast", get_weather_forecast)
if config.get("weather_gov_access"):
mcp_server.register_tool("us_weather", get_us_weather_service)
if config.get("accuweather_key"):
mcp_server.register_tool("detailed_forecast", get_accuweather_data)
Ready to Build Advanced AI Agents?
You've built your first AI agent with MCP! Take the next step and create production-ready agents that transform business operations.
Get Expert MCP Development Support
Custom agent development โข Production deployment โข Performance optimization
Conclusion: Your AI Agent Development Journey
Congratulations! You've successfully built three progressively advanced AI agents using the Model Context Protocol:
- Hello World Agent: Understanding MCP fundamentals and basic tool registration
- Weather Intelligence Agent: Real-world API integration with error handling and validation
- Production-Ready Agent: Enterprise features including caching, monitoring, and security
Key Concepts Mastered
๐ ๏ธ MCP Protocol Mastery
Tools, resources, and JSON-RPC communication patterns
๐ Async Programming
Non-blocking operations and event loop optimization
๐ก๏ธ Production Practices
Error handling, validation, caching, and monitoring
๐ Performance Optimization
Connection pooling, rate limiting, and resource management
Your Next Steps
- Experiment with Custom APIs: Integrate your favorite services and build specialized agents
- Add Advanced Features: Implement machine learning, natural language processing, or computer vision
- Scale to Production: Deploy with Docker, implement monitoring, and add automated testing
- Build Agent Networks: Create multiple agents that coordinate and share information
The Model Context Protocol provides a powerful foundation for AI agent development. With the skills you've learned, you can build agents that transform business processes, enhance user experiences, and unlock new possibilities for AI integration.
Happy coding, and welcome to the future of AI agent development! ๐