Skip to content

Releases: malkreide/openlex-mcp

v0.2.5

Choose a tag to compare

@malkreide malkreide released this 02 Aug 21:20
0c8537f

[0.2.5] - 2026-08-02

Fixed

  • structlog carried no upper bound, and the index already serves a major past
    the floor.
    The declared range was structlog>=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 how swiss-energy-mcp 0.3.3 became
    uninstallable when mcp 2.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 against structlog 26.1.0 today, 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

Choose a tag to compare

@malkreide malkreide released this 31 Jul 15:33
6438565

[v0.2.4] — 2026-07-30

Fixed

  • The User-Agent reports the actual package version again. The published
    0.2.3 sent openlex-mcp/0.2.0 to 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() rief mcp.streamable_http_app() ohne host auf. Unter
    mcp 2.x ist das kein neutraler Default: das SDK leitet daraus eine Allow-List
    ab und aktiviert bei loopback-artigem Wert automatisch 127.0.0.1:*. Da der
    Default 127.0.0.1 ist, galt das auch für den MCP_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
    

    /healthz antwortete 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
    neuen MCP_ALLOWED_HOSTS gebaut. 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.com allein würde auch ein zurückfallender Loopback-Default
    abweisen. Mutationsgetestet: nimmt man den host-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 policySECURITY.md (English) and SECURITY.de.md (German),
    linked from both READMEs and CONTRIBUTING.md.
  • German contribution guideCONTRIBUTING.de.md, linked from
    CONTRIBUTING.md.

Fixed

  • Capped mcp at <2. mcp 2.0.0, published 2026-07-28, removed
    mcp.server.fastmcp — the module this server imports. With the previous
    unbounded >=1.28.1 every fresh resolve picked 2.0.0 and failed at import
    with ModuleNotFoundError, in CI and for anyone running pip install alike.
    Verified in both directions: 2.0.0 fails, <2 resolves 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_metadata permalink — the legacy
    http://www.zhlex.zh.ch/Erlass.html?Open&Ordnr=<ordnr> permalink was replaced
    upstream and now returns 404 (it redirected to a lawcollection-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 on www.zh.ch.

  • Live tests — repaired three nightly live-test regressions caused by upstream
    drift:

    • zhlaw_get_article returned empty content for 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
      so content is never empty for run-on lines. Offline regression tests added.
    • zhlaw_get_law_metadata no longer resolved on zh.ch — the undated
      erlass-<ordnr>.html landing 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 on www.zh.ch.
    • test_live_list_laws asserted 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, ascending sr_numbers).

Changed

  • Egress allow-list (SEC-021 / SEC-004) — added www.zhlex.zh.ch to
    EGRESS_ALLOWLIST and introduced HTTP_ALLOWED_HOSTS so 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.md with 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). Updated LICENSE copyright year to 2026.

v0.2.0 — First production-ready release

Choose a tag to compare

@malkreide malkreide released this 29 May 13:05
b39498f

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 / typed results) instead of pre-formatted Markdown strings.
    FastMCP now emits an output schema + structuredContent for every tool.
    Clients parsing the old Markdown text output must migrate to the envelope.

✨ Added

  • Structured logging (OBS-003): structlog JSON 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-settings Settings + MCP_TRANSPORT (ARCH-004/SCALE-001).
  • openlex__ tool namespace + tool-hash snapshot (SEC-022).
  • MCP_PROTOCOL_VERSION = 2025-11-25 pin + Dependabot (ARCH-012).
  • Strict input validation (SEC-018): strict=True on 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
    isError results; internals logged to stderr only.
  • HTTP binding (SEC-016): defaults to 127.0.0.1; NeighborJack warning on
    public binding outside containers.

🐛 Fixed

  • User-Agent no longer contains a non-ASCII character (ZürichZuerich).

📚 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

Choose a tag to compare

@malkreide malkreide released this 12 Apr 12:19

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.100 or VSG)
  • 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 prefix
  • zhlaw_find_education_laws — Specialized search in education law (LS 412.x series)
  • zhlaw_search_articles — Search within all articles of a specific law
  • zhlaw_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