Skip to content

Releases: malkreide/meteoswiss-mcp

v0.6.1

Choose a tag to compare

@malkreide malkreide released this 02 Aug 21:14
cfdcba2

[0.6.1] - 2026-08-02

Behoben

  • structlog hatte keine Obergrenze, und der Index fuehrt bereits einen Major
    oberhalb der Untergrenze.
    Deklariert war structlog>=24.0.0; auf PyPI liegt
    26.1.0. Das Artefakt aendert sich nicht — die Antwort des Resolvers auf
    die naechste frische Installation schon, und genau so wurde
    swiss-energy-mcp 0.3.3 uninstallierbar, als mcp 2.0.0 das Modul entfernt
    hat, das es importierte.

    Neu structlog>=24.0.0,<27. Die Grenze ist gemessen, nicht geraten: dieses Paket installiert
    und importiert heute gegen structlog 26.1.0, die Obergrenze laesst also zu,
    was nachweislich funktioniert, und stoppt nur den naechsten, unbekannten
    Major.

Ein Abhaengigkeitsbereich erreicht die Nutzenden nur ueber ein neues
Release, daher der Versions-Bump. Am Code aendert sich nichts.

v0.6.0

Choose a tag to compare

@malkreide malkreide released this 30 Jul 14:56
40053ea

[0.6.0] - 2026-07-30

Reparatur-Release. In 0.5.0 lieferten drei der sechs Tools nichts
meteo_current für jede Station einen 404, meteo_forecast und
meteo_school_check gar keine Daten. Drei unabhängige Ursachen, zwei davon
Änderungen bei Datenquellen, eine ein Fehler im Server selbst. Alle drei sind
live gegen die echten APIs verifiziert.

Für Clients nicht breaking: Tool-Namen, Parameter und Rückgabetypen sind
unverändert. Einzig die Beschreibung von meteo_forecast wurde präzisiert
(siehe «Changed»), was tool-hashes.json ändert.

