Openlex Mcp

io.github.malkreide由 GitHub 帳號 malkreide 發布v0.3.0更新於 Oct 24, 2026

Canton Zurich legislation via ZH-Lex with full-text search and article extraction

概覽

AI 產生的概覽

唯讀 MCP 伺服器,讓助理對約 970 部蘇黎世州法律(ZH-Lex)做全文檢索與條文擷取,並從 zh.ch 取得即時中繼資料。

可以做什麼
  • 檢索與瀏覽法律:具 BM25 排序的全文檢索、依 LS 編號或縮寫查詢單一法律、依法律領域前綴列出法律,以及專門的教育法檢索。
  • 擷取條文:取出某部法律的特定條款(Art./§),並在單一法律的所有條文中搜尋。
  • 中繼資料與快取:取得 zh.ch 的即時中繼資料(PDF 連結、生效狀態),並從 HuggingFace 更新本機資料快取。
適用情境
適合涉及蘇黎世州法律的問題:查找某主題的法律、引用具體條文,或確認某法規是否列為有效。最初為蘇黎世市學校局建置,教育法(LS 412.x)涵蓋較完整,但涵蓋全部州法領域。
執行需求
本機 Python 3.11+ 程序,透過 uvx 或 pip 執行;首次下載並建立索引(約 25 秒)以及取得 zh.ch 即時中繼資料需要網路連線。不需 API 金鑰或帳號。選用的 HTTP 傳輸預設只綁定本機,可用 MCP_HOST 與 MCP_PORT 設定。
安裝前請注意
法律文本僅供參考,不構成法律意見。語料凍結於 2023-01-01,且所有條目都標示為有效,因此之後遭廢止的法律仍會顯示為有效;回應中帶有 corpus_as_of 與 corpus_note,更新快取並不會讓法律變新。PDF 擷取的文字有瑕疵,條文解析可能合併邊界。唯讀、不持有密鑰,對外連線僅限 zh.ch 主機。

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 Openlex Mcp,將其新增到工作區。
  2. 為需要使用其工具的對話啟用該服務。

Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。

其他 MCP 客戶端

參照 儲存庫 中的啟動說明。

README

🇨🇭 Part of the Swiss Public Data MCP Portfolio

⚖️ openlex-mcp

[Version] [License: MIT] [Python 3.11+] [MCP] [No Auth Required]

MCP Server for Canton Zurich legislation (ZH-Lex) — full-text search, article extraction, and education law tools for ~970 cantonal laws

🇩🇪 Deutsche Version

[Demo: Claude searches Zurich education law via MCP tool call]


Overview

openlex-mcp provides AI-native access to the entire legal collection of Canton Zurich (Zürcher Gesetzessammlung). It combines full-text data from HuggingFace with live metadata from the official zh.ch website, storing everything in a local SQLite database with FTS5 full-text indexing for sub-50ms search performance.

SourceDataAccess
HuggingFace974 ZH laws — full text (PDF extracts)Cached locally as SQLite + FTS5
zh.ch ZH-LexCurrent metadata, PDF links, validity statusLive HTTP requests

Built for the Schulamt (school department) of the City of Zurich, but covers all areas of cantonal law — from tax law to building regulations.

Anchor demo query: "What does the Volksschulgesetz say about parental involvement? Show me Art. 55 VSG and find all articles that mention 'Elternrat'."


Features

  • ⚖️ 8 tools covering search, retrieval, article extraction, and cache management
  • 🔍 FTS5 full-text search across ~970 cantonal laws with BM25 ranking
  • 📑 Article extraction — parse individual articles (Art. / §) with paragraph detection
  • 🏫 Education law shortcuts — specialized search for LS 412.x series (Volksschulgesetz, Lehrpersonalverordnung, etc.)
  • 🌐 Live metadata from zh.ch for current validity status and PDF links
  • 💾 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)

Development Phase

Current phase: Phase 1 — Read-Only. All tools are read-only (readOnlyHint: true); no writes to external systems. See ROADMAP.md for the phase plan and transition gates before any write or multi-agent capability is added.


Prerequisites

  • Python 3.11+
  • uv (recommended) or pip
  • Internet connection (for initial data download and live metadata)

Installation

bash
# Clone the repositorygit clone https://github.com/malkreide/openlex-mcp.gitcd openlex-mcp
# Installpip install -e .# or with uv:uv pip install -e .

Quickstart

bash
# stdio (for Claude Desktop)python -m openlex_mcp.server
# Streamable HTTP — binds to 127.0.0.1:8000 by default (localhost only)python -m openlex_mcp.server --http --port 8000

Network binding

By default the HTTP transport binds to 127.0.0.1 (localhost only). The host and port are configurable via the MCP_HOST / MCP_PORT environment variables (or the --host / --port CLI flags, which take precedence).

