Building Weather Tools with MCP and FastMCP
A Complete Beginner’s Guide to Creating MCP Weather Services
From Setup to Working MCP Tools: Your First Weather Service
Author: Aaron Borgi
Email: [email protected]
Company: THINKOFIT
2025-11-07
Introduction
Welcome to the world of MCP (Model Context Protocol) applications! In this comprehensive guide, we’ll build a weather service that provides current weather and daily forecasts for any location in the UK using FastMCP and Python. This tutorial is designed for beginners who want to understand how to create tools that can be used by AI assistants and other applications.
By the end of this walkthrough, you’ll have:
Built a professional MCP server using FastMCP
Created tools that fetch real-time weather data
Learned about modern Python package management with uv
Understood how MCP enables AI tool integration
Created a testable, maintainable codebase
MCP (Model Context Protocol) is a revolutionary way to extend AI capabilities by providing external tools and data sources. FastMCP makes it incredibly easy to create these tools using Python, allowing AI assistants to access real-world data like weather information.
Understanding MCP and FastMCP
What is MCP?
MCP (Model Context Protocol) is a standardized way to connect AI models with external tools and data sources. Think of it as a bridge that allows AI assistants to:
Access real-time information
Perform actions in external systems
Use specialized tools and services
Extend their capabilities beyond their training data
Why FastMCP?
FastMCP is a Python library that makes building MCP servers incredibly simple. It provides:
-
Decorator-based tool creation: Define tools with simple Python functions
-
Automatic validation: Built-in parameter validation and error handling
-
Easy testing: Built-in client for testing your tools
-
Standard compliance: Follows MCP specifications exactly
Setting Up Your Development Environment with uv
Why Choose uv Over pip?
Before we start coding, let’s talk about uv - a modern Python package manager that’s revolutionizing how we handle Python projects.
uv vs pip - The Key Differences:
Speed:
uv is 10-100x faster than pip for installing packages
Uses Rust under the hood for maximum performance
Parallel downloads and installations
Smart caching reduces redundant downloads
Dependency Resolution:
Advanced dependency resolver prevents conflicts
Deterministic installs - same result every time
Better handling of complex dependency trees
Clearer error messages when conflicts occur
Project Management:
Built-in virtual environment management
Automatic Python version management
Project-based dependency tracking
Lock file generation for reproducible builds
Modern Features:
Compatible with pyproject.toml
Built-in formatter and linter integration
Cross-platform consistency
Better security with package verification
Installing uv
Installing uv is straightforward on all platforms:
On macOS and Linux:
curl -LsSf https://astral.sh/uv/install.sh | sh
On Windows:
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
Alternative installation methods:
With pip:
pip install uv-
With conda:
conda install -c conda-forge uv -
With homebrew:
brew install uv
Creating Your Project
Now let’s create our weather MCP project:
# Create a new project
uv init uk-weather-mcp
cd uk-weather-mcp
# Add required dependencies
uv add mcp fastmcp httpx
# Create our main files
touch main.py
touch test.py
What uv did for us:
Created a new project directory with proper structure
Initialized a virtual environment automatically
Created pyproject.toml for dependency management
Added fastmcp and httpx to our dependencies
Generated a lock file for reproducible installs
Understanding the Weather APIs
Open-Meteo API
Our weather service uses the Open-Meteo API, which provides:
Free access: No API key required
-
Global coverage: Weather data for any location
-
Multiple endpoints: Geocoding and weather forecasting
-
High accuracy: Professional-grade weather data
Two main endpoints we’ll use:
1. Geocoding API:
-
URL:
https://geocoding-api.open-meteo.com/v1/search Purpose: Convert city names to coordinates
Input: City name or postcode
Output: Latitude and longitude
2. Forecast API:
URL:
https://api.open-meteo.com/v1/forecastPurpose: Get weather data for coordinates
Input: Latitude, longitude, and parameters
Output: Weather data in JSON format
Building the MCP Server
Project Structure
Our simple but effective project structure:
uk-weather-mcp/
├── main.py # MCP server implementation
├── test.py # Test client
├── pyproject.toml # Project configuration
├── uv.lock # Dependency lock file
└── .python-version # Python version specification
Creating the Main Server
Let’s build our main.py file step by step:
from mcp.server.fastmcp import FastMCP
import httpx
# Initialize our MCP server
mcp = FastMCP("uk-weather")
# API endpoints
API_GEO = "https://geocoding-api.open-meteo.com/v1/search"
API_FORECAST = "https://api.open-meteo.com/v1/forecast"
UK_TZ = "Europe/London"
async def coords_for(city: str) -> tuple[float, float]:
"""Look up lat/lon for a city or UK postcode (first match)."""
params = {"name": city, "count": 1}
async with httpx.AsyncClient(timeout=10) as client:
response = await client.get(API_GEO, params=params)
response.raise_for_status()
item = response.json()["results"][0]
return item["latitude"], item["longitude"]
@mcp.tool()
async def current_weather(city: str) -> dict:
"""Get current weather for a UK city or postcode."""
lat, lon = await coords_for(city=city)
params = {
"latitude": lat,
"longitude": lon,
"current_weather": "true",
"timezone": UK_TZ,
}
async with httpx.AsyncClient(timeout=10) as client:
response = await client.get(API_FORECAST, params=params)
response.raise_for_status()
return response.json()["current_weather"]
@mcp.tool()
async def daily_forecast(city: str, days: int = 5) -> dict:
"""Get daily weather forecast for a UK city or postcode."""
lat, lon = await coords_for(city=city)
params = {
"latitude": lat,
"longitude": lon,
"timezone": UK_TZ,
"daily": "temperature_2m_max,temperature_2m_min,precipitation_probability_max",
"forecast_days": days,
}
async with httpx.AsyncClient(timeout=10) as client:
response = await client.get(API_FORECAST, params=params)
response.raise_for_status()
return response.json()["daily"]
if __name__ == "__main__":
mcp.run(transport="stdio")
Breaking Down the Code
1. Imports and Setup:
from mcp.server.fastmcp import FastMCP
import httpx
mcp = FastMCP("uk-weather")
-
FastMCP: The main class for creating MCP servers
-
httpx: Modern async HTTP client for API calls
-
FastMCP("uk-weather"): Creates our server with a descriptive name
2. API Configuration:
API_GEO = "https://geocoding-api.open-meteo.com/v1/search"
API_FORECAST = "https://api.open-meteo.com/v1/forecast"
UK_TZ = "Europe/London"
-
API_GEO: Endpoint for converting city names to coordinates
API_FORECAST: Endpoint for weather data
-
UK_TZ: Timezone setting for consistent UK time
3. Coordinate Lookup Function:
async def coords_for(city: str) -> tuple[float, float]:
"""Look up lat/lon for a city or UK postcode (first match)."""
params = {"name": city, "count": 1}
async with httpx.AsyncClient(timeout=10) as client:
response = await client.get(API_GEO, params=params)
response.raise_for_status()
item = response.json()["results"][0]
return item["latitude"], item["longitude"]
Why this function is important:
-
Abstraction: Hides coordinate lookup complexity
-
Reusability: Both weather functions use this
-
Error handling:
raise_for_status()catches HTTP errors -
Timeout protection: Prevents hanging requests
4. Current Weather Tool:
@mcp.tool()
async def current_weather(city: str) -> dict:
"""Get current weather for a UK city or postcode."""
lat, lon = await coords_for(city=city)
params = {
"latitude": lat,
"longitude": lon,
"current_weather": "true",
"timezone": UK_TZ,
}
async with httpx.AsyncClient(timeout=10) as client:
response = await client.get(API_FORECAST, params=params)
response.raise_for_status()
return response.json()["current_weather"]
Key features:
-
@mcp.tool(): Decorator that registers this as an MCP tool
-
Type hints: Clear input/output types for validation
-
Docstring: Provides tool description for AI assistants
-
Current weather data: Real-time temperature, conditions, etc.
5. Daily Forecast Tool:
@mcp.tool()
async def daily_forecast(city: str, days: int = 5) -> dict:
"""Get daily weather forecast for a UK city or postcode."""
lat, lon = await coords_for(city=city)
params = {
"latitude": lat,
"longitude": lon,
"timezone": UK_TZ,
"daily": "temperature_2m_max,temperature_2m_min,precipitation_probability_max",
"forecast_days": days,
}
async with httpx.AsyncClient(timeout=10) as client:
response = await client.get(API_FORECAST, params=params)
response.raise_for_status()
return response.json()["daily"]
Advanced features:
-
Default parameter:
days=5provides sensible default -
Flexible data: Max/min temperatures and precipitation
-
Configurable range: User can specify number of days
-
Structured output: Returns organized daily data
Understanding Async Programming
You’ll notice async and await throughout our code. This is crucial for MCP servers:
Why async matters:
-
Non-blocking: Server can handle multiple requests simultaneously
-
Efficient: Doesn’t waste CPU while waiting for API responses
Scalable: Better performance under load
-
MCP requirement: MCP protocol expects async operations
Async patterns in our code:
# Async function definition
async def coords_for(city: str) -> tuple[float, float]:
# Async HTTP client
async with httpx.AsyncClient(timeout=10) as client:
# Awaiting async operations
response = await client.get(API_GEO, params=params)
lat, lon = await coords_for(city=city)
Creating the Test Client
Understanding the Test Structure
Testing is crucial for ensuring our MCP server works correctly. Let’s create test.py:
from fastmcp import Client
import asyncio
async def main() -> None:
"""Test our MCP server by connecting via stdio."""
# Connect via stdio to a local script
async with Client("main.py") as client:
# List available tools
tools = await client.list_tools()
print(f"Available tools: {tools}")
# Test current weather
result = await client.call_tool("current_weather", {"city": "London"})
print(f"Current weather result: {result.content[0].text}")
# Test daily forecast
result = await client.call_tool("daily_forecast", {"city": "Manchester", "days": 3})
print(f"Daily forecast result: {result.content[0].text}")
if __name__ == "__main__":
asyncio.run(main())
Breaking Down the Test Code
1. Test Setup:
from fastmcp import Client
import asyncio
async def main() -> None:
async with Client("main.py") as client:
Client: FastMCP’s built-in test client
asyncio: Python’s async event loop
-
Context manager: Ensures proper connection cleanup
-
stdio connection: Connects to our server via standard input/output
2. Tool Discovery:
tools = await client.list_tools()
print(f"Available tools: {tools}")
-
list_tools(): Discovers all available MCP tools
-
Introspection: Lets us see what our server provides
-
Debugging: Helps verify our tools are registered correctly
3. Testing Individual Tools:
# Test current weather
result = await client.call_tool("current_weather", {"city": "London"})
print(f"Current weather result: {result.content[0].text}")
# Test daily forecast
result = await client.call_tool("daily_forecast", {"city": "Manchester", "days": 3})
print(f"Daily forecast result: {result.content[0].text}")
call_tool(): Invokes specific MCP tools
Parameters: Passed as a dictionary
-
Result handling: Extracts text from MCP response format
-
Multiple tests: Verifies different tools and parameters
Running Your MCP Server
Starting the Server
To test your MCP server, run the test client:
uv run test.py
What happens when you run this:
uv automatically activates the virtual environment
The test client launches your MCP server
Server registers the two weather tools
Client tests both tools with sample data
Results are printed to the console
Expected Output
You should see output similar to:
Available tools: [
{
'name': 'current_weather',
'description': 'Get current weather for a UK city or postcode.',
'inputSchema': {'type': 'object', 'properties': {'city': {'type': 'string'}}}
},
{
'name': 'daily_forecast',
'description': 'Get daily weather forecast for a UK city or postcode.',
'inputSchema': {'type': 'object', 'properties': {'city': {'type': 'string'}, 'days': {'type': 'integer'}}}
}
]
Current weather result: {
"temperature": 15.2,
"windspeed": 8.4,
"winddirection": 230,
"weathercode": 1,
"is_day": 1,
"time": "2024-01-15T14:30"
}
Daily forecast result: {
"time": ["2024-01-15", "2024-01-16", "2024-01-17"],
"temperature_2m_max": [16.1, 14.8, 12.3],
"temperature_2m_min": [8.2, 7.1, 5.9],
"precipitation_probability_max": [20, 45, 80]
}
Understanding the Output
Tool Registration:
Both tools are properly registered with descriptions
-
Input schemas are automatically generated from function signatures
FastMCP handles all the MCP protocol details
Weather Data Structure:
-
Current weather: Temperature, wind, weather codes, time
-
Daily forecast: Arrays of daily values over multiple days
-
Real-time data: Actual weather from Open-Meteo API
Understanding MCP Tool Integration
How AI Assistants Use Your Tools
When your MCP server is connected to an AI assistant, here’s what happens:
1. Tool Discovery:
-
AI assistant calls
list_tools()to see available tools Reads tool descriptions and input schemas
Understands what each tool can do
2. Intelligent Tool Selection:
User asks: "What’s the weather like in Birmingham?"
AI recognizes this needs weather data
Selects
current_weathertoolCalls tool with
{"city": "Birmingham"}
3. Response Integration:
Receives structured weather data
Interprets the data (temperature, conditions, etc.)
Provides natural language response to user
May combine with other tools for richer answers
Why This Architecture Works
Separation of Concerns:
-
AI Assistant: Handles conversation and reasoning
-
MCP Server: Provides specialized data and tools
APIs: Supply real-time information
Flexibility:
Multiple AI assistants can use the same MCP server
Easy to add new tools without changing AI code
Tools can be developed and tested independently
Advanced Features and Error Handling
Built-in Error Handling
Our code includes several error handling mechanisms:
HTTP Error Handling:
response.raise_for_status() # Raises exception for HTTP errors
Timeout Protection:
async with httpx.AsyncClient(timeout=10) as client:
What happens when things go wrong:
-
Invalid city: API returns empty results, causing IndexError
-
Network issues: httpx raises connection errors
-
API downtime: HTTP status errors are caught
-
Timeout: Request cancelled after 10 seconds
Understanding the Weather Data
Current Weather Fields:
-
temperature: Current temperature in Celsius
windspeed: Wind speed in km/h
-
winddirection: Wind direction in degrees
-
weathercode: Numeric weather condition code
-
is_day: Whether it’s daytime (1) or nighttime (0)
time: Timestamp of the reading
Daily Forecast Fields:
time: Array of dates
-
temperature_2m_max: Daily maximum temperatures
-
temperature_2m_min: Daily minimum temperatures
-
precipitation_probability_max: Chance of rain (%)
Using Your MCP Server
Connecting to AI Assistants
Once your MCP server is working, you can connect it to AI assistants that support MCP:
Common connection methods:
-
stdio: Direct connection via standard input/output
HTTP: Web-based API endpoint
-
WebSocket: Real-time bidirectional communication
Our server uses stdio:
if __name__ == "__main__":
mcp.run(transport="stdio")
Configure Claude to Use MCP
Open the Claude desktop application
-
Go to Settings (usually found in the menu or profile section in the top left corner)
Look for MCP or Extensions settings
Click on File - Settings
Click on Developer - Edit Config
-
Configure the MCP Client to connect to your local server by adding this code snippet:
-
{ "mcpServers": { "uk-weather": { "command": "uv", "args": [ "--directory", "C:\\ABSOLUTE_PATH\\TO\\YOUR\\PROJECT\\DIRECTORY", "run", "main.py" ] }, } } Save the settings
Real-World Usage Examples
Example conversations with AI assistants:
User: "What’s the weather like in London today?"
AI: Calls current_weather("London") and responds with current conditions.
User: "Should I plan a picnic in Manchester this weekend?" AI: Calls
daily_forecast("Manchester", 7) and analyzes the forecast.
User: "Compare the weather in Edinburgh and Cardiff" AI: Calls both tools for different cities and provides comparison.
Project Management with uv
Understanding pyproject.toml
uv automatically created a pyproject.toml file for our project:
[project]
name = "uk-weather-mcp"
version = "0.1.0"
description = "MCP server for UK weather data"
dependencies = [
"fastmcp>=0.1.0",
"httpx>=0.25.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Key sections:
-
[project]: Project metadata and dependencies
dependencies: Runtime requirements
-
[build-system]: How to build the project
Useful uv Commands
Dependency management:
# Add a new dependency
uv add requests
# Remove a dependency
uv remove requests
# Update all dependencies
uv sync
# Show dependency tree
uv tree
# Run scripts
uv run main.py
uv run test.py
Conclusion
Congratulations! You’ve successfully built a professional MCP server that provides weather tools for AI assistants. In this tutorial, you learned:
How to set up a modern Python project using uv
How to create MCP tools using FastMCP
How to integrate external APIs for real-time data
How to test MCP servers with built-in clients
-
How async programming enables efficient network operations
Your weather MCP server demonstrates the power of the Model Context Protocol in extending AI capabilities with real-world data. The tools you’ve created can be used by any MCP-compatible AI assistant to provide current weather information and forecasts for UK locations.
The modular architecture you’ve built makes it easy to extend with additional weather features, support for other countries, or entirely different types of tools. Whether you’re building weather services, data analysis tools, or system integrations, the patterns you’ve learned here will serve as a strong foundation.
Keep building, keep experimenting—and welcome to the future of AI
Happy coding!
Author: Aaron Borgi
Email: [email protected]
Company: THINKOFIT
Date: 2025-11-07