Zum Hauptinhalt springen

Building Your First MCP Server: A Weather Forecast Integration

· 12 Minuten Lesezeit

Updated 2026-02-05

Grid banner

In my Agent Skills tutorial, I showed how to build a weather forecast capability using Anthropic's lightweight skills format. Today, we'll build the same weather service as an MCP server—demonstrating the architectural differences and production-ready approach of the Model Context Protocol.

While Agent Skills offer simplicity and filesystem-based discovery, MCP servers provide process isolation, network-level integration, and enterprise-ready deployment patterns. By using the same weather API example, you'll see exactly how these two approaches differ in practice.

What is MCP?

The Model Context Protocol (MCP) is an open standard that enables AI applications to securely access external data sources and tools. Think of it as "USB-C for AI"—a universal interface that lets LLMs discover and interact with your services in a standardized way.

Key Concepts (Spec 2025-11-25)

MCP defines three main capabilities:

  • Tools: Functions the LLM can invoke (like API calls)
  • Resources: Data the LLM can read (like files or API responses)
  • Prompts: Reusable templates for common workflows

Unlike Agent Skills that run in the client's process, MCP servers are independent processes communicating via JSON-RPC 2.0 over:

  • stdio: Local development, spawned by clients
  • HTTP/SSE: Remote servers, cloud deployments

Why MCP for Production?

Comparing to Agent Skills:

AspectAgent SkillsMCP Server
ExecutionIn-process scriptsSeparate process
IsolationShared environmentProcess boundaries
SecurityClient's credentialsScoped credentials
DistributionFilesystem copynpm/PyPI package
TransportLocal filesystemstdio or HTTP
DeploymentClient-managedIndependent service

When to use MCP:

  • Production deployments with SLAs
  • Third-party integrations requiring isolation
  • Multiple clients accessing the same service
  • Organizational security policies
  • Services requiring authentication/authorization

Project Setup

tipp

The complete code for this tutorial is available on GitHub.

We'll build the same weather forecast server from the Agent Skills tutorial, but as a proper MCP server using Python and UV.

Directory Structure

mkdir weather-mcp-server
cd weather-mcp-server
uv init --lib

Dependencies

Update pyproject.toml:

pyproject.toml
[project]
name = "weather-forecast-mcp-server"
version = "1.0.0"
description = "MCP server for weather forecasts using the National Weather Service API"
authors = [
{ name = "Nils Friedrichs" }
]
readme = "README.md"
license = { text = "MIT" }
requires-python = ">=3.10"

dependencies = [
"mcp[cli]>=1.2.0",
"httpx>=0.28.0",
]

[project.urls]
Homepage = "https://friedrichs-it.de/blog/weather-forecast-api-as-stdio-python-mcp-server"
Repository = "https://github.com/slin-master/weather-forecast-mcp-server"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/weather_forecast_mcp_server"]

[tool.hatch.build.targets.sdist]
include = [
"src/weather_forecast_mcp_server",
"README.md",
"pyproject.toml",
]

Install dependencies:

uv sync

Implementing the MCP Server

Create src/weather_forecast_mcp_server/server.py:

src/weather_forecast_mcp_server/server.py
#!/usr/bin/env python3
"""
Weather forecast MCP server using National Weather Service API.
"""
from typing import Any
import httpx
from mcp.server.fastmcp import FastMCP

# Initialize MCP server
mcp = FastMCP("weather-forecast-mcp-server")

# Constants
NWS_API_BASE = "https://api.weather.gov"
NOMINATIM_BASE = "https://nominatim.openstreetmap.org"
USER_AGENT = "WeatherMCPServer/1.0"


@mcp.tool()
async def geocode_city(city_name: str) -> dict[str, Any]:
"""
Convert a city name to geographic coordinates.

Args:
city_name: City name (e.g., "San Francisco, CA" or "Portland, Oregon")

Returns:
Dictionary with lat, lon, and display_name, or error message
"""
async with httpx.AsyncClient() as client:
try:
response = await client.get(
f"{NOMINATIM_BASE}/search",
params={
"q": city_name,
"format": "json",
"limit": 1,
"countrycodes": "us" # NWS is US-only
},
headers={"User-Agent": USER_AGENT},
timeout=10.0
)
response.raise_for_status()

