Skip to main content
Glama
README.md
# SMHI MCP Server

A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that provides Swedish weather data through SMHI's open APIs, deployed as a Cloudflare Worker.

[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/yourusername/smhi-mcp)

## 🌀️ Features

- **Real-time Weather Data**: Access current weather conditions from SMHI weather stations
- **Weather Forecasts**: Get detailed forecasts for any location in Sweden
- **Station Search**: Find weather stations by name using fuzzy matching
- **Multi-Resolution Data**: Access hourly, daily, and monthly weather data
- **Historical Data**: Query past weather records with pagination and date filtering
- **MCP Protocol**: Full compatibility with Claude and other MCP clients
- **Serverless**: Runs on Cloudflare Workers with global edge deployment
- **No API Keys Required**: Uses SMHI's free open data APIs
- **Smart Caching**: Multi-level caching with Cloudflare Cache API and R2 storage for optimal performance
- **Rate Limiting**: Built-in request rate limiting to respect SMHI API limits

Forecasts use SMHI's `SNOW1gv1` endpoint. The legacy `PMP3gv2` forecast API was retired by SMHI on `2026-03-31`.

## πŸš€ Live Demo

**Deployed Server:** https://smhi-mcp.hakan-3a6.workers.dev

## πŸ“‹ Available Tools (16 Total)

### Snowmobile Conditions Tools
| Tool | Description | Parameters |
|------|-------------|------------|
| `list_snowmobile_conditions` | List weather stations for snowmobile conditions by region with capability info | None |
| `get_station_temperature` | Get latest temperature reading from a specific station | `station_id` (string) |
| `get_station_snow_depth` | Get latest snow depth reading from a specific station | `station_id` (string) |
| `get_weather_forecast` | Get weather forecast for coordinates | `lat` (number), `lon` (number) |

### Legacy Tools (Deprecated)
| Tool | Description | Parameters |
|------|-------------|------------|
| `list_temperature_stations` | [DEPRECATED] Use `list_snowmobile_conditions` instead | None |
| `list_snow_depth_stations` | [DEPRECATED] Use `list_snowmobile_conditions` instead | None |

### Multi-Resolution Data Tools
| Tool | Description | Parameters |
|------|-------------|------------|
| `get_station_precipitation` | Get precipitation data with multiple resolutions | `station_id`, `parameter` (5=daily, 7=hourly, 14=15min, 23=monthly), `period` |
| `get_temperature_multi_resolution` | Get temperature data with multiple resolutions | `station_id`, `parameter` (1=hourly, 2=daily-mean, 19=min, 20=max, 22=monthly), `period` |
| `get_station_metadata` | Get detailed station metadata and available periods | `station_id`, `parameter` |

### Historical Data & Pagination
| Tool | Description | Parameters |
|------|-------------|------------|
| `get_historical_data` | Get historical data with pagination and date filtering. Reads the CSV archive for `corrected-archive` and the JSON feed for the `latest-*` periods, which SMHI does not publish as CSV | `station_id`, `parameter`, `period`, `limit`, `cursor`, `reverse`, `fromDate`, `toDate` |
| `list_all_temperature_stations` | Get all temperature stations with pagination | `cursor` (optional) |
| `list_all_snow_depth_stations` | Get all snow depth stations with pagination | `cursor` (optional) |
| `list_all_precipitation_stations` | Get all precipitation stations with pagination | `parameter`, `cursor` |

### Station Search Tools
| Tool | Description | Parameters |
|------|-------------|------------|
| `search_stations_by_name` | Search stations by name within a specific parameter type | `query`, `parameter` (1=temp, 5=precip, 8=snow), `limit`, `threshold`, `active_only` |
| `search_stations_by_name_multi_param` | Search stations by name across all parameter types | `query`, `limit`, `threshold`, `active_only` |
| `get_stations_near_location` | Find nearest stations to given coordinates | `latitude`, `longitude`, `parameter`, `radius_km`, `limit`, `active_only` |

#### Search Parameters Details
- **`query`** (required): Station name to search for (fuzzy matching)
- **`parameter`** (search_stations_by_name only): Parameter type filter
  - `1` = Temperature stations
  - `5` = Daily precipitation stations
  - `7` = Hourly precipitation stations
  - `8` = Snow depth stations
- **`limit`** (optional, default: 10): Maximum number of results to return
- **`threshold`** (optional, default: 0.3): Minimum similarity score (0.0-1.0) for fuzzy matching
- **`active_only`** (optional, default: true): Only return stations with recent data activity

