The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Swiss Environment MCP listing page.
🇨🇭 Part of the Swiss Public Data MCP Portfolio
MCP server connecting AI models to Swiss environmental data from BAFU – air quality, hydrology, natural hazards, wildfire danger and open environmental datasets.
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 →
Or with uvx (no permanent installation):
Or via pip:
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?"
Minimal (recommended):
Config file locations:
~/Library/Application Support/Claude/claude_desktop_config.json%APPDATA%\Claude\claude_desktop_config.jsonAfter saving, restart Claude Desktop completely.
For use via claude.ai in the browser (e.g. on managed workstations without local software):
Render.com (recommended):
render.yaml automaticallyhttps://your-app.onrender.com/sseDocker:
💡 "stdio for the developer laptop, SSE for the browser."
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.
| 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 |
| 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) |
| 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 |
| 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 |
| 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 |
| 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 |
| 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.
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).
"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 → 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.
| 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 |
| 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, hydrodaten.admin.ch, naturgefahren.ch, waldbrandgefahr.ch |
lindas/ moduleAll 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).
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:
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.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.
| Source | Data | Licence |
|---|---|---|
| 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 | Water levels, flow rates, temperatures (REST fallback) | BAFU OGD |
| naturgefahren.ch | Natural hazard bulletin (SLF/BAFU) | BAFU/SLF |
| waldbrandgefahr.ch | Wildfire danger index | BAFU |
| SLF data service | Snow depth, new snow (IMIS); avalanche bulletin | SLF (WSL) CC BY 4.0 |
| jagdstatistik.ch | Federal hunting statistics (cull, game loss, population) | BAFU — source attribution required (no explicit licence published) |
| api3.geo.admin.ch | BAZL aircraft noise cadastre (identify, LV95) | swisstopo / BAZL — free reuse with attribution |
| opendata.swiss | 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.
Single-module layout (rationale, audit ARCH-011): the 21 tools live in one
server.pyrather than atools/package. They are thin, uniform wrappers overapi_client.pysharing 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.
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 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 together.
tool-snapshot.json; CI fails until the snapshot
is regenerated and a CHANGELOG entry + version bump are added.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. Security architecture (SSRF/egress, secret
management, lethal-trifecta assessment): docs/security.md.
Scaling/session strategy: docs/scaling.md.
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.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.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.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.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 (addendum N1–N7).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.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.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.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.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.env_noise_limits_check refuses the check for period="military" and points to the correct basis instead of applying a plausible-looking wrong table.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).
TODO (out of scope here): mirror this matrix in the meteoswiss-mcp README —
that repository is not part of this change.
See CONTRIBUTING.md (English) · CONTRIBUTING.de.md (German)
Security policy and posture: SECURITY.md (English) · SECURITY.de.md (German).
Full security architecture: docs/security.md.
See CHANGELOG.md
MIT License — see LICENSE
Source data is subject to BAFU terms of use. Attribution to BAFU is required when using their data.
Hayal Oezkan · github.com/malkreide
| Server | Description |
|---|---|
| zurich-opendata-mcp | City of Zurich open data (OSTLUFT air quality, weather, parking, geodata) |
| swiss-transport-mcp | Swiss public transport – OJP 2.0 journey planning, SIRI-SX disruptions |
| swiss-road-mobility-mcp | GBFS shared mobility, EV charging, DATEX II traffic |
| 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)
Run via 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):