results = response.json()
if not results:
return {"error": f"City not found: {city_name}"}

location = results[0]
return {
"lat": float(location["lat"]),
"lon": float(location["lon"]),
"display_name": location["display_name"]
}
except httpx.HTTPError as e:
return {"error": f"Geocoding failed: {str(e)}"}


@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> dict[str, Any]:
"""
Get weather forecast for specific coordinates.

Fetches current conditions, 7-day forecast, hourly predictions,
and active weather alerts from the National Weather Service.

Args:
latitude: Latitude in decimal degrees
longitude: Longitude in decimal degrees

Returns:
Dictionary containing location info, current conditions,
forecast periods, hourly data, and active alerts
"""
async with httpx.AsyncClient() as client:
try:
# Step 1: Get grid point metadata
points_response = await client.get(
f"{NWS_API_BASE}/points/{latitude},{longitude}",
headers={"User-Agent": USER_AGENT},
timeout=10.0
)

if points_response.status_code == 404:
return {
"error": "Location not covered by National Weather Service. "
"This API only supports US territories."
}

points_response.raise_for_status()
points_data = points_response.json()

# Step 2: Fetch forecast and hourly data
forecast_url = points_data["properties"]["forecast"]
hourly_url = points_data["properties"]["forecastHourly"]

forecast_response = await client.get(
forecast_url,
headers={"User-Agent": USER_AGENT},
timeout=10.0
)
forecast_response.raise_for_status()
forecast_data = forecast_response.json()

hourly_response = await client.get(
hourly_url,
headers={"User-Agent": USER_AGENT},
timeout=10.0
)
hourly_response.raise_for_status()
hourly_data = hourly_response.json()

# Step 3: Check for active alerts
alerts_response = await client.get(
f"{NWS_API_BASE}/alerts/active",
params={"point": f"{latitude},{longitude}"},
headers={"User-Agent": USER_AGENT},
timeout=10.0
)
alerts_response.raise_for_status()
alerts_data = alerts_response.json()

# Extract current period
current_period = forecast_data["properties"]["periods"][0]

return {
"location": {
"lat": latitude,
"lon": longitude,
"city": points_data["properties"]["relativeLocation"]["properties"]["city"],
"state": points_data["properties"]["relativeLocation"]["properties"]["state"]
},
"current": {
"temperature": current_period["temperature"],
"temperatureUnit": current_period["temperatureUnit"],
"windSpeed": current_period["windSpeed"],
"windDirection": current_period["windDirection"],
"shortForecast": current_period["shortForecast"],
"detailedForecast": current_period["detailedForecast"]
},
"forecast": {
"periods": forecast_data["properties"]["periods"][:7]
},
"hourly": {
"periods": hourly_data["properties"]["periods"][:24]
},
"alerts": [
{
"event": alert["properties"]["event"],
"headline": alert["properties"]["headline"],
"severity": alert["properties"]["severity"],
"urgency": alert["properties"]["urgency"]
}
for alert in alerts_data.get("features", [])
]
}

except httpx.HTTPError as e:
return {"error": f"API error: {str(e)}"}
except (KeyError, IndexError) as e:
return {"error": f"Unexpected API response format: {str(e)}"}


@mcp.tool()
async def get_alerts(latitude: float, longitude: float) -> dict[str, Any]:
"""
Get active weather alerts for a location.

Args:
latitude: Latitude in decimal degrees
longitude: Longitude in decimal degrees

Returns:
Dictionary with list of active alerts or error message
"""
async with httpx.AsyncClient() as client:
try:
response = await client.get(
f"{NWS_API_BASE}/alerts/active",
params={"point": f"{latitude},{longitude}"},
headers={"User-Agent": USER_AGENT},
timeout=10.0
)
response.raise_for_status()
alerts_data = response.json()

alerts = [
{
"event": alert["properties"]["event"],
"headline": alert["properties"]["headline"],
"description": alert["properties"]["description"],
"severity": alert["properties"]["severity"],
"urgency": alert["properties"]["urgency"],
"onset": alert["properties"]["onset"],
"expires": alert["properties"]["expires"]
}
for alert in alerts_data.get("features", [])
]

