Weather MCP Server
A Model Context Protocol server that provides real-time weather data, secured with AWS Cognito OAuth 2.1 Bearer token authentication.
Implements the full MCP Authorization Specification (2025-11-25):
- RFC 9728 — Protected Resource Metadata (PRM) discovery
- RFC 6750 — Bearer token usage
- RFC 7591 — Dynamic Client Registration (DCR) bridged to Cognito
Architecture
Client / AI Agent
│
├─ GET /.well-known/oauth-protected-resource → discover auth server
├─ POST /register → dynamic client registration (optional)
├─ POST Cognito /oauth2/token → exchange credentials for JWT
└─ POST /mcp Authorization: Bearer <token> → call MCP tools
Weather data is sourced from Open-Meteo — free, no API key required.
Project Structure
weather-mcp/
├── weather_mcp/
│ ├── __init__.py
│ ├── config.py # All env var loading (COGNITO_*, SERVER_URL)
│ ├── auth.py # JWT validation, middleware, PRM + DCR handlers
│ ├── tools.py # MCP instance + weather tools
│ └── main.py # Starlette app factory + uvicorn entrypoint
├── infra/
│ └── cognito.yaml # CloudFormation — Cognito User Pool, App Client
├── pyproject.toml
├── Dockerfile
├── .env.example
└── README.md
Prerequisites
- Python 3.13+ and uv
- AWS account with CLI configured (
aws configure) - Docker (optional, for containerised deployment)
Quick Start
1 — Deploy AWS Cognito
aws cloudformation deploy \
--template-file infra/cognito.yaml \
--stack-name weather-mcp \
--region us-east-1 \
--capabilities CAPABILITY_NAMED_IAM
Get the output values:
aws cloudformation describe-stacks \
--stack-name weather-mcp \
--query "Stacks[0].Outputs" \
--output table
2 — Configure environment
cp .env.example .env
# Fill in the values from the CloudFormation Outputs
3 — Run
Locally:
uv sync
uv run python -m weather_mcp.main
Docker:
docker build -t weather-mcp:local .
docker run --env-file .env -p 8000:8000 weather-mcp:local
Server starts at http://localhost:8000.
API Endpoints
| Endpoint | Auth | Description |
|---|---|---|
GET /health | None | Health check |
GET /.well-known/oauth-protected-resource | None | RFC 9728 discovery document |
POST /register | None | RFC 7591 Dynamic Client Registration |
POST /mcp | Bearer token | MCP tools (streamable HTTP) |
MCP Tools
| Tool | Description |
|---|---|
get_current_weather | Current weather for any location by latitude/longitude |
Usage
Option A — Static credentials (pre-registered client)
# 1. Get a token
TOKEN=$(curl -s -X POST \
https://weather-mcp-auth.auth.us-east-1.amazoncognito.com/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&scope=weather-mcp/read" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
# 2. List tools
curl -s -X POST http://localhost:8000/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
# 3. Call the weather tool
curl -s -X POST http://localhost:8000/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"get_current_weather","arguments":{"latitude":37.77,"longitude":-122.42}}}'
Option B — Dynamic Client Registration (zero pre-configuration)
# 1. Register a new client
CREDS=$(curl -s -X POST http://localhost:8000/register \
-H "Content-Type: application/json" \
-d '{"client_name":"my-agent","grant_types":["client_credentials"],"scope":"weather-mcp/read"}')
CLIENT_ID=$(echo $CREDS | python3 -c "import sys,json; print(json.load(sys.stdin)['client_id'])")
CLIENT_SECRET=$(echo $CREDS | python3 -c "import sys,json; print(json.load(sys.stdin)['client_secret'])")
# 2. Get a token with the new client
TOKEN=$(curl -s -X POST \
https://weather-mcp-auth.auth.us-east-1.amazoncognito.com/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&scope=weather-mcp/read" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
# 3. Call MCP
curl -s -X POST http://localhost:8000/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
Environment Variables
| Variable | Description |
|---|---|
COGNITO_REGION | AWS region (e.g. us-east-1) |
COGNITO_USER_POOL_ID | Cognito User Pool ID |
COGNITO_APP_CLIENT_ID | App Client ID for token audience validation |
COGNITO_DOMAIN_PREFIX | Hosted-UI domain prefix |
SERVER_URL | Public URL of this server (default: http://localhost:8000) |
How Authentication Works
- A request arrives at
/mcpwithout a token → server responds with401and aWWW-Authenticateheader pointing to/.well-known/oauth-protected-resource - The client fetches the discovery document to find the Cognito authorization server
- The client obtains a JWT access token from Cognito (via
client_credentialsor Dynamic Client Registration) - The client includes
Authorization: Bearer <token>on subsequent requests - The middleware validates the JWT signature against Cognito's JWKS endpoint (RS256, cached 1 hour)