#### Coordinate Search Parameters
- **`latitude`** (required): Latitude in WGS84 decimal degrees (e.g., 65.353 for Adak)
- **`longitude`** (required): Longitude in WGS84 decimal degrees (e.g., 18.5837 for Adak)
- **`parameter`** (optional, default: "1"): Parameter type (1=temp, 5=precip, 8=snow)
- **`radius_km`** (optional, default: 50): Maximum search radius in kilometers
- **`limit`** (optional, default: 10): Maximum number of stations to return
- **`active_only`** (optional, default: true): Only return active stations

## πŸ› οΈ Quick Start

### Option 1: Use the Deployed Server

Connect your MCP client to: `https://smhi-mcp.hakan-3a6.workers.dev`

### Option 2: Deploy Your Own

```bash
# Clone the repository
git clone https://github.com/yourusername/smhi-mcp.git
cd smhi-mcp

# Install dependencies
npm install

# Create R2 bucket for caching (required)
wrangler r2 bucket create smhi-historical-data

# Deploy to Cloudflare Workers
npm run deploy
```

### Option 3: Local Development

```bash
# Start local development server
npm run dev

# The server will be available at http://localhost:8787
```

## πŸ“‘ MCP Protocol Examples

### Initialize Connection
```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {}
  }
}
```

### List Available Tools
```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list"
}
```

### Get Weather Forecast (Stockholm)
```json
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "get_weather_forecast",
    "arguments": {
      "lat": 59.3293,
      "lon": 18.0686
    }
  }
}
```

### List Snowmobile Conditions Stations
```json
{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tools/call",
  "params": {
    "name": "list_snowmobile_conditions",
    "arguments": {}
  }
}
```

### Get Station Temperature
```json
{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "tools/call",
  "params": {
    "name": "get_station_temperature",
    "arguments": {
      "station_id": "159880"
    }
  }
}
```

### Search for Weather Stations
```json
{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "tools/call",
  "params": {
    "name": "search_stations_by_name_multi_param",
    "arguments": {
      "query": "Stockholm",
      "limit": 5,
      "threshold": 0.3,
      "active_only": true
    }
  }
}
```

### Find Nearest Stations by Coordinates
```json
{
  "jsonrpc": "2.0",
  "id": 6,
  "method": "tools/call",
  "params": {
    "name": "get_stations_near_location",
    "arguments": {
      "latitude": 65.353,
      "longitude": 18.5837,
      "parameter": "1",
      "radius_km": 50,
      "limit": 5
    }
  }
}
```

### Get Historical Data with Pagination
```json
{
  "jsonrpc": "2.0",
  "id": 6,
  "method": "tools/call",
  "params": {
    "name": "get_historical_data",
    "arguments": {
      "station_id": "159880",
      "parameter": "1",
      "period": "corrected-archive",
      "limit": 20,
      "fromDate": "2024-01-01",
      "toDate": "2024-01-31"
    }
  }
}
```

## πŸ§ͺ Testing

### Using cURL

```bash
# Test MCP initialization
curl -X POST -H "Content-Type: application/json" \
  -d '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}}}' \
  https://smhi-mcp.hakan-3a6.workers.dev

# List snowmobile conditions stations
curl -X POST -H "Content-Type: application/json" \
  -d '{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "list_snowmobile_conditions", "arguments": {}}}' \
  https://smhi-mcp.hakan-3a6.workers.dev

# Test weather forecast for Stockholm
curl -X POST -H "Content-Type: application/json" \
  -d '{"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"name": "get_weather_forecast", "arguments": {"lat": 59.3293, "lon": 18.0686}}}' \
  https://smhi-mcp.hakan-3a6.workers.dev

# Search for weather stations
curl -X POST -H "Content-Type: application/json" \
  -d '{"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"name": "search_stations_by_name_multi_param", "arguments": {"query": "Stockholm", "limit": 3}}}' \
  https://smhi-mcp.hakan-3a6.workers.dev

# Find nearest stations to coordinates (Adak area)
curl -X POST -H "Content-Type: application/json" \
  -d '{"jsonrpc": "2.0", "id": 6, "method": "tools/call", "params": {"name": "get_stations_near_location", "arguments": {"latitude": 65.353, "longitude": 18.5837, "radius_km": 50}}}' \
  https://smhi-mcp.hakan-3a6.workers.dev
```

### Using Make Commands

```bash
# Run built-in tests
make test-mcp
make test-forecast
make test-stations
```

### Unit Tests

```bash
npm test          # vitest run
```

Covers transport behaviour (`test/mcp-transport.test.js`) and request
accounting (`test/request-limits.test.js`). Both exist because of a real
incident β€” see below.

