Skip to content

v2.0.0

Choose a tag to compare

@malkreide malkreide released this 01 Aug 14:25
15c3dff

[2.0.0] - 2026-08-01

Sammelrelease. Diese Version liefert alles aus, was seit 1.0.3 auf main
aufgelaufen ist — 22 Commits, darunter die beiden Stände, die bis zum
01.08.2026 fälschlich als [1.1.0] und [1.2.0] in diesem CHANGELOG standen,
obwohl keiner von beiden je ausgeliefert wurde: kein Tag, kein PyPI-Upload,
kein GitHub-Release. Ihr Inhalt ist unverändert übernommen und unten als
eigene Blöcke erhalten, damit nachvollziehbar bleibt, was wann entstanden ist.

Warum 2.0.0 und nicht das vorbereitete 1.2.0: die Migration auf das
mcp-SDK 2.x hebt die Abhängigkeit von <2 auf >=2.0.0,<3. In einer
Umgebung, die mcp auf 1.x festhält, bricht die Installation — ein Breaking
Change, den auch der Commit selbst so markiert (feat!). Der MCP-Tool-Vertrag
ist davon nicht betroffen: 12 Tools, 2 Resources, tool-definitions.lock.json
unverändert. Wer den Server wie dokumentiert über uvx startet, merkt vom
Major-Sprung nichts.

Fixed

  • Streamable-HTTP wies unter jedem echten Hostnamen mit 421 ab (SEC-005).
    _run_http() baute die App mit mcp.streamable_http_app() ohne host — und
    zwar bevor Host und Port überhaupt aufgelöst waren. Der Bind konnte also
    gar nicht ankommen. Unter mcp 2.x ist das kein neutraler Default: das SDK
    leitet aus dem App-Argument seine Host-Allow-List ab und aktiviert bei
    loopback-artigem Wert automatisch 127.0.0.1:*. Da der Default 127.0.0.1
    ist, traf das jeden Start mit FEDLEX_HOST=0.0.0.0.

    Der Bind wird jetzt zuerst ermittelt (resolve_http_bind()) und dann an beide
    Abnehmer gegeben — uvicorn und die App. Eine echte Allow-List entsteht aus dem
    neuen FEDLEX_ALLOWED_HOSTS; ohne diese Variable bleibt der Schutz auf einem
    Nicht-Loopback-Bind bewusst aus und der Aufrufer warnt.

    13 neue Tests, darunter der tragende Fall „richtiger Hostname, falscher Port"
    und einer, der die Bind-Auflösung festnagelt (PORT schlägt --port, --port
    schlägt die Settings) — sonst könnte die App später wieder einen anderen Wert
    sehen als uvicorn. Mutationsgetestet: nimmt man den host-Kwarg wieder weg,
    reproduziert der Test das 421.

    Geprüft mit den wörtlichen CI-Kommandos: 94 passed / 3 deselected,
    ruff check src/ tests/ clean.

Geändert — Breaking

  • Migration auf das mcp-Python-SDK 2.x. Der Server importiert nicht mehr
    mcp.server.fastmcp, sondern mcp.server.mcpserver; aus FastMCP wird
    MCPServer. Die Abhängigkeit lautet damit mcp[cli]>=2.0.0,<3 statt
    >=1.28.1,<2 — das ist der Breaking-Anteil: wer fedlex-mcp in eine Umgebung
    installiert, die mcp auf 1.x festhält, bekommt jetzt einen
    Auflösungskonflikt. Unter uvx, dem dokumentierten Installationsweg, ist die
    Umgebung isoliert und die Änderung unsichtbar.

    Der Tool-Vertrag bleibt bewusst unverändert. mcp_types 2.x hat die
    Python-Felder auf snake_case umbenannt (inputSchemainput_schema,
    outputSchemaoutput_schema); compute_tool_signature_hash() liest jetzt
    die neuen Namen und dumpt die Annotations mit by_alias=True, damit die
    Draht-Schreibweise (readOnlyHint, …) erhalten bleibt. Ohne dieses by_alias
    hätte sich der Signatur-Hash geändert, ohne dass sich am Vertrag etwas ändert —
    die Drift-Erkennung aus SEC-022 hätte falsch angeschlagen.
    tool-definitions.lock.json ist entsprechend unverändert geblieben; die 12
    Tools und 2 Resources sind identisch.

    Protokollseitig bringt 2.x eine neuere Revision mit
    (LATEST_PROTOCOL_VERSION = 2026-07-28). Sie wird beim initialize-Handshake
    ausgehandelt, ältere Clients bleiben also bedient.