Behoben

  • Ortsnamen mit Zusatz waren gar nicht auflösbar
    (#37). Die
    Geocoding-API kennt nur einzelne Ortsnamen und liefert für
    «Schulhaus Leutschenbach Zürich» nichts. _geocode() schickte den vollen
    String in beiden Versuchen — der «Fuzzy»-Retry liess nur die
    Sprachrestriktion weg und kürzte die Anfrage nie. Damit scheiterten auch die
    Beispiele, die die Tools selbst dokumentieren (meteo_forecast-Docstring und
    die location-Beschreibung von meteo_school_check).

    Neu werden bei Misserfolg führende Tokens nach und nach weggelassen —
    Schweizer Ortsangaben sind konventionell Gattungswort… Ort Stadt, das
    verallgemeinert also von spezifisch nach allgemein und endet bei der Stadt.
    Zusätzlich wird jedes führende Token einzeln probiert, aber nur mit
    Namensprüfung
    : «Leutschenbach» → Leutschenbach wird angenommen,
    «Schulhaus» → Dübendorf / Schulhaus Wil verworfen. Ohne diese Prüfung
    bekäme eine Zürcher Anfrage stillschweigend Wetter aus einer anderen
    Gemeinde — schlimmer als der bisherige harte Fehler.

    Live verifiziert:

    Anfrage Ergebnis
    Schulhaus Leutschenbach Zürich Leutschenbach, ZH (47.4175, 8.5648)
    Sportanlage Heerenschürli Zürich Zurich, ZH (47.3667, 8.5500)
    Zürich / Bern unverändert exact, ein einziger Request

    match_type kennt dafür neu "shortened" — Aufrufende sehen damit, dass die
    Antwort allgemeiner ist als die Frage. Ein Volltreffer löst weiterhin genau
    einen Request aus; gekürzt wird nur, wenn der volle String scheitert.

  • meteo_forecast und meteo_school_check lieferten gar nichts mehr
    (#35). Open-Meteo hat
    die provider-eigenen Pfade abgeschafft; /v1/meteoswiss antwortet mit 404.
    Modelle werden neu über models= auf /v1/forecast gewählt.

    Ein reiner URL-Tausch reicht nicht, denn keines der MeteoSwiss-Modelle
    liefert, was die beiden Tools zusagen. Live gemessen für
    meteoswiss_icon_seamless (ICON-CH1 + ICON-CH2):

    • Reichweite 5 Tage. forecast_days=16 liefert darüber hinaus nur
      Nullwerte — das ist der ICON-CH2-Horizont, kein Parameterproblem.
      meteo_forecast verspricht aber bis zu 16 Tage.
    • Kein UV-Index, auch nicht stündlich (0 von 72 Werten über 3 Tage).
      Open-Meteo bezieht UV aus CAMS, nicht aus dem Modelloutput.
      meteo_school_check warnt aber ab UV 6.

    Der Server holt deshalb neu beide Modelle und mischt sie entlang der
    Zeitachse: MeteoSwiss ICON gewinnt überall, wo es einen Wert hat, None
    fällt auf best_match zurück. Dieselbe Regel erledigt die 5-Tage-Grenze und
    die UV-Lücke. Gemischt wird bewusst über Zeitstempel statt Listenindizes —
    der ICON-Block ist kürzer, und ein Indexversatz würde Werte still auf den
    falschen Tag schieben.

    Die Herkunft steht in jeder Antwort, statt im Ungefähren zu bleiben: die
    Markdown-Fussnote und die JSON-Felder modell / modell_details weisen aus,
    welche Tage aus MeteoSwiss ICON stammen, ab wann best_match übernimmt und
    dass UV durchgehend von dort kommt. Fällt der ICON-Request aus, trägt
    best_match die Antwort allein — und sagt das ebenfalls, statt einen
    Totalausfall zu produzieren.

    Kein Test hätte das finden können: die Unit-Tests deckten ausschliesslich
    Geocoding-Fehlerpfade ab, der erfolgreiche Prognosepfad war komplett
    ungetestet. Neu 11 Tests für Merge-Logik (inklusive Zeitachsen-Versatz),
    Provenance-Label, den vollen Hybrid-Pfad in Markdown und JSON, den
    ICON-Ausfall und die UV-Herkunft in meteo_school_check.

Changed

  • meteo_forecast-Beschreibung präzisiert — sie nannte pauschal
    «MeteoSwiss ICON-CH1/CH2-EPS» und verschwieg damit, dass Tage jenseits von 5
    und der UV-Index aus best_match stammen. tool-hashes.json entsprechend neu
    generiert (Rug-Pull-Signal, SEC-022). Für Clients nicht breaking: Parameter
    und Rückgabetyp sind unverändert, days akzeptiert weiterhin 1–16.

  • meteo_current lieferte für jede Station 404
    (#33). Die STAC-Item-ID
    ist der nackte Stationscode in Kleinschreibung (…/items/klo); der Server
    stellte ihm die Collection-ID voran (…/items/ch.meteoschweiz.ogd-smn-klo).
    Live gegen die BGDI-API verifiziert: /items/klo → 200, die Präfix-Variante
    und die Grossschreibung → je 404. Die URL wurde an drei Stellen dupliziert
    (Fetch, Fehlermeldung, JSON-Provenance) — jetzt einmal in
    _smn_stac_item_url().

    Beim Beheben kamen drei weitere Fehler zum Vorschein, die der 404 verdeckt
    hatte — sie hätten nach dem URL-Fix plausibel aussehende, aber falsche Daten
    geliefert:

    • Die Asset-Auswahl konnte nie greifen. Gesucht wurde nach /now/ im
      Pfad, doch die Granularität steckt im Dateinamen
      (ogd-smn_klo_t_now.csv); ein Verzeichnis /now/ existiert nicht. Der
      Fallback nahm daraufhin das erste CSV im Item — d_historical, also
      Tageswerte ab 1980, ausgegeben als «aktuelle Beobachtung». Neu wird gezielt
      _t_now gewählt, ersatzweise _t_recent; gibt es beides nicht, ist das ein
      Fehler statt eines stillen Griffs ins Archiv.
    • Der Zeitstempel war immer . Gelesen wurde time/Date/datum, die
      OGD-CSV führt aber reference_timestamp.
    • Die Zeile «Luftdruck (reduziert auf Meeresniveau)» blieb immer leer.
      prestah0 gibt es in der CSV nicht; der QNH-Wert heisst pp0qnhs0.

Tests

Alle drei Fehler oben hatten einen Test, der sie hätte finden müssen, aber so
formuliert war, dass auch der Fehlerfall ihn erfüllte. Das ist das eigentliche
Thema dieses Releases:

  • meteo_current (live): prüfte "KLO" in result or "Zürich" in result
    beides steht auch in der Fallback-Fehlermeldung. Der Test lief also durch
    einen Totalausfall hindurch grün. Verlangt jetzt echte Messwerte.

  • meteo_forecast / meteo_school_check: der erfolgreiche Prognosepfad war
    komplett ungetestet, abgedeckt waren nur Geocoding-Fehlerpfade. Zudem mockten
    die Tests die eigene Endpoint-Konstante — sie konnten das Abschalten upstream
    gar nicht bemerken.

  • _geocode (live): prüfte nur einen Koordinatenbereich (8.4–8.7), den
    Dübendorf mit 8.62 ebenfalls erfüllt — der Fehlgriff auf eine andere Gemeinde
    wäre durchgewinkt worden. Verlangt jetzt den Ortsnamen im Ergebnis.

  • Cache-Isolation zwischen Tests. Der TTL-Cache ist modul-global und
    überlebte den einzelnen Test. Ein Test sah dadurch Einträge eines früheren,
    umging sein eigenes respx-Mock und schlug je nach Ausführungsreihenfolge fehl
    — oder bestand aus dem falschen Grund. Eine autouse-Fixture räumt jetzt vor
    und nach jedem Test auf.

v0.5.0

Choose a tag to compare

@malkreide malkreide released this 30 Jul 13:27
5513e9b

[0.5.0] - 2026-07-30

Reparatur-Release für einen kaputten PyPI-Stand. mcp 2.0.0 erschien am
28.07.2026 und entfernte mcp.server.fastmcp — genau das Modul, das v0.4.0
importierte. Weil v0.4.0 keine Obergrenze auf mcp hatte, zog jede frische
Installation aus PyPI die 2.0.0 und scheiterte sofort beim Import. Das
betrifft uvx meteoswiss-mcp genauso wie pip install meteoswiss-mcp
(#31).

Der Code auf main war bereits migriert; dieses Release bringt die Korrektur
zu den Nutzenden. Für Clients ist nichts breaking — die 6 Tools, ihre Schemata
und das Wire-Format sind unverändert (tool-hashes.json bleibt gleich).

Behoben

  • Frische Installationen starten wieder. Der ModuleNotFoundError: No module named 'mcp.server.fastmcp' beim Start von uvx meteoswiss-mcp ist
    weg. Ursache war nicht der Code, sondern die Release-Lücke: der Fix lag seit
    #29/#30 auf main, aber PyPI führte weiterhin v0.4.0.

Changed

  • Migration auf das mcp-Python-SDK 2.x (#30). Der Server-Import wechselt
    von mcp.server.fastmcp auf mcp.server.mcpserver, FastMCP heisst neu
    MCPServer. Ohne Kompatibilitäts-Shim im SDK ist der Boden hart: der Pin
    lautet jetzt mcp[cli]>=2.0.0,<3 statt >=1.28.1. Weitere Anpassungen im
    SDK, die mitgezogen werden mussten:

    • mcp_types snake_cased sämtliche Modell-Attribute (inputSchema
      input_schema, isErroris_error, …). Das sind Pydantic-Aliase, das
      Wire-Format bleibt identisch.
    • McpError heisst MCPError und nimmt (code, message, data=None) direkt
      entgegen statt eine ErrorData-Instanz zu umschliessen.
    • call_tool() liefert ein CallToolResult statt des 1.x-Tupels
      (content, structured).
    • MCPServer.__init__ akzeptiert host/port/stateless_http/
      transport_security nicht mehr — das sind neu run()- bzw. App-Kwargs.
    • Der Lowlevel-Server ist als _lowlevel_server erreichbar und exponiert
      kein request_handlers-Mapping mehr.

    Gegen eine aufgezeichnete 1.x-Baseline verifiziert: die Suite besteht vor
    und nach der Migration exakt dieselben Tests, ohne neue Fehlschläge und ohne
    stillschweigend übersprungene Tests.

Added

  • serverInfo.version im initialize-Handshake. Der Server meldete bisher
    einen leeren Version-String — ausgerechnet die Angabe, die bei einem
    Bug-Report als Erstes gebraucht wird. Sie kommt neu aus der installierten
    Distribution (importlib.metadata), kann also nicht gegenüber dem Paket
    veralten; im Source-Checkout ohne Installation lautet sie 0.0.0+unknown.

Docs

  • README (DE/EN) und CONTRIBUTING (DE/EN) sprachen weiterhin von «FastMCP» und
    nannten mcp[cli]>=1.0.0 als SDK-Version — beides auf MCPServer bzw.
    mcp[cli]>=2.0.0,<3 korrigiert.

v0.4.0

Choose a tag to compare

@malkreide malkreide released this 26 Jul 12:50
073c291

[0.4.0] - 2026-07-26

Feature-Release: meteo_warnings liefert neu echte Live-Warnungen aus dem
öffentlichen MeteoSwiss-App-Backend statt nur eines Linkstacks. Kein Breaking
Change — bestehende Aufrufe funktionieren unverändert (neue Parameter plz /
language sind optional).

Added

Live-Wetterwarnungen (meteo_warnings)

  • meteo_warnings liefert jetzt echte, aktive Warnungen statt nur eines
    Linkstacks. Live-Quelle ist das öffentliche MeteoSwiss-App-Backend
    (app-prod-ws.meteoswiss-app.ch/v1/plzDetail) — öffentlich und ohne Auth.
    Aggregierte Wetterwarnungen (Sturm, Gewitter, Hitze, Waldbrand, Frost,
    Schnee, …) mit Typ- und Stufen-Label, betroffener Warnregion, Gültig-ab und
    offiziellem Handlungslink; zusätzlich eine Vorausschau-Sektion für noch
    nicht aktive Warnungen.
  • Drei Abfrage-Granularitäten: plz="8001" (ortsgenau), canton="TI"
    (Kanton via Hauptort-PLZ) oder ohne Filter → landesweite Aggregation über je
    eine Kantonshauptort-PLZ pro Kanton (26 Abfragen, dedupliziert, nach Typ
    gruppiert). Einzelne fehlgeschlagene PLZ-Abfragen degradieren den Aufruf
    nicht (Teilergebnis + Hinweis).
  • Mehrsprachige Warntexte via neuen Parameter language (de/fr/it/
    en, Default de) — wird als Accept-Language an die App-API durchgereicht.
  • Neuer Egress-Host app-prod-ws.meteoswiss-app.ch in der Allow-List.
    MCP_WARNINGS_API_URL überschreibt die App-Quelle weiterhin (Vorbereitung
    auf die künftige offizielle OGD-Warnings-REST-API).
  • warnType-Mapping (7=Hitze, 10=Waldbrand gegen die natural-hazards.ch-Slugs
    verifiziert) mit Slug-Fallback für unbekannte Codes; Warnstufen 1–5.

Changed

  • meteo_warnings-Tool-Definition erweitert (neue Parameter plz,
    language; aktualisierte Description) → tool-hashes.json neu generiert
    (Rug-Pull-Signal, SEC-022). Für Clients nicht breaking: bestehende Aufrufe
    ohne plz/language funktionieren unverändert.

Tests

  • 12 neue Tests für die App-API-Warnungen (respx-gemockt, kein Netzwerk):
    PLZ-Detailansicht, landesweite Aggregation, JSON-Schema, unbekannter Kanton,
    Fehler-Degradation, PLZ-/Sprach-Validierung sowie Unit-Tests für
    _warn_type_label, _epoch_millis_to_iso, _dedupe_warnings und die
    Egress-Allow-List.

v0.3.0 — Phase 2: Caching + Climate Data + Warnings Hook

Choose a tag to compare

@malkreide malkreide released this 21 May 04:49
8069cea

Phase-2-Release. Kein Breaking Change gegenüber 0.2.0 — alle neuen Features sind opt-in via ENV.

Highlights

  • Performance: TTL-Cache für alle Upstream-Calls (STAC, Open-Meteo, Geocoding, opendata.swiss). Asyncio-safe, per-Endpoint-TTLs via ENV, default 5 min für Live-Daten / 1 h für Stammdaten.
  • Klimanormwerte 1991–2020 für 19 SMN-Stationen in data/climate-normals.json, ingested aus der offiziellen MeteoSwiss-NBCN-Publikation: BAS, BER, CHU, DAV, GVE, INT, JUN, KLO, LUG, LUZ, PIL, PUY, REH, SAE, SIO, SMA, STG, TAE, WAE (REC nicht in NBCN). Vorher: nur 5 eingebettete Stationen.
  • Runtime-Lookup für Klimadaten via MCP_CLIMATE_NORMALS_URL_TEMPLATE — fängt Stationen ab, die weder eingebettet noch in der JSON sind (z.B. via STAC).
  • Strukturierte Warnings-API via MCP_WARNINGS_API_URL — Hook für die geplante MeteoSwiss-OGD-Phase-2-API. Schema-tolerant (GeoJSON / warnings-Array / items).
  • Ingest-Skript: python scripts/ingest_climate_normals.py --dir <path> scannt einen MeteoSwiss-NBCN-Dump-Ordner, filtert relevante Parameter, schreibt data/climate-normals.json (Tab-separated TSV, cp1252-encoded).

Neue ENV-Variablen

Variable Default Zweck
MCP_CACHE_ENABLED 1 TTL-Cache an/aus
MCP_CACHE_TTL_STAC 300 SMN-Now-Beobachtungen
MCP_CACHE_TTL_OPEN_METEO 600 ICON-Prognosen
MCP_CACHE_TTL_GEOCODING 3600 Geocoding
MCP_CACHE_TTL_OPENDATA 3600 opendata.swiss-Katalog
MCP_CACHE_TTL_WARNINGS 300 Strukturierte Warnings-API
MCP_CACHE_TTL_STAC_CLIMATE 86400 Climate-Runtime-Lookup
MCP_CLIMATE_NORMALS_PATH unset Pfad auf climate-normals.json
MCP_CLIMATE_NORMALS_URL_TEMPLATE unset URL-Template mit {station} / {STATION} / {param}
MCP_WARNINGS_API_URL unset URL der strukturierten Warnings-API

Vollständiges CHANGELOG

Siehe CHANGELOG.md.

Upgrade-Pfad

# uvx
uvx meteoswiss-mcp@0.3.0

# pip
pip install -U meteoswiss-mcp==0.3.0

Release v0.2.1 — Audit-Remediation komplett (0 critical/high/medium offen)

Choose a tag to compare

@malkreide malkreide released this 20 May 09:46
0d4adf7

Komplette Umsetzung des mcp-audit-skill-Reviews. 0 critical / 0 high / 0 medium Findings offen.

Production-ready für:

  • stdio (Claude Desktop, lokal)
  • Single-instance HTTP auf Render via render.yaml-Blueprint
  • Multi-Replica HTTP mit MCP_STATELESS_HTTP=1

⚠️ Breaking Changes

  • JSON-Output-Format: Tools mit response_format="json" liefern jetzt einen Envelope { "payload": ..., "provenance": { source, license, attribution, retrieved_at, data_source_url } } statt eines flachen Dicts. Markdown-Outputs unverändert. Konsumenten, die JSON parsen, müssen auf result["payload"] zugreifen.
  • Entry-Point: meteoswiss_mcp.server:main statt mcp.run. Der neue Wrapper liest MCP_TRANSPORT / MCP_HOST / MCP_PORT / MCP_ALLOW_ANY_HOST aus ENV. CLI-Flags --http / --port bleiben als Override.

Sicherheit

  • SSRF-Prevention (SEC-004/SEC-021): Egress-Allow-List validiert alle Requests inkl. Redirect-Targets. Blockiert RFC1918 / Loopback / Link-Local-IPs (z.B. 169.254.169.254).
  • NeighborJack-Hardening (SEC-016): MCP_HOST defaultet auf 127.0.0.1; 0.0.0.0-Binding nur mit explizitem MCP_ALLOW_ANY_HOST=1.
  • Container-Sandboxing (SEC-007): Multi-Stage-Dockerfile mit non-root user mcp:10001.
  • Optionaler API-Key-Auth via MCP_API_KEY (X-API-Key / Bearer, constant-time-Vergleich).

Neue Features (opt-in)

  • CORS-Middleware via MCP_ALLOWED_ORIGINS — claude.ai Web kann jetzt Sessions aufbauen.
  • Stateless-HTTP-Mode via MCP_STATELESS_HTTP=1 für Multi-Replica.
  • OpenTelemetry-Tracing via OTEL_EXPORTER_OTLP_ENDPOINT + pip install meteoswiss-mcp[otel].
  • Structured Logging (structlog → JSON auf stderr): tool_invoked, upstream_failed, egress_blocked, auth_rejected, ...
  • Health-Endpoint GET /health (ohne Upstream-Pings).
  • OGD-Provenance-Envelope in allen JSON-Responses (CC BY 4.0 Attribution).
  • Fuzzy-Geocoding-Fallback: nicht-deutsche Ortsnamen werden via Zweitversuch ohne language-Restriktion aufgelöst.
  • Tool-Hash-Pinning (tool-hashes.json + CI-Guard) gegen Rug-Pull.

Infrastruktur

  • Multi-Stage-Dockerfile + render.yaml-Blueprint (plan starter, healthCheckPath, numInstances=1).
  • Dependabot für pip / github-actions / docker.
  • docs/roadmap.md mit expliziter Phasen-Statustabelle.

Audit-Findings geschlossen

ARCH-002, ARCH-003, ARCH-009, ARCH-012, OPS-003, OBS-001, OBS-002, OBS-003, OBS-004, OBS-006, SDK-001, SDK-002, SDK-003, SDK-004, SCALE-001, SCALE-002, SCALE-003, SCALE-004, SCALE-006, SEC-004, SEC-005, SEC-006, SEC-007, SEC-009, SEC-013, SEC-016, SEC-021, SEC-022, CH-004.

Vollständiges CHANGELOG

Siehe CHANGELOG.md.

Upgrade-Pfad

# uvx
uvx meteoswiss-mcp@0.2.0

# pip
pip install -U meteoswiss-mcp==0.2.0

# Docker
docker pull ghcr.io/malkreide/meteoswiss-mcp:v0.2.0   # falls Image-Publish konfiguriert

v0.2.0 — Initial PyPI Release

Choose a tag to compare

@malkreide malkreide released this 01 Apr 12:22
fix: use StrEnum instead of (str, Enum) to pass UP042 lint rule

Python 3.11+ provides enum.StrEnum natively.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

v0.1.0 – Initial Release

Choose a tag to compare

@malkreide malkreide released this 31 Mar 13:42
76e27ad
initial commit