## πŸ›‘οΈ Protecting the request quota

### The 2026-08 runaway incident

`GET /mcp` used to answer with a **finite** `text/event-stream` body: one
`data:` line, then the stream closed. `EventSource` treats a closed stream as a
dropped connection and reconnects after ~1s, forever. Three concurrent clients
doing that generated ~420,000 Worker requests in four days against a
100,000/day account limit, and tripped Cloudflare's daily limit email.

The fix: a server with no server-initiated messages must answer `405` on GET,
per the MCP Streamable HTTP spec. Conforming clients then stop retrying.
`test/mcp-transport.test.js` locks this in.

### What the in-Worker limiter can and cannot do

`checkRequestLimits()` now runs for **every** method, not just POST (it
previously sat in the POST branch only, which is why the GET flood went
unnoticed). But be clear about its scope:

> **An in-Worker limiter cannot protect the Cloudflare request quota.** By the
> time it runs, the request is already billed. A 429 costs exactly as much
> quota as a 200.

Its real value is shedding load off the upstream SMHI APIs and failing loudly
rather than silently. It is also backed by `caches.default`, which is
**per-colo**, so the count is per-edge-location, not global.

### Actually protecting the quota

Requests must be rejected *before* they reach the Worker:

1. **Put the service on a real zone** (not `*.workers.dev`) and add a WAF
   rate-limiting rule. Traffic blocked at the WAF is not billed as a Worker
   invocation.
2. **Disable the `workers.dev` route** once a custom domain is in place, so the
   unprotected hostname stops being reachable.
3. Note that the endpoint currently has **no authentication** β€” anyone with the
   URL can invoke it.

## πŸ—οΈ Architecture

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   MCP Client    β”‚    β”‚  Cloudflare     β”‚    β”‚   SMHI APIs     β”‚
β”‚   (Claude)      │◄──►│    Worker       │◄──►│  (opendata)     β”‚
β”‚                 β”‚    β”‚  (smhi-mcp)     β”‚    β”‚                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

The server acts as a bridge between MCP clients and SMHI's weather APIs:

1. **MCP Client** sends JSON-RPC requests
2. **Cloudflare Worker** processes requests and calls SMHI APIs
3. **SMHI APIs** provide real-time weather data
4. **Worker** formats response according to MCP protocol

## ⚑ Performance & Caching

The server implements a multi-level caching strategy for optimal performance:

### Cloudflare Cache API
- **Current weather data**: 15 minutes TTL
- **Weather forecasts**: 30 minutes TTL  
- **Station metadata**: 1 week TTL
- **Historical data**: 24 hours TTL

### R2 Storage (CSV Data)
- **Standard CSV files**: 24 hours TTL
- **Recent year data**: 1 hour TTL (more volatile)
- **Historical data (>2 years)**: 1 week TTL (stable)

### Rate Limiting
- **30 requests per minute** maximum to respect SMHI API limits
- Automatic request throttling with user-friendly error messages

### Configuration Requirements
```toml
# wrangler.toml - R2 bucket for CSV caching
[[r2_buckets]]
binding = "HISTORICAL_DATA"
bucket_name = "smhi-historical-data"
```

## πŸ“Š Data Sources