return {
"count": len(alerts),
"alerts": alerts
}

except httpx.HTTPError as e:
return {"error": f"Failed to fetch alerts: {str(e)}"}

Understanding the Code

FastMCP Decorator Pattern:

  • @mcp.tool() automatically registers functions as MCP tools
  • Type hints define the input schema (JSON Schema generation)
  • Docstrings become tool descriptions for the LLM
  • Return values are automatically serialized to JSON

Async/Await:

  • MCP servers use async I/O for efficient request handling
  • httpx.AsyncClient for non-blocking HTTP requests
  • Multiple API calls can run concurrently

Error Handling:

  • Network errors return structured error messages
  • 404 responses provide helpful context (US-only coverage)
  • Try-except blocks prevent server crashes

Testing Locally

Start the Server (stdio)

Using uv run

uv run mcp run src/weather_forecast_mcp_server/server.py

Or after installing

uv pip install -e .
mcp run weather-forecast-mcp-server

The server is now running and waiting for stdio connections.

Testing with MCP Inspector

The MCP Inspector provides a web UI for testing servers:

Install inspector globally

npm install -g @modelcontextprotocol/inspector

Connect to your server

npx @modelcontextprotocol/inspector uv run mcp run src/weather_forecast_mcp_server/server.py

Open http://localhost:5173 in your browser. You should see:

  1. Connect: Server initializes successfully
  2. Tools tab: Three tools listed (geocode_city, get_forecast, get_alerts)
  3. Test tools: Click "geocode_city", enter "Seattle, WA", see results
http://localhost:6274

Using with Claude Desktop

Add to ~/.config/claude/claude_desktop_config.json:

info

Claude Desktop: Settings → Developer → Edit Config

{
"mcpServers": {
"weather": {
"command": "uv",
"args": [
"--directory",
"/absolute/path/to/weather-forecast-mcp-server",
"run",
"mcp",
"run",
"src/weather_forecast_mcp_server/server.py"
]
}
}
}
hinweis

Make sure uv is in your system PATH. You can verify by running which uv in your terminal. Also ensure the absolute path to your server is correct.

Restart Claude Desktop, then ask:

  • "What's the weather in Portland, Oregon?"
  • "Are there any weather alerts in Miami?"

Claude will use your MCP server via stdio transport!

https://claude.ai

Containerizing for Production

While stdio works great locally, production deployments often require Docker for isolation, reproducibility, and security.

Why Docker for MCP Servers?

  1. Process Isolation: Complete separation from host system
  2. Dependency Management: No conflicts with system packages
  3. Security Boundaries: Limit filesystem and network access
  4. Reproducibility: Same environment everywhere (dev/staging/prod)
  5. Easy Deployment: Ship to any Docker host or Kubernetes cluster

Dockerfile

Create Dockerfile:

FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim

# Set working directory
WORKDIR /app

# Copy dependency files first (for layer caching)
COPY pyproject.toml ./
COPY uv.lock ./
COPY README.md ./

# Install dependencies
RUN uv sync --frozen --no-dev

# Copy source code
COPY src ./src

# Create non-root user for security
RUN useradd -m -u 1000 mcpuser && \
chown -R mcpuser:mcpuser /app

USER mcpuser

# Expose port for HTTP transport (if using)
EXPOSE 8000

# Default to stdio transport
CMD ["uv", "run", "mcp", "run", "src/weather_forecast_mcp_server/server.py"]

Docker Compose

For easier management, create docker-compose.yml:

services:
weather-mcp:
build: .
container_name: weather-mcp-server
restart: unless-stopped

# For stdio: not needed (spawn container per request)
# For HTTP: expose port
ports:
- "8000:8000"

# Resource limits
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M

# Security options
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp

# Health check
healthcheck:
test: [ "CMD", "python", "-c", "import sys; sys.exit(0)" ]
interval: 30s
timeout: 10s
retries: 3

Build and Test

Build image

docker compose build

Run container

docker compose up -d

Test stdio locally

docker compose exec weather-mcp uv run mcp run src/weather_forecast_mcp_server/server.py

View logs

docker compose logs -f weather-mcp

