The Model Context Protocol (MCP) is rapidly transforming how AI assistants interact with external data and tools. If you want to empower AI with real-world data—like live weather updates—building a custom MCP server is a great project. Here’s a step-by-step guide on setting up a simple weather server using MCP and connecting it with Claude Desktop, with a focus on code quality, architecture, and troubleshooting
Setting Up Your Python Environment
First, set up your Python environment using uv, a tool for dependency management and virtual environments:
curl -LsSf https://astral.sh/uv/install.sh | sh
uv init weather
cd weather
uv venv
source .venv/bin/activate
uv add "mcp[cli]" httpx
touch weather.py
Writing the Weather Server
Your weather.py will house the server’s logic. Start by importing the necessary libraries and initialising the server:
from typing import Any
import httpx
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather")
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"
Making External API Requests Safely
Create a helper function to query the National Weather Service (NWS) API with proper headers and error handling:
async def make_nws_request(url: str) -> dict[str, Any] | None:
headers = {
"User-Agent": USER_AGENT,
"Accept": "application/geo+json"
}
async with httpx.AsyncClient() as client:
try:
response = await client.get(url, headers=headers, timeout=30.0)
response.raise_for_status()
return response.json()
except Exception:
return None
def format_alert(feature: dict) -> str:
"""Format an alert feature into a readable string."""
props = feature["properties"]
return f"""
Event: {props.get('event', 'Unknown')}
Area: {props.get('areaDesc', 'Unknown')}
Severity: {props.get('severity', 'Unknown')}
Description: {props.get('description', 'No description available')}
Instructions: {props.get('instruction', 'No specific instructions provided')}
"""
Defining the Tools
MCP tools expose external capabilities to AI assistants. Here, you’ll define two: one to get weather alerts by state, and another to fetch forecast data by latitude and longitude.
Each tool fetches and formats results, returning friendly, readable output to the host application.
@mcp.tool()
async def get_alerts(state: str) -> str:
"""Get weather alerts for a US state.
Args:
state: Two-letter US state code (e.g. CA, NY)
"""
url = f"{NWS_API_BASE}/alerts/active/area/{state}"
data = await make_nws_request(url)
if not data or "features" not in data:
return "Unable to fetch alerts or no alerts found."
if not data["features"]:
return "No active alerts for this state."
alerts = [format_alert(feature) for feature in data["features"]]
return "\n---\n".join(alerts)
@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
"""Get weather forecast for a location.
Args:
latitude: Latitude of the location
longitude: Longitude of the location
"""
# First get the forecast grid endpoint
points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
points_data = await make_nws_request(points_url)
if not points_data:
return "Unable to fetch forecast data for this location."
# Get the forecast URL from the points response
forecast_url = points_data["properties"]["forecast"]
forecast_data = await make_nws_request(forecast_url)
if not forecast_data:
return "Unable to fetch detailed forecast."
# Format the periods into a readable forecast
periods = forecast_data["properties"]["periods"]
forecasts = []
for period in periods[:5]: # Only show next 5 periods
forecast = f"""
{period['name']}:
Temperature: {period['temperature']}°{period['temperatureUnit']}
Wind: {period['windSpeed']} {period['windDirection']}
Forecast: {period['detailedForecast']}
"""
forecasts.append(forecast)
return "\n---\n".join(forecasts)
Initialise and run the server
if __name__ == "__main__":
mcp.run(transport='stdio')
Connecting to Claude Desktop
After the server is ready, you’ll need to integrate it with Claude Desktop. This involves editing the claude_desktop_config.json file to add your server under the mcpServers key:
{
"mcpServers": {
"weather": {
"command": "/Users/<username>/.local/bin/uv",
"args": [
"--directory",
"/Absolute_path_to_parent_folder/weather",
"run",
"weather.py"
]
}
}
}
Ensure you use the correct absolute paths for your system, and the uv executable is discoverable.
Running and Testing the Server
Start your MCP weather server:
uv run weather.py
The Claude Desktop app should now offer a “Search and tools” icon, indicating that the MCP server is connected. If things don’t work immediately, check the logs located in ~/Library/Logs/Claude. mcp.log and mcp-server-SERVERNAME.log will help debug connection or runtime issues.
Note: Since this is the US National Weather service, the queries will only work for US locations.
Troubleshooting Tips
- Double-check JSON syntax in
claude_desktop_config.json. - Confirm all file paths use correct formatting for your OS.
- Use logs to identify and resolve errors or misconfigurations.
By following these steps, you’ll have a robust weather MCP server that delivers live data to Claude Desktop and can serve as a starting point for other integrations. This opens the door for AI-powered workflows that are contextually rich and responsive to real-world information..