
Swiss Environment Mcp
io.github.malkreide由 GitHub 帳號 malkreide 發布v0.7.0更新於 Oct 24, 2026
BAFU environmental data, NABEL air quality, hydrology
概覽
本機 MCP 伺服器,讓助理以唯讀方式存取瑞士公共環境資料:空氣品質、水文、自然災害、積雪、狩獵統計與飛機噪音區。
- 可以做什麼
- 空氣品質:列出 NABEL 測站、讀取目前污染物數值,並與瑞士 LRV 及 WHO 2021 限值比較。水文與水體:列出水文測站,讀取目前水位、流量與水溫,查詢洪水警報與浴場水質。自然災害與積雪:依邦查詢野火危險等級、雪崩通報等級、積雪測站與目前積雪深度,並提供官方災害入口網站的指引。狩獵統計:列出統計物種,依物種、邦與年份讀取獵殺與損失資料。開放資料目錄:在 opendata.swiss 搜尋 BAFU 資料集並取得中繼資料與下載連結。飛機噪音:依 LV95 座標查詢 BAZL 噪音地籍,並將音量與 LSV 限值比較。
- 適用情境
- 適合需要快速了解瑞士目前環境狀況或官方環境資料集的情境,例如某測站的空氣品質、水位、洪水或雪崩警報、野火危險、狩獵資料,或某地點的飛機噪音暴露。較適合瑞士相關查詢與初步判斷,而非歷史趨勢分析。
- 執行需求
- 以 stdio 本機程序執行,需要 Python 3.11+,可透過 uvx 或 pip 安裝。無需 API 金鑰或身分驗證。需要網路存取瑞士公共資料來源(LINDAS、SLF、waldbrandgefahr.ch、jagdstatistik.ch、api3.geo.admin.ch、opendata.swiss)。飛機噪音查詢必須使用 LV95 座標,而非 WGS84 經緯度。
安裝
在 SourceWeft 中
- 開啟 儀表板中的 Swiss Environment Mcp,將其新增到工作區。
- 為需要使用其工具的對話啟用該服務。
Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。
其他 MCP 客戶端
參照 儲存庫 中的啟動說明。
README
🇨🇭 Part of the Swiss Public Data MCP Portfolio
🌿 swiss-environment-mcp
[Version] [License: MIT] [Python 3.11+] [MCP] [CI] [Data Source]
MCP server connecting AI models to Swiss environmental data from BAFU – air quality, hydrology, natural hazards, wildfire danger and open environmental datasets.
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 →
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
Or with uvx (no permanent installation):
Or via pip:
Quickstart
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):
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):
- Push/fork the repository to GitHub
- On render.com: New Web Service → connect GitHub repo
- Render detects
render.yamlautomatically - In claude.ai under Settings → MCP Servers, add:
https://your-app.onrender.com/sse
Docker:
💡 "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)
💧 Hydrology (5 tools)
🏔️ Natural Hazards (3 tools)
❄️ Snow & Avalanches / SLF (3 tools)
🦌 Hunting & Wildlife (2 tools)
📊 Environmental Data Catalogue (2 tools)
✈️ Aircraft Noise / BAZL noise cadastre (3 tools)
⚖️ 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).
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 → 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.
Example Use Cases
🛡️ Safety & Limits
Architecture
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:
- 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_freshnesswould report thevalidfromas 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. - 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.
Data Sources
All data: publicly accessible, no authentication required.
Attribution required: BAFU / SLF (WSL) must be cited as the source when using their data.
Project Structure
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.
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.
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.
That pin measures the SDK, though, not this server: it would stay green even if
the server did not answer the modern era at all. The measured half lives in
tests/test_modern_protocol.py — real requests
in both eras through the production app (build_cors_app(), the same one uvicorn
starts in operation), with negative controls: a missing _meta envelope, a
mismatched routing header, an initialize on a modern connection.
What 2026-07-28 actually means for this server
- No session, no
initialize— and so noserverInfoin a handshake result. The server introduces itself throughserver/discoverand theio.modelcontextprotocol/serverInfo_metastamp carried by every response. Name, title, description, website and version come from theMCPServerconstructor; the version from package metadata, never a literal. Without it the server reportsversion: "", and a stateless client cannot tell two releases apart. - Routing headers.
Mcp-Method,Mcp-NameandMCP-Protocol-Versionmust match the request body, or the request is rejected with-32020. That is why they appear inCORS_ALLOW_HEADERS— a browser that may not send them gets a rejection, not an answer. - Logging is a per-request opt-in (SEP-2577). The capability is gone;
without the reserved
io.modelcontextprotocol/logLevel_metakey the server sends nothing. WithlogLevel: "warning"the response switches totext/event-streamand carries the entry. Both directions are measured. - Freshness hints (SEP-2549): the listing methods carry
ttlMsandcacheScope— seeCACHE_HINTS.
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-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 aCHANGELOGentry + 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. Security architecture (SSRF/egress, secret
management, lethal-trifecta assessment): docs/security.md.
Scaling/session strategy: docs/scaling.md.
Known Limitations
- Hydrology via LINDAS:
env_hydro_current,env_hydro_stationsandenv_flood_warningsquery 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. Seedocs/probe-lindas-hydro.md. - Historical hydrology /
env_hydro_history(BUG-01 resolved): the oldhydrodaten.admin.ch/lhg/az/*REST endpoints (hourly CSV,warnings.json, station JSON) are decommissioned (404).env_flood_warningsnow uses LINDASdangerLevelinstead. 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 ([email protected]).env_hydro_historyreturns the latest LINDAS value plus this access path. - Flood warnings:
env_flood_warningsreads LINDASdangerLevel. A canton filter is not available there (LINDAS carries no canton code), so the response is always nationwide; acantonvalue is echoed back and flagged as not applied — in the JSON envelope viamatch_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_stationsno longer servescanton. 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 withcantonset returns an explanation instead of a station list; usewater_body, or fetch the full list (233 stations) unfiltered. - Bathing water quality (
env_bathing_water): reads the LINDAS data cubefoen/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. Seedocs/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 formernaturgefahren.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 tometeoswiss-mcp. Seedocs/probe-naturgefahren-hazards.md. - Wildfire danger (
env_wildfire_danger):waldbrandgefahr.chreplaced 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'sdata-react-propsyields 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 bycantonfor a full cantonal list. Seedocs/probe-naturgefahren-waldbrand.md. - Hunting statistics (
env_hunting_stats): Thejagdstatistik.chbackend 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. Seedocs/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 layersch.bafu.laerm-strassenlaerm_tag/_nachtanswer the sameidentifyrequest 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. Seedocs/probe-fluglaerm.md. - Aircraft noise is a cut-off-date cadastre, not a live service (
env_noise_*):validfromranges 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_freshnesstherefore never claims "live" — it carries thevalidfromof 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 distinguishesno_cadastrefromwide_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_checkrefuses the check forperiod="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.
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.
Testing
Contributing
See CONTRIBUTING.md (English) · CONTRIBUTING.de.md (German)
Security
Security policy and posture: SECURITY.md (English) · SECURITY.de.md (German).
Full security architecture: docs/security.md.
Changelog
See CHANGELOG.md
License
MIT License — see 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
Credits & Related Projects
- Data: BAFU / Bundesamt für Umwelt · hydrodaten.admin.ch · naturgefahren.ch · opendata.swiss
- Protocol: Model Context Protocol – Anthropic / Linux Foundation
- Related:
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
Installation
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):
來源:README.md,提交 45094cd
工具
0版本歷史
2- v0.7.0最新Sep 20, 2026
- v0.6.0Sep 16, 2026
評分
已滿足適用檢查項的 50%
該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。
- 近 30 天內可連通25 分
請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。
- 已宣告認證方式10 分
請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。
- 已列出工具15 分
請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。
- 描述必需15 分
- 圖示10 分
- 文件15 分
- 授權條款10 分