{
  "markdown": "# 🌦️ meteoswiss-mcp\n\n[![CI](https://github.com/malkreide/meteoswiss-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/malkreide/meteoswiss-mcp/actions/workflows/ci.yml)\n[![PyPI](https://img.shields.io/pypi/v/meteoswiss-mcp)](https://pypi.org/project/meteoswiss-mcp/)\n[![Python](https://img.shields.io/pypi/pyversions/meteoswiss-mcp)](https://pypi.org/project/meteoswiss-mcp/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n[![swiss-public-data-mcp](https://img.shields.io/badge/portfolio-swiss--public--data--mcp-blue)](https://github.com/malkreide/swiss-public-data-mcp)\n\n**MCP server for Swiss weather and climate data from MeteoSwiss.**\n\nConnects AI models to the SwissMetNet measurement network (160+ stations, 10-minute interval), MeteoSwiss ICON-CH1/CH2-EPS forecasts and climate normals 1991–2020. Part of the [swiss-public-data-mcp](https://github.com/malkreide/swiss-public-data-mcp) portfolio.\n\n🇩🇪 [Deutsche Version](README.de.md)\n\n---\n\n## Demo query (anchor example)\n\n<img src=\"assets/demo.png\" width=\"720\" alt=\"Demo: Claude asks about sports-day suitability → meteo_school_check tool call → structured weather traffic-light response\">\n\n```\nHow suitable is next Wednesday for the sports day at Leutschenbach school?\n```\n\n→ `meteo_school_check(location=\"Zürich Oerlikon\", activity=\"Sporttag\")` returns a 🟢/🟡/🔴 traffic light for each day of the coming week — straight from the MeteoSwiss ICON model.\n\n**Combined with [swiss-environment-mcp](https://github.com/malkreide/swiss-environment-mcp):**\n\n```\nHow were air quality and weather at Leutschenbach school yesterday?\n```\n\n→ `meteo_current(station='REH')` + `env_nabel_current(station='ZUE')` = a complete environmental picture.\n→ [More use cases by audience](EXAMPLES.md) →\n\n---\n\n## Tools (6)\n\n| Tool | Description | Data source |\n|------|-------------|-------------|\n| `meteo_stations` | List SwissMetNet stations (filterable by canton) | Embedded |\n| `meteo_current` | Current 10-min observations for a station | BGDI STAC API |\n| `meteo_forecast` | 1–16 day forecast for a place or coordinates | Open-Meteo / MeteoSwiss ICON |\n| `meteo_school_check` | 🟢/🟡/🔴 traffic light for outdoor school events | Open-Meteo / MeteoSwiss ICON |\n| `meteo_climate_normals` | Monthly climate normals 1991–2020 | Embedded (KLO, SMA, BER, LUG, GVE) |\n| `meteo_warnings` | Active official weather warnings (storm, thunderstorm, heat, forest fire, …) — nationwide, by canton, or by PLZ | MeteoSwiss App-API + opendata.swiss |\n\n### Tool annotations (MCP hints)\n\nAll tools carry explicit [MCP annotations](https://modelcontextprotocol.io/specification/draft/server/tools#tool-annotations) — relevant for the client approval UI and for the LLM's safety decisions.\n\n| Tool | `readOnlyHint` | `destructiveHint` | `idempotentHint` | `openWorldHint` |\n|------|---|---|---|---|\n| `meteo_stations` | ✅ | ✗ | ✅ | ✗ (curated list) |\n| `meteo_current` | ✅ | ✗ | ✗ (live data) | ✅ (upstream STAC) |\n| `meteo_forecast` | ✅ | ✗ | ✗ (live data) | ✅ (upstream Open-Meteo) |\n| `meteo_school_check` | ✅ | ✗ | ✗ (live data) | ✅ (geocoding + forecast) |\n| `meteo_climate_normals` | ✅ | ✗ | ✅ | ✗ (embedded normals) |\n| `meteo_warnings` | ✅ | ✗ | ✗ (live data) | ✅ (MeteoSwiss App-API) |\n\n**Read rules**: all 6 tools are `readOnly + non-destructive` — the server fundamentally cannot write or delete anything. `idempotentHint=False` marks tools that return different values depending on when they are called.\n\n### MCP protocol version\n\n| Aspect | Value |\n|---|---|\n| Tested spec versions | `2024-11-05`, `2025-03-26`, `2025-06-18` (via the `mcp[cli]` SDK) |\n| MCP SDK version | see `pyproject.toml` → `mcp[cli]>=2.0.0,<3` (the `MCPServer` API from `mcp.server.mcpserver`) |\n| Update policy | Dependabot watches `mcp[cli]`; spec bumps are documented in the CHANGELOG with a \"Tool Definition Changes\" marker |\n\n→ Full roadmap & update strategy: [`docs/roadmap.md`](docs/roadmap.md)\n\n---\n\n## Quick start\n\n### Claude Desktop\n\n```json\n{\n  \"mcpServers\": {\n    \"meteoswiss\": {\n      \"command\": \"uvx\",\n      \"args\": [\"meteoswiss-mcp\"]\n    }\n  }\n}\n```\n\n### Claude Desktop (local development)\n\n```json\n{\n  \"mcpServers\": {\n    \"meteoswiss\": {\n      \"command\": \"uv\",\n      \"args\": [\"run\", \"--directory\", \"/path/to/meteoswiss-mcp\", \"meteoswiss-mcp\"]\n    }\n  }\n}\n```\n\n### Cloud / Render.com (Streamable HTTP)\n\nConfiguration via ENV variables (the CLI flags `--http` / `--port N` still work as an override):\n\n| Variable | Default | Meaning |\n|---|---|---|\n| `MCP_TRANSPORT` | `stdio` | `stdio` or `streamable-http` |\n| `MCP_HOST` | `127.0.0.1` | Bind address — **never change locally** |\n| `MCP_PORT` | `8000` | Port |\n| `MCP_ALLOW_ANY_HOST` | _unset_ | Must be set to `1` to allow the server to bind to `0.0.0.0` (containers/cloud only) |\n| `MCP_LOG_LEVEL` | `INFO` | `DEBUG` / `INFO` / `WARNING` / `ERROR` — structured JSON logs on stderr |\n| `MCP_ALLOWED_ORIGINS` | _unset_ | Comma-separated list of allowed origins for CORS. Empty = CORS disabled (same-origin only). `Mcp-Session-Id` is exposed automatically. |\n| `MCP_API_KEY` | _unset_ | If set: every request except `/health` requires `X-API-Key: <key>` or `Authorization: Bearer <key>`. Constant-time comparison. |\n| `MCP_STATELESS_HTTP` | `0` | `1` enables the SDK's stateless mode → each HTTP request opens a new session. Prerequisite for multi-replica deploys without sticky sessions (SCALE-002/003). |\n| `OTEL_EXPORTER_OTLP_ENDPOINT` | _unset_ | If set + `pip install meteoswiss-mcp[otel]`: OpenTelemetry spans per tool call + automatic httpx instrumentation are sent as OTLP-HTTP to the collector. |\n| `OTEL_SERVICE_NAME` | `meteoswiss_mcp` | Service name in the OTel resources |\n| `MCP_CACHE_ENABLED` | `1` | `0` disables the TTL cache entirely (e.g. for end-to-end tests) |\n| `MCP_CACHE_TTL_STAC` | `300` | TTL in seconds for STAC SMN observations (default 5 min) |\n| `MCP_CACHE_TTL_OPEN_METEO` | `600` | TTL for ICON forecasts (default 10 min) |\n| `MCP_CACHE_TTL_GEOCODING` | `3600` | TTL for geocoding lookups (default 1 h) |\n| `MCP_CACHE_TTL_OPENDATA` | `3600` | TTL for the opendata.swiss catalogue (default 1 h) |\n| `MCP_CACHE_TTL_WARNINGS` | `300` | TTL for warnings (MeteoSwiss App-API / structured override; default 5 min) |\n| `MCP_CLIMATE_NORMALS_PATH` | _unset_ | Path to a JSON file with additional climate normals — see `data/climate-normals.example.json` |\n| `MCP_WARNINGS_API_URL` | _unset_ | **Override** for the default MeteoSwiss App-API source: URL of a structured MeteoSwiss warnings API (e.g. the future OGD warnings REST endpoint). The host must be on the egress allow-list. Schema-tolerant (GeoJSON `features`, a `warnings` array or `items`). Unset → live App-API. |\n| `MCP_CLIMATE_NORMALS_URL_TEMPLATE` | _unset_ | URL template for runtime lookup of climate normals (for stations without embedded or JSON values). Tokens: `{station}` (lowercase), `{STATION}` (uppercase), `{param}` (MeteoSwiss code `tre200m0`/`rre150m0`/`sre000m0`). Example: `https://data.geo.admin.ch/.../{station}/{param}.txt`. The host must be on the egress allow-list. |\n\n```bash\n# Local test (safe, loopback only)\nMCP_TRANSPORT=streamable-http meteoswiss-mcp\n\n# Container / Render\nMCP_TRANSPORT=streamable-http MCP_HOST=0.0.0.0 MCP_ALLOW_ANY_HOST=1 meteoswiss-mcp\n```\n\n#### Docker / Render\n\nThe repo includes a production-ready **multi-stage Dockerfile** (non-root user, HEALTHCHECK) and a **`render.yaml`** blueprint:\n\n```bash\n# Build + test locally\ndocker build -t meteoswiss-mcp .\ndocker run --rm -p 8000:8000 meteoswiss-mcp\ncurl http://127.0.0.1:8000/health   # → {\"status\":\"ok\",\"service\":\"meteoswiss-mcp\"}\n```\n\nOn Render: \"New → Blueprint\" → select the repo. Defaults (plan `starter`, Frankfurt, single instance) are set in `render.yaml`.\n\n**Important:** `numInstances: 1` is set deliberately — sticky-session routing for multi-replica (audit SCALE-002/003) is not yet implemented.\n\n#### Structured logging\n\nAll tool invocations, upstream failures and egress blocks are emitted as JSON events on `stderr` (stdio-transport safe). Example:\n\n```json\n{\"tool\": \"meteo_forecast\", \"days\": 7, \"has_coords\": false, \"event\": \"tool_invoked\", \"level\": \"info\", \"timestamp\": \"2026-05-20T07:00:00Z\"}\n{\"tool\": \"meteo_forecast\", \"endpoint\": \"geocoding\", \"error_type\": \"HTTPStatusError\", \"event\": \"upstream_failed\", \"level\": \"warning\", \"timestamp\": \"...\"}\n{\"url\": \"https://evil.example.com/\", \"method\": \"GET\", \"reason\": \"host not in allow-list\", \"event\": \"egress_blocked\", \"level\": \"warning\", \"timestamp\": \"...\"}\n```\n\n#### HTTP-mode security\n\n- `MCP_HOST` deliberately defaults to `127.0.0.1` so that `--http` on a dev laptop is not accidentally exposed to the local subnet (audit finding SEC-016).\n- All outgoing HTTP calls (including redirect follows) are validated against an allow-list: `data.geo.admin.ch`, `api.open-meteo.com`, `geocoding-api.open-meteo.com`, `opendata.swiss`. Other hosts and IP literals (in particular `169.254.169.254`, RFC1918) are rejected with `EgressBlocked` (SEC-004 / SEC-021).\n- **CORS**: disabled by default (same-origin only). Browser clients (e.g. claude.ai web) need `MCP_ALLOWED_ORIGINS=<csv>` — the `Mcp-Session-Id` header is then automatically in `Access-Control-Expose-Headers` (SDK-004).\n- **API-key auth**: disabled by default. In a production HTTP setup, always set `MCP_API_KEY=<random>` — requests without a valid `X-API-Key` or `Authorization: Bearer …` are rejected with 401 (SEC-009 / SEC-013). `/health` stays open for container health probes.\n\n#### Example: production HTTP stack\n\n```bash\n# 32 bytes of randomness as the auth key\nexport MCP_API_KEY=$(python -c \"import secrets; print(secrets.token_urlsafe(32))\")\n\nMCP_TRANSPORT=streamable-http \\\nMCP_HOST=0.0.0.0 \\\nMCP_ALLOW_ANY_HOST=1 \\\nMCP_ALLOWED_ORIGINS=https://app.example.com \\\nMCP_API_KEY=\"$MCP_API_KEY\" \\\nmeteoswiss-mcp\n```\n\n---\n\n## Example queries\n\n### School planning\n\n```\nWhich days next week are suitable for a sports day in Zürich?\n→ meteo_school_check(location=\"Zürich\", activity=\"Sporttag\")\n\nWhat will the weather be at Leutschenbach school on Friday?\n→ meteo_forecast(location=\"Zürich Oerlikon\", days=5)\n\nShow me current readings from the nearest MeteoSwiss station to Zürich-Schwamendingen.\n→ meteo_current(station=\"REH\")\n```\n\n### Climate comparison\n\n```\nHow much rain normally falls in June in Zürich?\n→ meteo_climate_normals(station=\"KLO\")\n\nIs Lugano really much sunnier than Zürich? Show me the annual values.\n→ meteo_climate_normals(station=\"LUG\") + meteo_climate_normals(station=\"SMA\")\n```\n\n### Infrastructure & environment\n\n```\nAre there currently any weather warnings for the canton of Zürich?\n→ meteo_warnings(canton=\"ZH\")\n\nShow me a 10-day forecast for the Heerenschürli sports facility with hourly values.\n→ meteo_forecast(location=\"Sportanlage Heerenschürli Zürich\", days=10, hourly=True)\n```\n\n---\n\n## Architecture\n\n```\nClaude Desktop / AI agent\n        │\n        │ MCP (stdio / Streamable HTTP)\n        ▼\nmeteoswiss-mcp (MCPServer)\n        │\n        ├── meteo_stations ──────────────── [embedded: ~20 SMN stations]\n        │\n        ├── meteo_current ───────────────── BGDI STAC API\n        │                                   data.geo.admin.ch/api/stac/v1\n        │                                   Collection: ch.meteoschweiz.ogd-smn\n        │\n        ├── meteo_forecast ──────────────── Open-Meteo\n        ├── meteo_school_check ──────────── api.open-meteo.com/v1/meteoswiss\n        │                                   (MeteoSwiss ICON-CH1/CH2-EPS, 1–2 km)\n        │\n        ├── meteo_climate_normals ───────── [embedded: normals 1991–2020]\n        │\n        └── meteo_warnings ──────────────── app-prod-ws.meteoswiss-app.ch\n                                            (MeteoSwiss App-API) + opendata.swiss\n```\n\n### Data sources\n\n| Source | URL | License |\n|--------|-----|---------|\n| BGDI STAC API (MeteoSwiss OGD) | `data.geo.admin.ch/api/stac/v1` | CC BY 4.0 |\n| Open-Meteo (MeteoSwiss ICON) | `api.open-meteo.com/v1/meteoswiss` | CC BY 4.0 |\n| Open-Meteo Geocoding | `geocoding-api.open-meteo.com` | CC BY 4.0 |\n| opendata.swiss CKAN | `opendata.swiss/api/3/action` | CC BY 4.0 |\n| MeteoSwiss App-API (warnings) | `app-prod-ws.meteoswiss-app.ch/v1/plzDetail` | CC BY 4.0 |\n\n---\n\n## Safety & limits\n\n| Aspect | Details |\n|--------|---------|\n| **Access** | Read-only (`readOnlyHint: true` on all tools) — the server cannot modify or delete any data |\n| **Personal data** | No personal data — all sources are aggregated, publicly available open data |\n| **Rate limits** | Built-in per-query caps: max 50 results per API call, 30 s timeout |\n| **Authentication** | No API keys required — all data sources are publicly accessible |\n| **Licenses** | All data under CC BY 4.0 (MeteoSwiss Open Government Data) |\n| **Terms of Service** | Subject to the ToS of the respective data sources: [MeteoSwiss OGD](https://www.meteoswiss.admin.ch/services-and-publications/service/open-government-data.html), [Open-Meteo](https://open-meteo.com/en/terms), [opendata.swiss](https://opendata.swiss/en/terms-of-use) |\n\n---\n\n## Known limitations\n\n| ID | Tool | Description |\n|----|------|-------------|\n| BUG-01 | `meteo_current` | STAC asset structure can vary per station; fallback to a direct link is implemented |\n| LIM-01 | `meteo_climate_normals` | Only 5 stations embedded (KLO, SMA, BER, LUG, GVE); the rest via an opendata.swiss link |\n| LIM-02 | `meteo_warnings` | Live warnings come from the **MeteoSwiss App-API** (`plzDetail`) — public and unauthenticated, but undocumented (mobile-app backend, not the OGD REST API). There is no nationwide endpoint, so the countrywide view aggregates one representative capital PLZ per canton (sub-regional warnings outside that PLZ may be missed — narrow with `plz`/`canton`). `MCP_WARNINGS_API_URL` overrides it once the official OGD warnings REST API ships. |\n| LIM-03 | `meteo_current` | Shows 10-min values in UTC; no automatic conversion to local time |\n\n### Responsibility matrix — snow & precipitation (delineation vs. `swiss-environment-mcp`)\n\nTo avoid duplicating **snow and precipitation** data across the portfolio,\nresponsibilities are split as follows. `meteoswiss-mcp` owns atmospheric\nprecipitation and weather; `swiss-environment-mcp` (SLF domain) owns snow on the\nground and avalanche danger.\n\n| Data | meteoswiss-mcp (MeteoSwiss) | swiss-environment-mcp (BAFU / SLF) |\n|---|---|---|\n| Precipitation amount (mm): measurement network, forecast, climate normals | ✅ `meteo_current` / `meteo_forecast` / `meteo_climate_normals` | ❌ |\n| Snowfall as a current weather condition | ✅ `meteo_current` / `meteo_forecast` (weather code) | ❌ |\n| Weather warnings (storm, thunderstorm, heat) | ✅ `meteo_warnings` | ❌ |\n| Snow depth on the ground (`HS`) | ❌ | ✅ SLF IMIS / study-plot ¹ |\n| Fresh snow 24 h (`HN_1D`) | ❌ | ✅ SLF ¹ |\n| Avalanche danger level | ❌ | ✅ SLF avalanche bulletin ¹ |\n| Natural-hazard warnings (flood, avalanche, wildfire) | ❌ | ✅ `env_flood_warnings`, `env_hazard_*`, `env_wildfire_danger` |\n\n**Rule:** **atmospheric precipitation** (rain/snowfall as mm) plus weather,\nforecast, warnings and climate normals belong to `meteoswiss-mcp`; snow **on the\nground** and **avalanche** danger belong to `swiss-environment-mcp` (SLF). The SLF\nIMIS precipitation sensor is used there only as context for the snowpack and is\nnever exposed as a precipitation tool, so it does not duplicate MeteoSwiss.\n\n¹ SLF/snow tools in `swiss-environment-mcp` are in preparation (Phase-1 live-probe\ncompleted 2026-07-19, see that repo's `docs/probe-slf.md`); the demarcation is\nfixed now so the two servers do not collide once implemented.\n\n---\n\n## Portfolio synergies\n\n```\nmeteoswiss-mcp\n    │\n    ├── swiss-environment-mcp   Combine weather + air quality (NABEL)\n    │                           \"How were weather AND air at Leutschenbach school?\"\n    │\n    └── zurich-opendata-mcp     School locations → weather forecast\n                                \"Which schools in Zürich have sports-day weather?\"\n```\n\n---\n\n## MCP Protocol Version\n\nThis server speaks **two protocol eras** over the same endpoint. The client's\nfirst request on a connection decides which one applies; a later claim from the\nother era is refused.\n\n| Era | Revision | Who reaches it |\n|---|---|---|\n| `initialize` handshake | `2024-11-05` … **`2025-11-25`** | What today's clients speak. The server answers with the revision asked for, or with the `2025-11-25` ceiling when the request asks for something newer. |\n| Per-request envelope | **`2026-07-28`** | A request carrying the `2026-07-28` `_meta` envelope opens a modern connection. |\n\nBoth revisions are pinned in\n[`tests/test_protocol_version.py`](tests/test_protocol_version.py) and asserted\nagainst the installed SDK, so a Dependabot bump of `mcp` cannot move either one\nsilently. This server builds no ASGI app to send an `initialize` through, so\nthe gate asserts the SDK constants rather than a measured response — the\nweaker form, named rather than left unsaid.\n\nNote that the SDK's `LATEST_PROTOCOL_VERSION` is an alias for the **modern**\nera, not for the handshake era — pinning against it alone would leave the era\nthat current clients actually negotiate free to drift.\n\n**Update policy.** When the gate fails, do not edit the constant blindly: read\nthe spec changelog between the two revisions, verify the server still behaves,\nthen move the constant, this section, `README.de.md` and\n[`CHANGELOG.md`](CHANGELOG.md) together.\n\n---\n\n## Testing\n\n```bash\n# Unit tests (no network)\nPYTHONPATH=src pytest tests/ -m \"not live\" -v\n\n# Live tests (real APIs) — also run daily at 05:17 UTC via\n# .github/workflows/live-tests.yml, so a format change upstream\n# surfaces even though the unit tests stay green.\nPYTHONPATH=src pytest tests/ -m live -v\n\n# Linting — install the local gates once with `pre-commit install`\n# to run these (and the CI guards) before every commit.\nruff check src/ tests/ scripts/\nruff format --check src/ tests/ scripts/\n```\n\n---\n\n## Development\n\n```bash\ngit clone https://github.com/malkreide/meteoswiss-mcp\ncd meteoswiss-mcp\npip install -e \".[dev]\"\n```\n\n### MCP Inspector (local test)\n\n```bash\nPYTHONPATH=src npx @modelcontextprotocol/inspector python -m meteoswiss_mcp.server\n```\n\n---\n\n## Contributing\n\nSee the [contributing guidelines](CONTRIBUTING.md) ([Deutsch](CONTRIBUTING.de.md)).\n\n---\n\n## Security\n\nSee the [security policy](SECURITY.md) ([Deutsch](SECURITY.de.md)) for the\nsecurity posture and how to report a vulnerability.\n\n---\n\n## License\n\nMIT License – see [LICENSE](LICENSE).\n\nSource data: MeteoSwiss Open Government Data (CC BY 4.0).\nWhen using the data, cite: **Source: MeteoSwiss**.\n\n---\n\n## Author\n\n**Hayal Oezkan** · [github.com/malkreide](https://github.com/malkreide)\n\n---\n\n## Related servers\n\n[![swiss-environment-mcp](https://img.shields.io/badge/server-swiss--environment--mcp-green)](https://github.com/malkreide/swiss-environment-mcp)\n[![zurich-opendata-mcp](https://img.shields.io/badge/server-zurich--opendata--mcp-green)](https://github.com/malkreide/zurich-opendata-mcp)\n[![swiss-transport-mcp](https://img.shields.io/badge/server-swiss--transport--mcp-green)](https://github.com/malkreide/swiss-transport-mcp)\n</content>\n</invoke>\n\n<!-- mcp-name: io.github.malkreide/meteoswiss-mcp -->\n\n<!-- BEGIN GENERATED: install -->\n## Installation\n\nRun via [`uv`](https://docs.astral.sh/uv/)'s `uvx` — no clone or manual install needed. Add to your MCP client config (`mcpServers` for Claude Desktop, Cursor and Windsurf; use a top-level `servers` key for VS Code in `.vscode/mcp.json`):\n\n```json\n{\n  \"mcpServers\": {\n    \"meteoswiss-mcp\": {\n      \"command\": \"uvx\",\n      \"args\": [\n        \"meteoswiss-mcp\"\n      ]\n    }\n  }\n}\n```\n<!-- END GENERATED: install -->\n",
  "bytes": 19714,
  "sha": "5dafddeb0adf45c44a2e71ce89a89fc40b00ccf5067acf037438602a41584bba",
  "repo_slug": "malkreide/meteoswiss-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_malkreide_meteoswiss_mcp_e7c44129/readme"
}