Behoben

  • mcp auf <2 begrenzt. mcp 2.0.0, veröffentlicht am 28.07.2026, hat
    mcp.server.fastmcp entfernt — genau das Modul, das dieser Server importiert.
    Mit dem bisherigen offenen >=1.28.1 wählte jede frische Auflösung 2.0.0 und
    scheiterte beim Import mit ModuleNotFoundError, in der CI ebenso wie bei
    jedem pip install. In beide Richtungen verifiziert: 2.0.0 scheitert, <2
    löst auf 1.29.0 auf und importiert sauber. Die Migration auf die 2.x-API
    (mcp.server.mcpserver) bleibt eine eigene, bewusste Aufgabe.

    Zwischenstand, inzwischen überholt. Die Migration ist im selben
    Release-Zyklus erfolgt (siehe «Geändert — Breaking»). Ausgeliefert wird
    mcp[cli]>=2.0.0,<3, nicht <2. Der Eintrag bleibt stehen, weil er
    erklärt, warum diese Abhängigkeit überhaupt eine Obergrenze trägt.

  • mcp-Untergrenze auf >=1.28.1 angehoben (CVE-2026-59950). Vorher stand
    dort ein offenes >=1.3.0, unter dem eine frische Auflösung eine verwundbare
    SDK-Version hätte wählen können. Die heute ausgelieferte Untergrenze
    (>=2.0.0) schliesst den betroffenen Bereich weiterhin aus.