Security Best Practices

The Dockerfile implements several security measures:

Non-root user: Runs as mcpuser (UID 1000) ✅ Read-only filesystem: Prevents modification of container files ✅ No new privileges: Disables privilege escalation ✅ Resource limits: CPU and memory constraints ✅ Minimal base image: Reduces attack surface

HTTP Transport (Optional)

For remote access or multi-client scenarios, use HTTP transport:

Update server.py:

if __name__ == "__main__":
import sys

# Check for transport argument
if "--http" in sys.argv:
# HTTP transport on port 8000
mcp.run(transport="http", host="0.0.0.0", port=8000)
else:
# Default stdio transport
mcp.run()

Update Dockerfile CMD:

CMD ["uv", "run", "python", "src/weather_mcp/server.py", "--http"]

Client configuration for HTTP:

{
"mcpServers": {
"weather": {
"url": "http://localhost:8000/mcp",
"transport": {
"type": "http"
}
}
}
}

Comparing to Agent Skills

Let's revisit the architectural differences with our concrete example:

Same Weather API, Different Architecture

Agent Skills (weather-forecast-skill/):

~/.claude/skills/weather-forecast/
├── SKILL.md # Instructions + metadata
├── scripts/
│ ├── geocode.py # Python script (runs in-process)
│ └── get_forecast.py # Python script (runs in-process)
└── references/
└── NWS_API.md # Documentation

MCP Server (weather-mcp-server/):

weather-mcp-server/
├── src/weather_mcp/
│ └── server.py # MCP server (separate process)
├── pyproject.toml # Dependencies + metadata
├── Dockerfile # Container definition
└── docker-compose.yml # Orchestration

Execution Flow Comparison

Agent Skills:

  1. Claude reads SKILL.md from filesystem
  2. Claude spawns subprocess: python scripts/geocode.py "Seattle"
  3. Script runs in Claude's Python environment
  4. Output returned via stdout
  5. Claude parses JSON and uses result

MCP Server:

  1. Claude connects to MCP server (stdio or HTTP)
  2. Claude calls tool: geocode_city(city_name="Seattle")
  3. Server executes in separate process
  4. Result returned via JSON-RPC
  5. Claude receives structured response

Decision Matrix

Use CaseAgent SkillsMCP Server
Personal productivity✅ Perfect⚠️ Overkill
Rapid prototyping✅ Fastest⚠️ More setup
Production SaaS❌ No isolation✅ Production-ready
Multi-tenant❌ Shared env✅ Process isolation
Enterprise deployment❌ Security concerns✅ Containerized
Remote clients❌ Filesystem-bound✅ HTTP transport
Third-party code❌ Trust required✅ Sandboxed

Choose Agent Skills when:

  • Building for personal use
  • Rapid iteration is priority
  • You control all code
  • Filesystem-based discovery is sufficient

Choose MCP Server when:

  • Deploying to production
  • Multiple clients need access
  • Security isolation is required
  • You need HTTP/network transport
  • Containerization is necessary

Next Steps

Now that you've built a production-ready MCP server:

  1. Add authentication: Implement API key validation for HTTP transport
  2. Add caching: Store recent forecasts in Redis to reduce API calls
  3. Add resources: Expose weather history as MCP resources
  4. Add prompts: Create templates for common weather queries
  5. Monitor performance: Add logging and metrics (Prometheus, Grafana)
  6. Publish your server: See our upcoming guide on MCP Registry publishing

Learn More

Conclusion

By building the same weather forecast service as both an Agent Skill and an MCP server, we've seen the architectural trade-offs in practice:

Agent Skills excel at simplicity and rapid development—perfect for personal workflows and trusted code you control.

MCP servers provide production-grade isolation, security boundaries, and deployment flexibility—essential for enterprise use cases and third-party integrations.

The beauty of the ecosystem is that you can start with Agent Skills for prototyping, then graduate to MCP when you need production features. Both approaches are valuable, and understanding when to use each makes you a better AI infrastructure developer.

Ready to dive deeper? Continue with the second part of the tutorial about MCP Server Resources, check out my Agent Skills tutorial to see the other side of this comparison, or read about security architectures to understand the implications of each approach.