Releases: malkreide/openlex-mcp
Release list
v0.2.5
[0.2.5] - 2026-08-02
Fixed
-
structlogcarried no upper bound, and the index already serves a major past
the floor. The declared range wasstructlog>=24.1.0; PyPI has been serving
26.1.0. The artefact does not change — the resolver's answer to the next
fresh install does, and that is exactly howswiss-energy-mcp0.3.3 became
uninstallable whenmcp2.0.0 removed the module it imported.Now
structlog>=24.1.0,<27. The bound is measured rather than guessed: this package
installs and imports againststructlog 26.1.0today, so the cap admits what
demonstrably works and stops only the next, unknown major.
A dependency range only reaches users through a new release, hence the
version bump. No code changed.
v0.2.4
[v0.2.4] — 2026-07-30
Fixed
-
The User-Agent reports the actual package version again. The published
0.2.3sentopenlex-mcp/0.2.0to every upstream — the version string was
hardcoded and had been left behind by earlier bumps. The version now comes
from the package metadata, so it can no longer drift from the package. -
HTTP-Modus wies unter jedem echten Hostnamen mit 421 ab (SEC-005).
_build_http_app()riefmcp.streamable_http_app()ohnehostauf. Unter
mcp 2.x ist das kein neutraler Default: das SDK leitet daraus eine Allow-List
ab und aktiviert bei loopback-artigem Wert automatisch127.0.0.1:*. Da der
Default127.0.0.1ist, galt das auch für denMCP_HOST=0.0.0.0-Bind des
Containers. Nachgemessen an der echten ASGI-App vor dem Fix:Host 127.0.0.1:8000 -> 200 Host mcp.example.ch -> 421 Host openlex.example.com -> 421/healthzantwortete weiter mit 200 und verdeckte es, weshalb ein
Readiness-Probe nichts gemerkt hätte.Der Bind reist jetzt in die App, und eine explizite Allow-List wird aus dem
neuenMCP_ALLOWED_HOSTSgebaut. Ohne diese Variable bleibt der Schutz auf
einem Nicht-Loopback-Bind bewusst aus und der Aufrufer warnt — eine geratene
Liste würde genau das 421-Problem reproduzieren. Konfigurierte CORS-Origins
werden mit aufgenommen, sonst weist der Transport genau die Browser-Clients
ab, die CORS erlaubt.13 neue Tests, davon der tragende „richtiger Hostname, falscher Port": nur er
unterscheidet eine portgenaue Allow-List von einer, die alles durchlässt —
evil.example.comallein würde auch ein zurückfallender Loopback-Default
abweisen. Mutationsgetestet: nimmt man denhost-Kwarg wieder weg,
reproduziert der Test das 421 exakt.Geprüft mit dem wörtlichen CI-Kommando: 111 passed, 8 deselected;
ruff check src/ tests/clean.
Added
- Security policy —
SECURITY.md(English) andSECURITY.de.md(German),
linked from both READMEs andCONTRIBUTING.md. - German contribution guide —
CONTRIBUTING.de.md, linked from
CONTRIBUTING.md.
Fixed
-
Capped
mcpat<2.mcp2.0.0, published 2026-07-28, removed
mcp.server.fastmcp— the module this server imports. With the previous
unbounded>=1.28.1every fresh resolve picked 2.0.0 and failed at import
withModuleNotFoundError, in CI and for anyone runningpip installalike.
Verified in both directions: 2.0.0 fails,<2resolves to 1.29.0 and imports
cleanly. Migrating to the 2.x API (mcp.server.mcpserver) stays a separate,
deliberate piece of work. -
zhlaw_get_law_metadatapermalink — the legacy
http://www.zhlex.zh.ch/Erlass.html?Open&Ordnr=<ordnr>permalink was replaced
upstream and now returns 404 (it redirected to alawcollection-directlink
endpoint that 404s over HTTP). Metadata now resolves via the current
https://www.zhlex.zh.ch/bin/zhweb/publish/lawcollection-directlink?Open&Ordnr=<ordnr>
endpoint, which 302-redirects to the consolidated version onwww.zh.ch. -
Live tests — repaired three nightly live-test regressions caused by upstream
drift:zhlaw_get_articlereturned emptycontentfor single-line PDF extracts
(e.g. VSG § 1): the article parser captured the whole running text into the
title. The parser now derives the marginal-note title and the body separately
socontentis never empty for run-on lines. Offline regression tests added.zhlaw_get_law_metadatano longer resolved on zh.ch — the undated
erlass-<ordnr>.htmllanding URL was removed upstream (returns 404). Live
metadata now uses the stable per-ordinance permalink
http://www.zhlex.zh.ch/Erlass.html?Open&Ordnr=<ordnr>, which redirects to
the current consolidated version onwww.zh.ch.test_live_list_lawsasserted a brittle SR-prefix that does not hold for the
first page (laws are sorted ascending by ordinance number); it now checks the
real invariant (non-empty, ascendingsr_numbers).
Changed
- Egress allow-list (SEC-021 / SEC-004) — added
www.zhlex.zh.chto
EGRESS_ALLOWLISTand introducedHTTP_ALLOWED_HOSTSso that this single
legacy permalink host may be reached over HTTP (it has no HTTPS endpoint).
HTTPS remains mandatory for every other host; the allow-list, SSRF IP-block,
DNS-pinning, and per-hop redirect gate are unchanged. See
docs/network-egress.md. - Documentation consistency — re-synced
README.de.mdwith the English
README.md(Development Phase, network binding, expanded cloud config, design
decision, scaling constraints, MCP protocol version, tool output format,
security rows, updated project tree). UpdatedLICENSEcopyright year to 2026.
v0.2.0 — First production-ready release
openlex-mcp v0.2.0
First production-ready release. Resolves all 31 findings from the initial
MCP best-practice audit plus all 4 findings from the follow-up re-audit
(2026-05-29): 40/44 checks pass, 0 fail, production_ready: true.
⚠️ Breaking Changes
- Tool output contract (SDK-002): all 8 tools now return typed, structured
response envelopes (source/provenance/result_type/count/
message/ typedresults) instead of pre-formatted Markdown strings.
FastMCP now emits an output schema +structuredContentfor every tool.
Clients parsing the old Markdown text output must migrate to the envelope.
✨ Added
- Structured logging (OBS-003):
structlogJSON to stderr with per-call
bound context (tool+correlation_id). - Hardened egress (SEC-004/005/021): HTTPS-only, code-layer egress
allow-list, SSRF IP-blocking (incl.169.254.169.254), DNS-pinning, manual
redirect re-validation. - Lifespan-scoped shared
httpx.AsyncClient(SDK-001). - CORS for browser clients (SDK-004): exposes
Mcp-Session-Id, no wildcard. - Dockerfile + compose.yml (SEC-007/SCALE-004/006): non-root multi-stage
image, HEALTHCHECK, resource limits. pydantic-settingsSettings +MCP_TRANSPORT(ARCH-004/SCALE-001).openlex__tool namespace + tool-hash snapshot (SEC-022).MCP_PROTOCOL_VERSION = 2025-11-25pin + Dependabot (ARCH-012).- Strict input validation (SEC-018):
strict=Trueon all 8 models. - Context injection in
zhlaw_update_cache(SDK-003): progress + info/warning. - Live tests + nightly workflow (OPS-001):
tests/test_live.py(8 tests,
one per tool) +.github/workflows/live.yml. - Structured tool docstrings (ARCH-002):
<use_case>/<important_notes>
/<example>tags on all 8 tools.
🔧 Changed
- Error handling (OBS-001/002): execution errors surfaced as masked
isErrorresults; internals logged to stderr only. - HTTP binding (SEC-016): defaults to
127.0.0.1; NeighborJack warning on
public binding outside containers.
🐛 Fixed
User-Agentno longer contains a non-ASCII character (Zürich→Zuerich).
📚 Docs
ROADMAP.md(Phase 1 → 2 gates, accepted-risk table),docs/secret-management.md,
docs/network-egress.md, bilingual README updates.
🧪 Test Suite
- 89 unit tests (Python 3.11 / 3.12 / 3.13) + 8 live tests (nightly).
🔒 Audit
- 40 pass · 0 fail · 4 partial (2 accepted-risk: SCALE-002/003 — Phase-2 gates).
production_ready: true, 0 blocking findings.
Full changelog: v0.1.0...v0.2.0
v0.1.0 — Initial Release
openlex-mcp v0.1.0
MCP Server for Canton Zurich legislation (ZH-Lex) — full-text search, article extraction, and education law tools for ~970 cantonal laws.
Tools
zhlaw_search_laws— Full-text search across all ZH laws (FTS5 + BM25 ranking)zhlaw_get_law— Retrieve law by LS number or abbreviation (e.g.412.100orVSG)zhlaw_get_article— Extract a specific article from a law (e.g. Art. 28 VSG)zhlaw_list_laws— List and filter laws by legal area prefixzhlaw_find_education_laws— Specialized search in education law (LS 412.x series)zhlaw_search_articles— Search within all articles of a specific lawzhlaw_get_law_metadata— Live metadata from zh.ch (PDF links, validity status)zhlaw_update_cache— Refresh local cache from HuggingFace
Highlights
- 974 Canton Zurich laws with full-text search (SQLite FTS5)
- Article parser with paragraph detection (Art. / § / superscript digits)
- Hybrid architecture: cached full-text (HuggingFace) + live metadata (zh.ch)
- No API key required — all data under open licenses (CC-BY-SA 4.0)
- Dual transport: stdio (Claude Desktop) + Streamable HTTP (cloud)
Install
pip install openlex-mcp