io.github.malkreide/swiss-environment-mcp
BAFU environmental data, NABEL air quality, hydrology
Open source Open in the app JSON README (API)
About
BAFU environmental data, NABEL air quality, hydrology
Details
- Kind
- MCP servers
- Topic
- No topic detected
- Publisher
- malkreide
- Origin
- official
- Category
- ferramentas
- Transport
- local
- Version
- 0.6.0
- Stars
- 1
- Forks
- 1
- Open pull requests
- 1
- Last push
- 2026-09-01T23:56:24Z
- Repository state
- ativo
- Language
- Python
- License
- MIT
- Added
- 2026-08-29 04:00:27
- Updated
- 2026-08-29 04:00:27
- Origin id
io.github.malkreide/swiss-environment-mcp
README
> π¨π **Part of the [Swiss Public Data MCP Portfolio](https://github.com/malkreide)**
# πΏ swiss-environment-mcp

[](https://opensource.org/licenses/MIT)
[](https://www.python.org/downloads/)
[](https://modelcontextprotocol.io/)
[](https://github.com/malkreide/swiss-environment-mcp/actions)
[](https://opendata.swiss/en/organization/bafu)
> MCP server connecting AI models to Swiss environmental data from BAFU β air quality, hydrology, natural hazards, wildfire danger and open environmental datasets.
[π©πͺ Deutsche Version](README.de.md)
<p align="center">
<img src="assets/demo.svg" alt="Demo: Claude queries NABEL air quality via a swiss-environment-mcp tool call and gets a WHO 2021 compliance check" width="820">
</p>
---
## Overview
**swiss-environment-mcp** gives AI assistants like Claude direct access to real-time environmental data from Swiss federal authorities β no API keys required. Air quality readings from the national NABEL monitoring network, hydrological gauging stations, natural hazard bulletins, and the full BAFU dataset catalogue are all accessible through a single standardised MCP interface.
The server covers four thematic clusters: air quality (NABEL), hydrology, natural hazards, and the BAFU open data catalogue. Each cluster maps to a group of purpose-built tools that translate raw agency data into clean JSON responses.
**Anchor demo query:** *"What is the current air quality at the NABEL station ZΓΌrich-Kaserne β and does it comply with WHO 2021 guidelines?"*
β [More use cases by audience](./EXAMPLES.md) β
---
## Features
- π¬οΈ **Air quality monitoring** β 16 NABEL stations, NOβ/Oβ/PM10/PM2.5/SOβ/CO, Swiss LRV + WHO 2021 limit checks
- π§ **Hydrology** β water levels, flow rates, temperatures across Swiss gauging stations
- π¨ **Flood warnings** β active alerts filtered by danger level and canton
- ποΈ **Natural hazard bulletin** β SLF/BAFU bulletin in DE/FR/IT/EN, region-specific warnings
- π₯ **Wildfire danger** β canton- and region-level fire danger index
- βοΈ **Snow & avalanches (SLF)** β snow depth, new snow per IMIS station; avalanche danger levels (EAWS)
- π¦ **Hunting statistics** β cull & game-loss figures per species, canton and year (federal hunting statistics)
- π¦ **BAFU open data catalogue** β search and retrieve environmental datasets via CKAN
- π **No authentication required** β all data sources are publicly accessible
- βοΈ **Dual transport** β stdio for Claude Desktop, Streamable HTTP/SSE for cloud deployment
---
## Prerequisites
- Python 3.11+
- No API keys needed β all endpoints are publicly accessible without authentication
---
## Installation
```bash
# Clone the repository
git clone https://github.com/malkreide/swiss-environment-mcp.git
cd swiss-environment-mcp
# Install
pip install -e .
```
Or with `uvx` (no permanent installation):
```bash
uvx swiss-environment-mcp
```
Or via pip:
```bash
pip install swiss-environment-mcp
```
---
## Quickstart
```bash
# Start the server (stdio mode for Claude Desktop)
swiss-environment-mcp
```
Try it immediately in Claude Desktop:
> *"What is the current air quality at NABEL station ZΓΌrich-Kaserne?"*
> *"Are there any active flood warnings in Switzerland right now?"*
> *"What is the wildfire danger level in Canton Valais?"*
---
## Configuration
### Claude Desktop
**Minimal (recommended):**
```json
{
"mcpServers": {
"swiss-environment": {
"command": "uvx",
"args": ["swiss-environment-mcp"],
"env": {}
}
}
}
```
**Config file locations:**
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
After saving, restart Claude Desktop completely.
### Cloud Deployment (SSE for browser access)
For use via **claude.ai in the browser** (e.g. on managed workstations without local software):
**Render.com (recommended):**
1. Push/fork the repository to GitHub
2. On [render.com](https://render.com): New Web Service β connect GitHub repo
3. Render detects `render.yaml` automatically
4. In claude.ai under Settings β MCP Servers, add: `https://your-app.onrender.com/sse`
**Docker:**
```bash
docker build -t swiss-environment-mcp .
docker run -p 8000:8000 swiss-environment-mcp
```
> π‘ *"stdio for the developer laptop, SSE for the browser."*
---
## Available Tools
All tools share the stable `env_` name prefix β a deliberate namespace choice so
that the server's tools are recognisable and unlikely to collide when several MCP
servers are mounted together. Tool definitions (name, description, input schema)
are pinned via `tool-snapshot.json`; changes require a CHANGELOG entry (see
CONTRIBUTING).
**Tool budget (21 tools, 7 clusters) β exhausted.** Every tool maps to a distinct
user question, not to a REST endpoint β there is no CRUD/endpoint mirroring, and
the anchor queries are each answerable in a single call. The count sits above the
β€12 rule of thumb because the server deliberately spans seven environmental
domains (air, water, hazards, snow, hunting, catalogue, aircraft noise), each
needing a list/detail pair or a domain-specific action. Further consolidation was
considered and rejected: the `*_stations`/`*_current` pairs (NABEL, hydro, snow)
serve genuinely different intents (discovery vs. reading a known station) and
collapsing them would overload a single tool's parameters.
**This is the ceiling.** With the aircraft-noise cluster the server's tool budget
is spent: any further data source belongs in a separate `*-mcp` server, not here.
The next addition would instead trigger a review of whether some listings should
migrate to MCP resources rather than tools.
### π¬οΈ Air Quality / NABEL (3 tools)
| Tool | Description | Data Source |
|---|---|---|
| `env_nabel_stations` | List all 16 NABEL monitoring stations with location type and canton | NABEL / BAFU |
| `env_nabel_current` | Current air quality data for a station (NOβ, Oβ, PM10, PM2.5, SOβ, CO) | NABEL / BAFU |
| `env_air_limits_check` | Compare a measurement against Swiss LRV limits and WHO 2021 guidelines | Built-in |
### π§ Hydrology (5 tools)
| Tool | Description | Data Source |
|---|---|---|
| `env_hydro_stations` | Filter hydrological gauging stations by water body (canton filter unavailable β see notes) | **LINDAS SPARQL** β hydrodaten.admin.ch (fallback) |
| `env_hydro_current` | Current water level, flow rate and temperature at a station | **LINDAS SPARQL** β hydrodaten.admin.ch (fallback) |
| `env_hydro_history` | Historical hourly values (up to 30 days) with download links β οΈ | hydrodaten.admin.ch |
| `env_flood_warnings` | Active flood warnings filtered by danger level (nationwide β canton filter not applied) | **LINDAS SPARQL** |
| `env_bathing_water` | Bathing water quality (E.coli, enterococci) per bathing site β multi-year time series | **LINDAS SPARQL** (data cube `ubd0104`) |
### ποΈ Natural Hazards (3 tools)
| Tool | Description | Data Source |
|---|---|---|
| `env_hazard_overview` | Router: points to the dedicated live hazard tools + official portals (no network call β the aggregate `naturgefahren.ch` API was discontinued) | local |
| `env_hazard_regions` | Router: maps a hazard type (flood/avalanche/wildfire/snow) to the right live tool + portal (no network call) | local |
| `env_wildfire_danger` | Wildfire danger index by canton and region | waldbrandgefahr.ch |
### βοΈ Snow & Avalanches / SLF (3 tools)
| Tool | Description | Data Source |
|---|---|---|
| `env_snow_stations` | List automatic SLF/IMIS snow measurement stations (by canton) | measurement-api.slf.ch |
| `env_snow_current` | Current snow depth (HS) and 24 h new snow (HN_1D) per station, in cm | measurement-api.slf.ch |
| `env_avalanche_bulletin` | Avalanche danger levels (EAWS 1β5) per warning region, seasonal | aws.slf.ch |
### π¦ Hunting & Wildlife (2 tools)
| Tool | Description | Data Source |
|---|---|---|
| `env_hunting_species` | List the 36 species tracked by the federal hunting statistics (with codes) | jagdstatistik.ch (embedded) |
| `env_hunting_stats` | Cull / game-loss / population figures per species, canton and year (2015β2024) | jagdstatistik.ch |
### π Environmental Data Catalogue (2 tools)
| Tool | Description | Data Source |
|---|---|---|
| `env_bafu_datasets` | Search BAFU datasets on opendata.swiss (CKAN API) | opendata.swiss |
| `env_bafu_dataset_detail` | Full metadata and download URLs for a specific dataset | opendata.swiss |
### βοΈ Aircraft Noise / BAZL noise cadastre (3 tools)
| Tool | Description | Data Source |
|------|-------------|-------------|
| `env_noise_aircraft_at` | Aircraft noise exposure at an **LV95** point β resolves the overlapping noise contours and returns a dB bracket with the highest value as an upper bound | **api3.geo.admin.ch** (BAZL) |
| `env_noise_aircraft_registers` | Which airfields have a published cadastre, with validity dates, dB range and the official plan (PDF) β the provenance tool | **api3.geo.admin.ch** (BAZL) |
| `env_noise_limits_check` | Compare a rating level against the LSV exposure limit values (planning value / immission limit / alarm value) by sensitivity level ES IβIV | Built-in (SR 814.41, Annex 5) |
> βοΈ **Legal notice β carried in every response of these three tools.** The noise
> cadastre is an *orientation aid*. Legally binding information on construction
> projects is issued by the competent cantonal office or by the FOCA (BAZL).
> These tools do not replace a building permit clarification.
**Coordinates must be LV95** (EPSG:2056, metres: E β 2'480'000β2'840'000,
N β 1'070'000β1'300'000). WGS84 degrees such as `8.54 / 47.37` are rejected
**fail-fast** with a conversion hint β this is the single most common LLM error
with Swiss geodata. Convert via swisstopo REFRAME or `convert_coordinates` in
[swisstopo-mcp](https://github.com/malkreide/swisstopo-mcp).
**The contours are lines, not areas.** The cadastre publishes `MultiLineString`
isolines, so `identify` performs a *proximity* query within a search radius β not
a point-in-polygon test. `env_noise_aircraft_at` therefore returns a **bracket**
(Β«the point lies between the 61 dB and the 62 dB contourΒ») with the highest value
as a stated **upper bound**, never an interpolated point value. The search radius
is reported in every response; enlarging it inflates the result (at one Kloten
point: 100 m β 61β62 dB, but 500 m β 58β75 dB, because the 75 dB runway contour
sits 1.5 km away).
### Anchor demo query
> **"Is the planned school site inside an aircraft-noise zone with building
> restrictions β and at which dB level?"**
Cross-server flow: resolve an address or EGID via
[swiss-housing-mcp](https://github.com/malkreide/swiss-housing-mcp) β convert to
an LV95 coordinate β `env_noise_aircraft_at(east=β¦, north=β¦, period="day")` β
feed the resulting `level_db` into `env_noise_limits_check(level_db=β¦,
sensitivity_level="II", period="day")` for the legal classification.
```
swiss-housing-mcp β address / EGID β LV95 E/N
β
env_noise_aircraft_at β 62 dB (upper bound, LBK ZΓΌrich, valid from 03.07.2015)
β
env_noise_limits_check β immission limit ES II (60 dB) exceeded by 2 dB
```
### Example Use Cases
| Query | Tool |
|---|---|
| *"Air quality at ZΓΌrich-Kaserne right now?"* | `env_nabel_current` |
| *"Does 45 Β΅g/mΒ³ NOβ exceed the Swiss limit?"* | `env_air_limits_check` |
| *"Current water level of the Limmat in Zurich?"* | `env_hydro_current` |
| *"Is the water quality at Strandbad KΓΌsnacht safe for swimming?"* | `env_bathing_water` |
| *"Active flood warnings in Switzerland?"* | `env_flood_warnings` |
| *"Natural hazard bulletin for GraubΓΌnden?"* | `env_hazard_overview` |
| *"Wildfire danger in Canton Valais?"* | `env_wildfire_danger` |
| *"BAFU biodiversity datasets on opendata.swiss?"* | `env_bafu_datasets` |
| *"Is the planned school site in an aircraft-noise zone β at which dB level?"* | `env_noise_aircraft_at` |
| *"How old is the noise cadastre for Geneva airport?"* | `env_noise_aircraft_registers` |
| *"Does 62 dB at night exceed the LSV limit in an ES II residential zone?"* | `env_noise_limits_check` |
---
## π‘οΈ Safety & Limits
| Aspect | Details |
|--------|---------|
| **Access** | Read-only (`readOnlyHint: true`) β the server cannot modify or delete any data |
| **Personal data** | No personal data β all sources are aggregated, public environmental measurements |
| **Rate limits** | Built-in per-query caps (e.g. max 30 days hydrology history, 50 dataset search results) |
| **Timeout** | 30 seconds per API call |
| **Authentication** | No API keys required β all BAFU endpoints are publicly accessible |
| **Licenses** | BAFU Open Government Data (OGD) β free reuse with mandatory attribution |
| **Terms of Service** | Subject to ToS of the respective data sources: [BAFU / opendata.swiss](https://opendata.swiss/en/organization/bafu), [hydrodaten.admin.ch](https://hydrodaten.admin.ch), [naturgefahren.ch](https://naturgefahren.ch), [waldbrandgefahr.ch](https://waldbrandgefahr.ch) |
---
## Architecture
```
βββββββββββββββββββ βββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββ
β Claude / AI ββββββΆβ Swiss Environment MCP ββββββΆβ BAFU / Swiss Agencies β
β (MCP Host) βββββββ (MCP Server) βββββββ β
βββββββββββββββββββ β β β hydrodaten.admin.ch β
β 21 Tools Β· 3 Resources β β naturgefahren.ch β
β Stdio | SSE β β waldbrandgefahr.ch β
β β β opendata.swiss (CKAN) β
β api_client.py β ββββββββββββββββββββββββββββ
β server.py (FastMCP) β
βββββββββββββββββββββββββββββ
```
### Architecture note β extractable `lindas/` module
All LINDAS SPARQL access goes through the deliberately **extractable**
`src/swiss_environment_mcp/lindas/` module, built as three strict layers:
`client.py` knows only SPARQL and HTTP (GET/POST, 45 s client-side timeout,
`QueryError` carrying the server's MALFORMED message, 2 s/4 s/8 s retry);
`cube.py` knows the cube.link vocabulary (mandatory `observationSet`
two-phase access, version deduplication via `schema:expires`, codeβlabel
resolution, licence lookup); the tools only ever call `cube.py`. The module
will be lifted into a shared `lindas-mcp` as soon as a second server uses
LINDAS (candidate: `wsl-envidat-mcp`).
### Architecture decision β live API for the aircraft-noise cadastre
The rest of this server follows the portfolio's **dump-first** standard. The
aircraft-noise cluster deliberately deviates: `env_noise_aircraft_at` and
`env_noise_aircraft_registers` query `api3.geo.admin.ch` **live on every call**.
This is *not* a size argument. Measured on 2026-07-28, the entire cadastre is
**747 objects (~3 MB of GeoJSON)** across all eight sublayers β it would mirror
without difficulty. Two other reasons decide it:
1. **A mirror would falsify the freshness claim.** The cadastres are revised per
airfield, individually and without announcement (validity dates 2009β2024).
Read from a dump, `source_freshness` would report the `validfrom` as of the
mirroring date β the tool would *assert* provenance it no longer has. For a
cluster whose third tool exists precisely to answer "how old is the basis",
that is the worst possible failure mode.
2. **The spatial query is the value, not the attributes.** Evaluated locally it
means point-to-line distances over 26'000+ vertices per layer. That requires
either **shapely/GEOS** β the first compiled dependency in a deliberately
binary-free `pyproject.toml` (Docker image, wheel matrix and security surface
all change) β or hand-rolled distance maths. The latter is feasible, since
LV95 is a metric projection and Euclidean distance is exact; but then *this
server* would own the correctness of an official noise-cadastre statement
instead of the federal office.
Full measurements and reasoning: [`docs/probe-fluglaerm.md`](docs/probe-fluglaerm.md).
### Data Sources
| Source | Data | Licence |
|---|---|---|
| [lindas.admin.ch](https://lindas.admin.ch) | Current hydrology (level, discharge, water temperature) and bathing water quality via SPARQL | BAFU Open-Use / OGD (declared per graph/dataset β each response carries a licence field) |
| [hydrodaten.admin.ch](https://hydrodaten.admin.ch) | Water levels, flow rates, temperatures (REST fallback) | BAFU OGD |
| [naturgefahren.ch](https://naturgefahren.ch) | Natural hazard bulletin (SLF/BAFU) | BAFU/SLF |
| [waldbrandgefahr.ch](https://waldbrandgefahr.ch) | Wildfire danger index | BAFU |
| [SLF data service](https://www.slf.ch/en/services-and-products/slf-data-service/) | Snow depth, new snow (IMIS); avalanche bulletin | SLF (WSL) CC BY 4.0 |
| [jagdstatistik.ch](https://www.jagdstatistik.ch/de/home) | Federal hunting statistics (cull, game loss, population) | BAFU β source attribution required (no explicit licence published) |
| [api3.geo.admin.ch](https://api3.geo.admin.ch) | BAZL aircraft noise cadastre (`identify`, LV95) | swisstopo / BAZL β free reuse with attribution |
| [opendata.swiss](https://opendata.swiss/en/organization/bafu) | BAFU data catalogue (CKAN API) | OGD |
All data: publicly accessible, no authentication required.
**Attribution required:** BAFU / SLF (WSL) must be cited as the source when using their data.
---
## Project Structure
```
swiss-environment-mcp/
βββ src/swiss_environment_mcp/
β βββ __init__.py # Package
β βββ server.py # FastMCP server: 21 tools, 3 resources
β βββ api_client.py # HTTP client + egress allow-list (SSRF guard)
β βββ logging_setup.py # structlog -> stderr
βββ tests/
β βββ test_unit.py # Mocked unit tests (no network) β CI default
β βββ test_integration.py # Live API tests (marker: live)
β βββ test_20_scenarios.py # Live scenario coverage
βββ scripts/tool_snapshot.py # Tool-definition hash snapshot (rug-pull guard)
βββ docs/ # security.md, scaling.md, roadmap.md
βββ .github/
β βββ dependabot.yml # Monthly dependency/action updates
β βββ workflows/ # ci.yml, security.yml (gitleaks), live-tests.yml, publish.yml
βββ Dockerfile # Multi-stage, non-root container
βββ render.yaml / Procfile # Cloud deployment
βββ tool-snapshot.json # Committed tool-definition snapshot
βββ .env.example # Non-secret config template
βββ pyproject.toml # Build configuration (hatchling)
```
> **Single-module layout (rationale, audit ARCH-011):** the 21 tools live in one
> `server.py` rather than a `tools/` package. They are thin, uniform wrappers over
> `api_client.py` sharing the same input/response patterns, so a single
> well-sectioned module stays more navigable than 4 near-identical files. This is a
> deliberate, documented deviation; a split is revisited if tool logic grows non-uniform.
---
## MCP Protocol Version & Maintenance
This server speaks **two protocol eras** over the same endpoint. The client's
first request on a connection decides which one applies; a later claim from the
other era is refused.
| Era | Revision | Who reaches it |
|---|---|---|
| `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. |
| Per-request envelope | **`2026-07-28`** | A request carrying the `2026-07-28` `_meta` envelope opens a modern connection. |
Both revisions are pinned in
[`tests/test_protocol_version.py`](tests/test_protocol_version.py) and asserted
against the installed SDK, so a Dependabot bump of `mcp` cannot move either one
silently. This server builds no ASGI app to send an `initialize` through, so
the gate asserts the SDK constants rather than a measured response β the
weaker form, named rather than left unsaid.
Note that the SDK's `LATEST_PROTOCOL_VERSION` is an alias for the **modern**
era, not for the handshake era β pinning against it alone would leave the era
that current clients actually negotiate free to drift.
**Update policy.** When the gate fails, do not edit the constant blindly: read
the spec changelog between the two revisions, verify the server still behaves,
then move the constant, this section, `README.de.md` and
[`CHANGELOG.md`](CHANGELOG.md) together.
- **Tool-definition stability (audit SEC-022):** any change to a tool's name,
description or parameters changes `tool-snapshot.json`; CI fails until the snapshot
is regenerated and a `CHANGELOG` entry + version bump are added.
- **Update policy:** review Dependabot PRs monthly; bump the version (semver) on any
tool-definition or behaviour change.
## Lifecycle Phase
This server is in **Phase 1 (read-only)** β all tools read-only, no auth, no side
effects. The phase model and prerequisites for Phase 2 (write/auth) are in
[`docs/roadmap.md`](docs/roadmap.md). Security architecture (SSRF/egress, secret
management, lethal-trifecta assessment): [`docs/security.md`](docs/security.md).
Scaling/session strategy: [`docs/scaling.md`](docs/scaling.md).
---
## Known Limitations
- **Hydrology via LINDAS**: `env_hydro_current`, `env_hydro_stations` and `env_flood_warnings` query the BAFU LINDAS SPARQL endpoint (typed live values: level, discharge, water temperature, danger level). LINDAS holds **current values only** (one observation per station) β it is **not** a historical time series. See [`docs/probe-lindas-hydro.md`](docs/probe-lindas-hydro.md).
- **Historical hydrology / `env_hydro_history` (BUG-01 resolved)**: the old `hydrodaten.admin.ch/lhg/az/*` REST endpoints (hourly CSV, `warnings.json`, station JSON) are **decommissioned (404)**. `env_flood_warnings` now uses LINDAS `dangerLevel` instead. Real historical time series (daily / long-term means β e.g. *summer 2024 vs. long-term average*) are **not freely available via API**; they must be ordered from the **BAFU Hydrological Enquiry Service** (abfragezentrale@bafu.admin.ch). `env_hydro_history` returns the latest LINDAS value plus this access path.
- **Flood warnings**: `env_flood_warnings` reads LINDAS `dangerLevel`. A canton filter is not available there (LINDAS carries no canton code), so the response is **always nationwide**; a `canton` value is echoed back and flagged as not applied β in the JSON envelope via `match_type: "fuzzy"` plus a note, in Markdown as a warning line above the table. This matters most when there are no warnings at all: βno active warnings" plus a canton in the request otherwise reads as an all-clear for that canton.
- **Gauging stations / canton filter**: `env_hydro_stations` no longer serves `canton`. The only source that carried the canton code β `hydrodaten.admin.ch/lhg/az/json/mobile_stations.json` β is **decommissioned (404)**, and LINDAS has no canton attribute. A request with `canton` set returns an explanation instead of a station list; use `water_body`, or fetch the full list (233 stations) unfiltered.
- **Bathing water quality (`env_bathing_water`)**: reads the LINDAS data cube `foen/ubd01041prod` β the only hydro cube with a real multi-year time series (seasonal samples since 2020). Data is refreshed **annually after the bathing season** (no real-time monitoring), and the survey covers only the officially reported bathing sites (many popular lidos are not part of it). The licence is declared at graph/dataset level, not on the cube; every response therefore carries an explicit licence field β with an honest Β«not declaredΒ» note where none exists. See [`docs/probe-lindas-hydro.md`](docs/probe-lindas-hydro.md) (addendum N1βN7).
- **No groundwater data in LINDAS**: verified 2026-07-24 via multilingual cube search β LINDAS contains **no groundwater cube** (NAQUA groundwater levels are not available via SPARQL).
- **NABEL**: Near-real-time data only; no historical time series via this server.
- **Natural hazards (`env_hazard_overview` / `env_hazard_regions`)**: the former `naturgefahren.ch/api/v1/warnings/*` REST endpoints were **decommissioned (2026)** and β verified 2026-07-26 β there is **no stable, documented public JSON feed** for the aggregated warnings (MeteoSwiss OGD/STAC, opendata.swiss and the undocumented app API were all checked). Rather than a fragile scrape, both tools are now **network-free orientation/router tools**: they deterministically point to this server's dedicated live tools (floodβ`env_flood_warnings`, avalancheβ`env_avalanche_bulletin`, wildfireβ`env_wildfire_danger`, snowβ`env_snow_current`) and the official portals. Aggregated **weather warnings** (storm/thunderstorm/heat) are MeteoSwiss's domain and belong to `meteoswiss-mcp`. See [`docs/probe-naturgefahren-hazards.md`](docs/probe-naturgefahren-hazards.md).
- **Wildfire danger (`env_wildfire_danger`)**: `waldbrandgefahr.ch` replaced its REST API with a Rails/React app in 2026; there is **no stable JSON endpoint**. Current danger levels are read via a **two-step, HTML-borne contract**: the homepage's `data-react-props` yields a *signed* ActiveStorage blob URL (`warnMapJsonPath`) plus the canton mapping, which is then fetched. A schema-guard degrades gracefully if that structure changes. Unfiltered results are capped at 40 regions (highest levels first); filter by `canton` for a full cantonal list. See [`docs/probe-naturgefahren-waldbrand.md`](docs/probe-naturgefahren-waldbrand.md).
- **Hunting statistics (`env_hunting_stats`)**: The `jagdstatistik.ch` backend is **undocumented** (a content-negotiated web-app endpoint). A schema-guard degrades gracefully if the structure changes. Species/canton/datatype lookups are embedded (harvested 2026-07-19); figures are fetched live for 2015β2024. **Licence (researched 2026-07-19):** the data is owned by **BAFU** (compiled from cantonal offices; site tech by Wildtier Schweiz) and is **not** published as a licensed dataset on opendata.swiss; **no explicit licence is stated on the source**. Responses therefore require source attribution to BAFU; formal licence confirmation from BAFU is still pending. See [`docs/probe-jagdstatistik.md`](docs/probe-jagdstatistik.md).
- **Road and railway noise are out of scope (`ch.bafu.laerm-*`, `ch.bav.laermbelastung-*`)**: verified 2026-07-28. The BAFU road-noise layers `ch.bafu.laerm-strassenlaerm_tag` / `_nacht` answer the same `identify` request with **HTTP 400** β they are pure raster services (`type: wmts`, `tooltip: false`) with no attribute query, so a point lookup is technically impossible. Railway noise is different: `ch.bav.laermbelastung-eisenbahn_*` **does** answer with HTTP 200 and real attributes (`de_es`, `de_pointofdetermination`), so it is *queryable* but deliberately **not connected** β the server's tool budget is exhausted at 21 and rail noise would need its own period/attribute model. Mnemonic: *aircraft noise has lines, road noise has only pixels* β rail noise would have data, but is a conscious omission. See [`docs/probe-fluglaerm.md`](docs/probe-fluglaerm.md).
- **Aircraft noise is a cut-off-date cadastre, not a live service (`env_noise_*`)**: `validfrom` ranges from **01.03.2009** (CDB GenΓ¨ve) to **16.04.2024** (LBK St. Gallen-Altenrhein), and each airfield is revised individually and without notice. `source_freshness` therefore never claims "live" β it carries the `validfrom` of the register actually matched. Coverage is limited to the surroundings of airfields; most of Switzerland has no cadastre at all, which the tool reports explicitly rather than returning an empty list. A zero-hit result at a small radius is **ambiguous** (outside any cadastre *or* inside the innermost contour β on the Kloten runway both look identical at 100 m), so the tool re-queries once at a far radius and distinguishes `no_cadastre` from `wide_area_only`.
- **LSV limit check excludes military airfields**: Annex 5 of the Noise Abatement Ordinance applies expressly to *civil* airfields. For military airfields Annex 8 governs; it was not verified, so `env_noise_limits_check` **refuses** the check for `period="military"` and points to the correct basis instead of applying a plausible-looking wrong table.
### Responsibility matrix β water, snow & precipitation (delineation vs. `meteoswiss-mcp`)
To avoid duplicating **water, snow and precipitation** data across the portfolio,
responsibilities are split as follows. `meteoswiss-mcp` owns atmospheric
precipitation and weather; `swiss-environment-mcp` owns surface waters
(BAFU domain: discharge, water level, water temperature, bathing quality),
snow on the ground and avalanche danger. Checked against the actual LINDAS
cube dimensions (2026-07-24): there is **no overlap in measured quantities**.
| Data | swiss-environment-mcp (BAFU / SLF) | meteoswiss-mcp (MeteoSwiss) |
|---|---|---|
| Discharge (mΒ³/s) | β
`env_hydro_current` (LINDAS `hydro/river`) | β |
| Water level (m a.s.l.) | β
`env_hydro_current` (LINDAS `hydro/river` + `lake`) | β |
| Water temperature (Β°C) | β
`env_hydro_current` | β (measures air temperature) |
| Bathing water quality (E.coli etc.) | β
`env_bathing_water` (LINDAS `ubd0104`) | β |
| Snow depth on the ground (`HS`) | β
`env_snow_current` (SLF IMIS) | β |
| Fresh snow 24 h (`HN_1D`) | β
`env_snow_current` (SLF IMIS) | β |
| Avalanche danger level | β
`env_avalanche_bulletin` (SLF, EAWS 1β5) | β |
| Snowfall as a current weather condition | β | β
`meteo_current` / `meteo_forecast` (weather code) |
| Precipitation amount (mm): measurement network, forecast, climate normals | β | β
`meteo_current` / `meteo_forecast` / `meteo_climate_normals` |
| Precipitation at SLF IMIS mountain stations | β
only as snow-cover context, **no standalone precipitation tool** | (MeteoSwiss network) |
| Weather warnings (storm, thunderstorm, heat) | β | β
`meteo_warnings` |
| Natural-hazard warnings (flood, avalanche, wildfire) | β
`env_flood_warnings`, `env_hazard_*`, `env_wildfire_danger` | β |
**Rule:** everything **in and on water bodies** (discharge, level, water
temperature, bathing quality), snow **on the ground** and **avalanche** danger
belong to `swiss-environment-mcp` (BAFU/SLF); **atmospheric precipitation**
(rain/snowfall as mm) plus weather, forecast, warnings and climate normals belong
to `meteoswiss-mcp`. The SLF IMIS precipitation endpoint (`RR_10MIN_SUM`) is
deliberately **not** wired up as a tool, so it does not duplicate MeteoSwiss. The
snow/avalanche tools are live (see [`docs/probe-slf.md`](docs/probe-slf.md)).
*TODO (out of scope here): mirror this matrix in the `meteoswiss-mcp` README β
that repository is not part of this change.*
---
## Testing
```bash
# Unit tests (no API keys or network required)
PYTHONPATH=src pytest tests/ -m "not live"
# Integration tests (requires live BAFU APIs)
PYTHONPATH=src pytest tests/ -m "live"
# Linting
ruff check src/
```
---
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) (English) Β· [CONTRIBUTING.de.md](CONTRIBUTING.de.md) (German)
---
## Security
Security policy and posture: [SECURITY.md](SECURITY.md) (English) Β· [SECURITY.de.md](SECURITY.de.md) (German).
Full security architecture: [`docs/security.md`](docs/security.md).
---
## Changelog
See [CHANGELOG.md](CHANGELOG.md)
---
## License
MIT License β see [LICENSE](LICENSE)
Source data is subject to BAFU terms of use. Attribution to BAFU is required when using their data.
---
## Author
Hayal Oezkan Β· [github.com/malkreide](https://github.com/malkreide)
---
## Credits & Related Projects
- **Data:** [BAFU / Bundesamt fΓΌr Umwelt](https://www.bafu.admin.ch) Β· [hydrodaten.admin.ch](https://hydrodaten.admin.ch) Β· [naturgefahren.ch](https://naturgefahren.ch) Β· [opendata.swiss](https://opendata.swiss/en/organization/bafu)
- **Protocol:** [Model Context Protocol](https://modelcontextprotocol.io/) β Anthropic / Linux Foundation
- **Related:**
| Server | Description |
|---|---|
| [zurich-opendata-mcp](https://github.com/malkreide/zurich-opendata-mcp) | City of Zurich open data (OSTLUFT air quality, weather, parking, geodata) |
| [swiss-transport-mcp](https://github.com/malkreide/swiss-transport-mcp) | Swiss public transport β OJP 2.0 journey planning, SIRI-SX disruptions |
| [swiss-road-mobility-mcp](https://github.com/malkreide/swiss-road-mobility-mcp) | GBFS shared mobility, EV charging, DATEX II traffic |
| [swiss-statistics-mcp](https://github.com/malkreide/swiss-statistics-mcp) | BFS STAT-TAB β 682 statistical datasets |
**Synergy example:** *"What was the air quality at Schulhaus Leutschenbach today β and how does it compare to the national NABEL average?"*
β `zurich-opendata-mcp` (OSTLUFT, local) + `swiss-environment-mcp` (NABEL, national)
- **Portfolio:** [Swiss Public Data MCP Portfolio](https://github.com/malkreide)
<!-- mcp-name: io.github.malkreide/swiss-environment-mcp -->
<!-- BEGIN GENERATED: install -->
## Installation
Run 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`):
```json
{
"mcpServers": {
"swiss-environment-mcp": {
"command": "uvx",
"args": [
"swiss-environment-mcp"
]
}
}
}
```
<!-- END GENERATED: install -->