All weather data comes from [SMHI (Swedish Meteorological and Hydrological Institute)](https://www.smhi.se/):

- **Observations API**: Real-time weather station data
- **Forecast API**: Weather predictions up to 10 days
- **Station Metadata**: Information about monitoring stations

### Sample Weather Stations

| Station ID | Name | Location | Note |
|------------|------|----------|------|
| 159880 | Arvidsjaur | Northern Sweden | Temperature & precipitation |
| 155960 | TΓ€rnaby/Hemavan | Mountain region | Temperature data |
| 155940 | MosekΓ€lla/Hemavan | Mountain region | Same location, different parameter names |
| 132170 | Storlien-Storvallen | Norwegian border | Alpine weather station |
| 188850 | KatterjΓ₯kk/RiksgrΓ€nsen | Arctic region | Northernmost station |

**Note**: Some stations have different names depending on the parameter type. Use `search_stations_by_name_multi_param` to find stations across all data types.

**Active Filtering**: By default, search tools only return stations with recent data activity. Set `active_only: false` to include all stations, including inactive ones.

## πŸ”§ Configuration

### Required Configuration

The worker requires an R2 bucket for CSV caching:

```toml
# wrangler.toml - Required R2 configuration
[[r2_buckets]]
binding = "HISTORICAL_DATA" 
bucket_name = "smhi-historical-data"
```

### Optional Environment Variables

```toml
# wrangler.toml - Optional configuration
[env.production.vars]
# Add public configuration here

# Optional secrets for authentication
[[env.production.secrets]]
API_KEY = "your-api-key-for-auth"
```

### Authentication (Optional)

To enable API key authentication, set the `API_KEY` secret:

```bash
wrangler secret put API_KEY
```

## πŸ“‹ Development Commands

```bash
# Install dependencies
make install

# Start development server
make dev

# Deploy to Cloudflare
make deploy

# View logs
make logs

# Show deployment status
make status

# Run tests
make test-mcp
```

### Modular Architecture

The codebase has been refactored into a modular structure:

```
src/
β”œβ”€β”€ api/          # SMHI API integration
β”œβ”€β”€ config/       # Constants and configuration
β”œβ”€β”€ data/         # Station data and presets
β”œβ”€β”€ handlers/     # WebSocket and SSE handlers
β”œβ”€β”€ mcp/          # MCP protocol server
β”œβ”€β”€ middleware/   # Rate limiting and middleware
β”œβ”€β”€ services/     # Weather data services
β”œβ”€β”€ tools/        # MCP tool definitions
└── utils/        # Caching and utility functions
```

## 🌍 Claude Integration

To use this server with Claude:

1. **Desktop App**: Add the server URL in Claude's MCP settings
2. **API**: Include the server in your MCP client configuration
3. **Custom Integration**: Use the JSON-RPC protocol directly

Example Claude conversations:
```
You: "Show me weather stations for snowmobile conditions"
Claude: "Let me get the snowmobile conditions monitoring stations for you."
[Uses list_snowmobile_conditions]
Claude: "Here are 18 weather stations organized by region for snowmobile conditions. The Arctic/Mountain region has KatterjΓ₯kk/RiksgrΓ€nsen with both temperature and snow depth data..."

You: "What's the current temperature in Arvidsjaur?"
Claude: "Let me check the current temperature in Arvidsjaur for you."
[Uses get_station_temperature with station_id "159880"]
Claude: "The current temperature in Arvidsjaur is 15.8Β°C."

You: "Find weather stations near Stockholm"
Claude: "Let me search for weather stations near Stockholm."
[Uses search_stations_by_name_multi_param with query "Stockholm"]
Claude: "I found several weather stations near Stockholm, including Stockholm A (98230) and Stockholm-Observatoriekullen (98210)."

You: "Show me temperature data for MosekΓ€lla from January 2024"
Claude: "Let me search for MosekΓ€lla and get the temperature data."
[Uses search_stations_by_name_multi_param with query "MosekΓ€lla"]
[Uses get_historical_data with station_id "155940", fromDate "2024-01-01", toDate "2024-01-31"]
Claude: "Found MosekΓ€lla (station 155940) - here's the temperature data for January 2024..."

You: "Find weather stations near Adak (65.353Β°N, 18.5837Β°E)"
Claude: "Let me find the nearest weather stations to those coordinates."
[Uses get_stations_near_location with latitude 65.353, longitude 18.5837, radius_km 50]
Claude: "I found 4 temperature stations within 50km: MalΓ₯ (20.5km), MalΓ₯-BrΓ€nnan A (22.4km), BuresjΓΆn A (40.6km), and Arvidsjaur A (41.3km)."
```

## 🀝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

### Development Setup

1. Fork the repository
2. Create a feature branch: `git checkout -b feature/amazing-feature`
3. Make your changes
4. Test locally: `npm run dev`
5. Deploy and test: `npm run deploy`
6. Submit a pull request

## πŸ“„ License

This project is open source and available under the [MIT License](LICENSE).

## πŸ”— Related Projects

- [Model Context Protocol](https://modelcontextprotocol.io/) - The protocol specification
- [SMHI Open Data](https://www.smhi.se/data/meteorologi/ladda-ner-meteorologiska-observationer) - Weather data source
- [Cloudflare Workers](https://workers.cloudflare.com/) - Serverless platform

## πŸ“ž Support

- **Issues**: [GitHub Issues](https://github.com/yourusername/smhi-mcp/issues)
- **Discussions**: [GitHub Discussions](https://github.com/yourusername/smhi-mcp/discussions)
- **SMHI API Docs**: [SMHI Open Data Portal](https://www.smhi.se/data)

---

Built with ❀️ for the MCP ecosystem | Powered by SMHI Open Data | Deployed on Cloudflare Workers

Maintenance

ActivityMaintained
ResponsivenessNo issues