Never bind to 0.0.0.0 outside a container — it exposes the server to your local network (NeighborJack risk). For containerized/cloud deployments set MCP_HOST=0.0.0.0 explicitly; when that happens outside a detected container the server logs a warning.

Try it immediately in Claude Desktop:

"What is the Volksschulgesetz (VSG)?" "Find all Zurich laws about data protection" "Show me Art. 1 of the Volksschulgesetz" "Which education laws mention 'Schulleitung'?"


Configuration

Claude Desktop

Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):

json
{  "mcpServers": {    "openlex": {      "command": "python",      "args": ["-m", "openlex_mcp.server"]    }  }}

Or with the installed entry point:

json
{  "mcpServers": {    "openlex": {      "command": "openlex-mcp"    }  }}

Config file locations:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

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):

  1. Push/fork the repository to GitHub
  2. On render.com: New Web Service → connect GitHub repo
  3. Set start command: python -m openlex_mcp.server --http --port 8000
  4. Set environment variable MCP_HOST=0.0.0.0 so the container is reachable (the code default is 127.0.0.1; Render sets the RENDER env var, so no NeighborJack warning is logged)
  5. Set MCP_CORS_ORIGINS=https://claude.ai so the browser can read the Mcp-Session-Id header (comma-separated list; no wildcard — defaults to empty, i.e. no cross-origin access)
  6. In claude.ai under Settings → MCP Servers, add: https://your-app.onrender.com/sse

💡 "stdio for the developer laptop, SSE for the browser."


Available Tools

Search & Browse

ToolDescription
openlex__zhlaw_search_lawsFull-text search across all ~970 ZH laws (FTS5 + BM25 ranking)
openlex__zhlaw_get_lawRetrieve a law by LS number (e.g. 412.100) or abbreviation (e.g. VSG)
openlex__zhlaw_list_lawsList and filter laws by legal area prefix
openlex__zhlaw_find_education_lawsSpecialized search in education law (LS 412.x series)

Article Extraction

ToolDescription
openlex__zhlaw_get_articleExtract a specific article from a law (e.g. Art. 28 VSG)
openlex__zhlaw_search_articlesSearch within all articles of a specific law

Metadata & Cache

ToolDescription
openlex__zhlaw_get_law_metadataGet live metadata from zh.ch (PDF links, validity status)
openlex__zhlaw_update_cacheRefresh the local data cache from HuggingFace

Key Legal Area Prefixes (LS Numbers)

PrefixLegal AreaExample
131Constitution and popular rightsKantonsverfassung
170Administrative procedureDatenschutzgesetz
331Tax lawSteuergesetz
412Education and schoolsVolksschulgesetz (VSG)
700Spatial planning and buildingPlanungs- und Baugesetz
810HealthGesundheitsgesetz

Example Use Cases

QueryTool
"What is the Volksschulgesetz?"openlex__zhlaw_get_law
"Find laws about data protection"openlex__zhlaw_search_laws
"Show me Art. 55 VSG"openlex__zhlaw_get_article
"Which education laws mention Schulleitung?"openlex__zhlaw_find_education_laws
"Find all articles about Elternrat in the VSG"openlex__zhlaw_search_articles
"Is LS 412.100 still in force?"openlex__zhlaw_get_law_metadata

Architecture

┌─────────────────┐     ┌──────────────────────────────┐     ┌──────────────────────────┐│   Claude / AI   │────▶│  OpenLex MCP                 │────▶│  HuggingFace             ││   (MCP Host)    │◀────│  (MCP Server)                │◀────│  rcds/swiss_legislation   │└─────────────────┘     │                              │     │  (974 ZH laws, cached)   │                        │  8 Tools                     │     ├──────────────────────────┤                        │  SQLite + FTS5 Cache         │────▶│  zh.ch ZH-Lex            │                        │  Stdio | HTTP                │◀────│  (live metadata + PDFs)  │                        │                              │     ├──────────────────────────┤                        │  No authentication required  │     │  LexFind.ch              │                        └──────────────────────────────┘     │  (links only)            │                                                             └──────────────────────────┘

Data Source Characteristics

SourceProtocolCoverageAuthLicense
HuggingFace rcds/swiss_legislationDatasets API974 ZH laws (full text)NoneCC-BY-SA 4.0
zh.ch ZH-LexHTTP/HTMLCurrent metadata, PDFsNonePublic
LexFind.chHTTPCross-cantonal linksNonePublic

Design Decision: Tools-only (no MCP Resources)