Hinzugefügt — Verteilung und Installation

  • server.json ergänzt: Registry-Metadaten für die MCP Registry
    (io.github.malkreide/fedlex-mcp, registryType: pypi, runtimeHint: uvx,
    Kategorie «Legal, Courts & Regulatory»). Bis dahin existierte die Datei nicht,
    obwohl der mcp-name-Marker im README seit 1.0.3 auf sie vorbereitet war.
  • ## Installation im README mit dem uvx-Client-Snippet für die
    mcpServers-Konfiguration (Claude Desktop, Cursor, Windsurf; Hinweis auf den
    Top-Level-Schlüssel servers für VS Code). Der generierte Block war ans
    Dateiende gehängt worden — hinter ## Author und ## Credits, und als
    zweite ## Installation-Überschrift. Er steht jetzt an der richtigen
    Stelle (nach ## Prerequisites), die Marker BEGIN/END GENERATED: install
    sind erhalten, und der bisherige Abschnitt heisst zur Unterscheidung
    ## Installation from source. Damit löst der Anker #installation wieder auf
    den empfohlenen Weg auf und die Schluss-Sektionen stehen wieder am Schluss.
  • Publish in die MCP Registry als eigener Workflow-Job (publish-mcp),
    nachgelagert zum PyPI-Upload, authentifiziert über GitHub-OIDC
    (mcp-publisher login github-oidc). Der Job zieht version und
    packages[0].version in server.json aus dem Release-Tag nach, damit
    Tag, Paket und Registry-Eintrag nicht auseinanderlaufen können.

Geändert — Dokumentation und Interna

  • README (beide Sprachfassungen): Schluss-Sektionen in der Reihenfolge
    Contributing → Security statt Security → Contributing.
  • README (EN): Die Sektion «MCP Protocol Version» nannte die SDK-Grenze
    >=1.3.0 und war damit über drei Abhängigkeitsänderungen hinweg falsch; sie
    verweist jetzt auf pyproject.toml statt eine Version zu wiederholen.
  • ruff in pyproject.toml mit Obergrenze gepinnt (>=0.15.15,<0.17). Ohne
    Obergrenze installiert die CI die jeweils neuste Version; ein geänderter
    Default-Regelsatz färbt den Lauf dann rot, ohne dass sich eine Zeile Code
    geändert hat.
  • actions/checkout in ci.yml, publish.yml und security.yml von v6 auf v7
    angehoben (Dependabot).

Aus der nie ausgelieferten 1.2.0 (dokumentiert am 20.07.2026)

Vertieft die Vernehmlassungs-Schicht auf ein fristenzentriertes Produkt: die
Restfrist wird zur Kernaussage. Keine neuen Tools (bleibt bei 12); die drei
bestehenden fedlex_*consultation*-Tools werden gehärtet. Vollständig
rückwärtskompatibel für SR/AS/BBl/TERMDAT.

Added

  • days_remaining in jeder Vernehmlassungs-Antwort, zur Laufzeit berechnet
    (nie gecacht, nie geschätzt) gegen Europe/Zurich. Kalendertag-Semantik:
    0 = Frist endet heute, negativ = abgelaufen. Zentral in einer testbaren
    Funktion (consultations.days_until / deadline_status).
  • Abgeleiteter status — die Frist gewinnt. Sagt die Quelle «Laufend», die
    Frist liegt aber in der Vergangenheit, wird der Status Abgeschlossen; das
    rohe Quell-Label bleibt als status_source sichtbar, der Widerspruch als
    status_conflict: true. Eine abgelaufene Vernehmlassung erscheint nie als
    laufend.
  • Typisiertes Consultation-Pydantic-v2-Modell mit den Pflichtfeldern
    title, status, opened_on, deadline, days_remaining, lead_office, source_url, retrieved_at, language. retrieved_at (UTC) in jeder Antwort.
  • Thematischer Filter topic="education" — ausgewiesene Stichwort-Union
    (Freitext im Titel; Fedlex hat keine Sachgebiets-Taxonomie). Die Antwort nennt
    die tatsächlich gesuchten Begriffe (message + Markdown), damit klar ist,
    wonach nicht gesucht wurde. Sortierung nach kürzester Restfrist als Default.
  • Isoliertes consultations-Modul — Vernehmlassungs-Logik (Uhr,
    Fristenberechnung, Modell, Query-Bausteine, Themenfilter) getrennt von der
    SR-/AS-/BBl-Schicht in server.py.
  • Neue Tests (respx + injizierbare Uhr): korrektes days_remaining,
    Frist heute → days_remaining == 0, Frist gestern → «Abgeschlossen», nicht
    in der Liste laufender Verfahren
    (SPARQL-Frist-Grenze + Sprach-Dedupe im
    Query geprüft), Quelle «laufend» vs. Datum → Datum gewinnt, Themenfilter
    findet Bildungsvorlage inkl. ausgewiesener Strategie, Endpoint nicht
    erreichbar → erklärender Fehler statt leerem Resultat.

Known findings (live verifiziert 2026-07-20)

  • jolux:Consultation hat keine Sachgebiets-/Klassifikations-Taxonomie
    thematische Filterung ist ausschliesslich Freitext. Der Ankerbegriff
    «Volksschule» kommt in 0 Titeln vor (bildung 44, topic="education"-Union
    66; 42 aktuell offen).
  • SPARQL-Quirk: REGEX(LCASE(...), "a|b") liefert auf dem Fedlex-Endpoint
    still 0 Treffer — Alternation daher über OR-verkettetes CONTAINS.
  • eventEndDate ist xsd:date (reiner Kalendertag, keine Uhrzeit/TZ) →
    Vergleich gegen «heute in Europe/Zurich».
  • Scope-Grenze dokumentiert: nur Bund, keine kantonalen Vernehmlassungen;
    kein Push-Mechanismus (MCP ist Pull-basiert).

Refactor

  • Geteilter SPARQL-/JSON-Client extrahiert (sparql_client.py, vendored
    Portfolio-Baustein). Der bisherige _execute_sparql-Retry-Kern ist jetzt eine
    dünne Bindung an das wiederverwendbare Modul; sparql_escape / val daraus
    re-exportiert, RETRYABLE_STATUS entfernt. Verhalten unverändert
    (Retry/Backoff, Egress-Guard, sparql_retry-Log via Callback erhalten),
    öffentliche Namen stabil, 76 Tests grün. Die Datei ist byte-identisch zur
    Kopie in swiss-environment-mcp — bis ein installierbares swiss-mcp-commons
    (PyPI/OIDC) existiert, sind die Kopien synchron zu halten.

Aus der nie ausgelieferten 1.1.0 (dokumentiert am 18.07.2026)

Erweitert den Server um zwei zusätzliche, ebenfalls SPARQL-basierte Datenquellen
— Vernehmlassungen (Fedlex) und die Terminologiedatenbank TERMDAT (via LINDAS).
Von 7 auf 12 Tools; 2 Resources unverändert. Vollständig rückwärtskompatibel:
die bestehenden fedlex_*-Tools und beide Resources bleiben unangetastet.

Hinweis zur Version: Der zugrunde liegende Auftrag nannte 0.2.0; da das Repo
bereits auf 1.0.3 stand, wäre das ein Downgrade gewesen. Als
semver-konformer Minor-Bump für eine rückwärtskompatible Funktionserweiterung
ist es 1.1.0.

Added

  • Vernehmlassungen (Fedlex, jolux:Consultation) — drei neue Tools:
    • fedlex_get_open_consultations — Fristen-Monitoring; filtert primär über
      eventEndDate >= heute
      , nicht über den Status (die beiden Signale sind
      unabhängig). Sortiert nach Frist aufsteigend, optionaler keyword-Filter.
      Leeres Resultat liefert eine explizite Sachaussage inkl. Prüfzeitpunkt statt
      einer nackten leeren Liste.
    • fedlex_search_consultations — Volltextsuche über eventTitle/
      eventDescription mit Filtern für Status, Zeitraum (Frist) und
      federführendes Amt.
    • fedlex_get_consultation — Detail zu einer eventId: Fristen, Departement/
      Amt, Status, Vernehmlassungsunterlagen (opinionHasDraftRelatedDocument),
      betroffene Rechtsressource (foreseenImpactToLegalResource).
  • TERMDAT (LINDAS, schema.org) — zwei neue Tools:
    • termdat_lookup_term — Begriff → Entsprechungen in de/fr/it/rm/en samt
      Definition.
    • termdat_get_concept — vollständiger Eintrag zu einer ID oder URI; akzeptiert
      ID, Konzept-URI und Term-URI mit Sprachsuffix und normalisiert intern.
  • Zweiter, isolierter SPARQL-Endpoint (LINDAS) mit eigenem httpx-Client und
    eigenem Timeout (ARCH A / Isolationspflicht): Ein LINDAS-Ausfall lässt die
    fedlex_*-Tools unbeeinträchtigt und umgekehrt — getrennte Fehler-/Statusmeldung.
  • Retry-Logik für transiente Fehler (HTTP 429/502/503/504, Timeout, Netzwerk)
    mit exponentiellem Backoff; deterministische Fehler (z.B. HTTP 400) werden
    nicht wiederholt.
  • TERMDAT-Attribution in jeder Response (source-Feld + Markdown-Footer),
    inkl. des Teilbestand-Hinweises (77'692 von ~400'000 Einträgen).
  • Neue Tests (respx-gemockt): Happy-Path je Tool, Retry bei 503, Timeout-/
    Netzwerk-Masking, Endpoint-Isolation, Status-Frist-Konflikt (Quirk 1),
    Consultation ohne hasSubTask, SPARQL-Escaping. Live-Tests gegen beide
    Endpoints unter -m live (aus CI ausgeschlossen).

Changed

  • fedlex://info-Resource: Version, zweiter Endpoint, alle 12 Tools und die
    Isolationsnotiz ergänzt (Resource-URI unverändert).
  • handle_error und der Response-Envelope tragen jetzt den betroffenen Dienst
    (Fedlex vs. TERMDAT (LINDAS)), damit ein isolierter LINDAS-Fehler nicht als
    Fedlex-Fehler gelesen wird.

Known findings

Live verifiziert am 18.07.2026:

Vernehmlassungen (Fedlex)

  • Gesamtbestand jolux:Consultation: 2 553.
  • 48 von 2 553 Consultations haben keine eventStartDate/eventEndDate
    (kein hasSubTask mit Fristen) → deadline: null, kein Tool-Fehler.
  • Quirk 1: Status «Laufend» (consultation-status/2) und eine Frist in der
    Zukunft sind unabhängige Signale. Es gibt «laufende» Einträge mit
    abgelaufener Frist. Die Tools filtern über die Frist und markieren
    Widersprüche mit status_conflict: true statt sie stillschweigend aufzulösen.
  • previousConsultationStatus ist mit nur 234 Werten dünn besetzt und wird
    nicht als Filter verwendet.

TERMDAT (LINDAS)

  • Quirk 2 (Reality-Check): Die Bundeskanzlei kommuniziert ~400 000 TERMDAT-
    Einträge; als Linked Data auf LINDAS liegen 77 692 (schema.ld.admin.ch/Term).
    Die Differenz ist nicht erklärt — vermutlich ist nur der validierte/freigegebene
    Teilbestand publiziert. Ein Negativtreffer bedeutet daher nicht, dass der
    Begriff in TERMDAT fehlt, sondern nur, dass er nicht im LINDAS-Teilbestand liegt.
    Dieser Hinweis steht in jeder TERMDAT-Response.
  • Quirk 3 (zwei URI-Ebenen): Konzept-URIs (…/termdat/40109) tragen die
    bevorzugten Benennungen je Sprache, die Definition und via schema:hasPart die
    Synonym-/Varianten-Term-URIs mit Sprach- und Positionssuffix (…/40109/3/de).
    Beide Eingaben werden akzeptiert und intern auf die Konzept-ID normalisiert.
  • rm (Rätoromanisch) ist im LINDAS-Teilbestand faktisch nicht besetzt
    (0 Namen) — als Zielsprache erlaubt, liefert aber in aller Regel keinen Treffer.