All 8 endpoints are exposed as Tools rather than MCP Resources. Rationale:

  • Every lookup is parametric — queries, abbreviations, article numbers vary per call. Static Resources (one URI per document) don't capture this naturally.
  • The corpus is 974 laws × many articles — registering each as a Resource URI would create an impractically large resource list.
  • MCP Resource templates (zhlex://laws/{sr_number}) are a future consideration for Phase 2 if clients benefit from resource-level caching or subscriptions.

Scaling Constraints

The constraint depends on the protocol era the client speaks:

  • 2026-07-28 (modern) — every request is a self-contained POST carrying its own _meta envelope; the server answers without issuing an Mcp-Session-Id and keeps no session between requests. Any replica can answer any request; no shared store, no sticky routing. (subscriptions/listen streams live on the replica that opened them — this server's lists are fixed at import, so there is nothing to fan out.)
  • Handshake era (initialize, up to 2025-11-25) — session state is kept in-process. Multiple replicas break active sessions because there is no shared session store (Redis, Durable Objects, etc.). A single-replica Render deployment routes all requests to one process, so no sticky-session LB is needed today.

Before scaling handshake-era clients beyond one instance: either add a shared session store or configure your edge load balancer to route on the Mcp-Session-Id header with a stick-table and an appropriate TTL.


MCP Protocol Version

ItemValue
Served via the initialize handshake2024-11-05 … 2025-11-25 — the handshake ceiling
Served via the per-request envelope2026-07-28
Who picksThe client's first request, once per connection. A request carrying the 2026-07-28 _meta envelope opens a modern connection; anything else opens a handshake connection.
SDKmcp[cli]>=2.0.0,<3
Cache hintstools/list, prompts/list, resources/list, resources/templates/list and server/discover: ttlMs 300000, cacheScope public
serverInfo.versionThe package version (openlex_mcp.__version__), in every server/discover result
Deprecated in 2026-07-28, not usedLogging (ctx.info/ctx.warning, SEP-2577) — status goes into the tool result, diagnostics into the server log
Pinned insrc/openlex_mcp/server.py — MCP_PROTOCOL_VERSION constant

Update policy

  1. When mcp is upgraded (via Dependabot PR), verify the protocol version in the SDK release notes.
  2. If the protocol version changes, update MCP_PROTOCOL_VERSION in server.py, regenerate docs/tool-hashes.json (PYTHONPATH=src python scripts/gen_tool_hashes.py --write), and note the change in CHANGELOG.md.
  3. Run pytest tests/ -m "not live" to confirm compatibility before merging.

Project Structure

openlex-mcp/├── src/openlex_mcp/│   ├── __init__.py              # Package│   ├── __main__.py              # Entry point for python -m│   ├── server.py                # 8 MCP tool definitions (FastMCP) + Settings│   ├── responses.py             # Typed structured response envelopes (SDK-002)│   ├── logging_config.py        # structlog JSON logging setup (OBS-003)│   ├── net.py                   # SSRF/egress-hardened outbound HTTP│   ├── api_client.py            # zh.ch HTTP client + metadata extraction│   ├── data_cache.py            # SQLite + FTS5 cache management│   └── law_parser.py            # Article extraction from law texts├── tests/                       # 89 unit tests (parser, cache, net, tools…)├── scripts/gen_tool_hashes.py   # Tool-definition hash snapshot (SEC-022)├── docs/                        # network-egress, secret-management, tool-hashes├── .github/workflows/ci.yml     # GitHub Actions (Python 3.11/3.12/3.13)├── .github/dependabot.yml       # Monthly grouped dependency PRs (ARCH-012)├── Dockerfile                   # Hardened multi-stage build (SEC-007/SCALE-004)├── compose.yml                  # Resource limits for local testing (SCALE-006)├── pyproject.toml├── claude_desktop_config.json   # Example config for Claude Desktop├── CHANGELOG.md├── ROADMAP.md                   # Phase plan + accepted-risk register├── CONTRIBUTING.md              # Contribution guide (English)├── CONTRIBUTING.de.md           # Contribution guide (German)├── SECURITY.md                  # Security policy (English)├── SECURITY.de.md               # Security policy (German)├── LICENSE├── README.md                    # This file (English)└── README.de.md                 # German version

Tool output format

All tools return a structured response envelope (not Markdown text), so MCP clients receive structuredContent they can parse directly:

jsonc
{  "source": "Kanton Zürich Rechtssammlung — HuggingFace … & zh.ch",  "provenance": "cache",          // cache | live | parser | cache+parser | none  "result_type": "law_summaries", // law_summaries | law_detail | articles | metadata | cache_status  "count": 2,  "message": null,                // human-readable guidance for empty/edge results  "results": [ /* typed items */ ]}

Known Limitations

  • HuggingFace dataset: The html_content field is unreliable (cross-contaminated between laws); the server uses pdf_content instead, which is correct but has PDF extraction artefacts (hyphenation, layout artefacts)
  • Article parser: PDF text extraction sometimes merges article boundaries; complex nested articles may not parse perfectly
  • Initial load: First start requires ~25s to download and index 974 laws from HuggingFace (~38 MB SQLite database)
  • zh.ch metadata: No official API; metadata extraction relies on HTML patterns that may change
  • Offline mode: Full-text search works offline after initial load; live metadata requires internet
  • The corpus is frozen at 2023-01-01. This is the limitation that matters most for a legal server, and it was the one not stated. The newest version in the entire dataset carries version_active_since = 2023-01-01; the HuggingFace dataset itself was last touched 2024-10-10. The 24-hour cache TTL and provenance="cache" describe where an answer came from, not how old the laws in it are. Every response now carries corpus_as_of and corpus_note alongside provenance.
  • Repeals after the cut-off are invisible. All 974 entries carry is_active = True and not one has a version_inactive_since. That is not a server bug — the source lists only the statutes in force at snapshot time. The consequence is what matters: a law repealed since then still appears to be in force. Consult the ZH-Lex permalink for the operative text.
  • zhlaw_update_cache does not make the laws newer. It re-downloads the same frozen dataset. Its docstring previously read "only call when law search results seem outdated", which suggested exactly the effect it does not have.

Safety & Limits

AspectDetails
AccessRead-only (readOnlyHint: true) — the server cannot modify or delete any data
Personal dataNo personal data — all sources are aggregated, public legal texts
Rate limitsBuilt-in per-query caps (max 50 search results, 5000 chars content preview)
Timeout30 seconds per HTTP call to zh.ch
EgressOutbound requests are restricted to an allow-list (www.zh.ch over HTTPS, plus the HTTP-only legacy permalink host www.zhlex.zh.ch), with SSRF IP-blocking and DNS-pinning — see docs/network-egress.md
AuthenticationNo API keys required — HuggingFace dataset is public, zh.ch is open
Security posture (Lethal Trifecta)Score 1 / 3: public data only (no private/sensitive data) ✓ · GET-only egress to *.zh.ch — no POST, no webhooks, no email ✓ · no code execution ✓. Structurally safe by design.
Session handlingMcp-Session-Id generated and managed by the MCP SDK (cryptographically secure UUIDs). No user-identity binding — auth_model=none is correct for public read-only data. If authentication is ever added, bind sessions to the validated OAuth sub claim before deployment.
SecretsNo secrets held — all data sources are public. See docs/secret-management.md.
LicensesLaw data: CC-BY-SA 4.0 (rcds/swiss_legislation); zh.ch metadata: public
Terms of ServiceSubject to ToS of HuggingFace and Canton Zurich
DisclaimerThis server provides legal texts for informational purposes only — it does not constitute legal advice

To report a vulnerability, see the Security Policy.


Testing

bash
# Unit + contract tests (no network) — this is what CI runsPYTHONPATH=src pytest tests/ -m "not live"
# Live tests against zh.ch and HuggingFacePYTHONPATH=src pytest tests/ -m "live"
# Re-measure the corpus date and the live hostsPYTHONPATH=src python scripts/record_fixtures.py

150 tests — 142 offline, 8 live. Eight tools, eight live tests: the best coverage in this portfolio, which is why the finding here is not about mechanics but about a confusion between two questions. provenance="cache" answers where an answer came from; corpus_as_of answers how old the laws in it are. Only the first was ever answered, and the second is the one a user means when they ask "is this current?".

A measurement limit, deliberately not resolved by editing a test

test_live_get_law_metadata fails in the recording environment: zhlex.zh.ch is not reachable from it. Nothing follows from that. Public DNS resolves the host (NOERROR, 194.247.8.174) and an NXDOMAIN control shows the query discriminates — so the limit is the environment's, not the source's.

The test was therefore left untouched. A test you see red because your own network cannot get out is not a test to rewrite; rewriting it would leave you measuring your own environment instead of the source. PROVENANCE.md records this as open.


Changelog

See CHANGELOG.md


Roadmap

See ROADMAP.md


Contributing

See CONTRIBUTING.md


Security

See SECURITY.md


License

MIT License — see LICENSE


Author

Hayal Oezkan · malkreide


Credits & Related Projects

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):

json
{  "mcpServers": {    "openlex-mcp": {      "command": "uvx",      "args": [        "openlex-mcp"      ]    }  }}

來源:README.md,提交 0fac9c8

工具

0
SourceWeft 尚未在沙箱中啟動過該軟體包,因此其工具未知。

版本歷史

2
  1. v0.3.0最新Sep 27, 2026
  2. v0.2.5Sep 16, 2026

評分

已滿足適用檢查項的 50%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通25 分

    請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。

  • 已宣告認證方式10 分

    請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。

  • 已列出工具15 分

    請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。

  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分