
Openlex Mcp
io.github.malkreidePublished by GitHub account malkreidev0.3.0Updated Oct 24, 2026
Canton Zurich legislation via ZH-Lex with full-text search and article extraction
Overview
Read-only MCP server giving an assistant full-text search and article-level access to ~970 Canton Zurich laws (ZH-Lex), with live metadata from zh.ch.
- What you can do
- Search and browse laws: full-text search with BM25 ranking, look up a law by LS number or abbreviation, list laws by legal area, and run a dedicated education-law search.
- Extract articles: pull a specific article (Art./§) from a law and search within all articles of one law.
- Check metadata and cache: fetch live zh.ch metadata such as PDF links and validity status, and refresh the local data cache from HuggingFace.
- When to use it
- Useful when questions concern Zurich cantonal legislation — finding laws on a topic, quoting a specific article, or checking whether a statute is listed as in force. Built with the City of Zurich school department in mind, so education law (LS 412.x) is well covered, but it spans all cantonal areas.
- Requirements
- Local Python 3.11+ process, run via uvx or pip; internet access for the first download and indexing of the dataset (about 25 seconds) and for live zh.ch metadata. No API key or account. Optional HTTP transport binds to localhost by default, configurable via MCP_HOST and MCP_PORT.
Installation
In SourceWeft
- Open Openlex Mcp in the dashboard and add it to a workspace.
- Enable the server for the chats that should use its tools.
Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.
Other MCP clients
Follow the launch instructions in the repository.
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
[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.
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
Quickstart
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):
Or with the installed entry point:
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):
- Push/fork the repository to GitHub
- On render.com: New Web Service → connect GitHub repo
- Set start command:
python -m openlex_mcp.server --http --port 8000 - Set environment variable
MCP_HOST=0.0.0.0so the container is reachable (the code default is127.0.0.1; Render sets theRENDERenv var, so no NeighborJack warning is logged) - Set
MCP_CORS_ORIGINS=https://claude.aiso the browser can read theMcp-Session-Idheader (comma-separated list; no wildcard — defaults to empty, i.e. no cross-origin access) - 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
Article Extraction
Metadata & Cache
Key Legal Area Prefixes (LS Numbers)
Example Use Cases
Architecture
Data Source Characteristics
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_metaenvelope; the server answers without issuing anMcp-Session-Idand keeps no session between requests. Any replica can answer any request; no shared store, no sticky routing. (subscriptions/listenstreams 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 to2025-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
Update policy
- When
mcpis upgraded (via Dependabot PR), verify the protocol version in the SDK release notes. - If the protocol version changes, update
MCP_PROTOCOL_VERSIONinserver.py, regeneratedocs/tool-hashes.json(PYTHONPATH=src python scripts/gen_tool_hashes.py --write), and note the change inCHANGELOG.md. - Run
pytest tests/ -m "not live"to confirm compatibility before merging.
Project Structure
Tool output format
All tools return a structured response envelope (not Markdown text), so MCP
clients receive structuredContent they can parse directly:
Known Limitations
- HuggingFace dataset: The
html_contentfield is unreliable (cross-contaminated between laws); the server usespdf_contentinstead, 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 andprovenance="cache"describe where an answer came from, not how old the laws in it are. Every response now carriescorpus_as_ofandcorpus_notealongsideprovenance. - Repeals after the cut-off are invisible. All 974 entries carry
is_active = Trueand not one has aversion_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_cachedoes 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
To report a vulnerability, see the Security Policy.
Testing
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
- Data: rcds/swiss_legislation — HuggingFace dataset (CC-BY-SA 4.0)
- ZH-Lex: zh.ch Gesetzessammlung — Official Canton Zurich legal collection
- LexFind: lexfind.ch — Cross-cantonal legislation database
- Protocol: Model Context Protocol — Anthropic / Linux Foundation
- Related: swiss-courts-mcp — Law text + case law = complete legal research
- Related: zurich-opendata-mcp — Law text + city council decisions = full context
- Portfolio: Swiss Public Data MCP Portfolio
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):
Source: README.md at commit 0fac9c8
Tools
0Version history
2- v0.3.0LatestSep 27, 2026
- v0.2.5Sep 16, 2026
Score
50% of the applicable checks met
How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.
- Reached in the last 30 days25 pts
Keep the endpoint answering the MCP handshake; SourceWeft probes it every 30 days.
- Auth declared10 pts
Declare the credential headers in the registry entry, or keep the endpoint answering the probe so its auth can be classified.
- Tools listed15 pts
Answer tools/list without a credential, or ship a package that starts in a sandbox without one.
- DescriptionRequired15 pts
- Logo10 pts
- Documentation15 pts
- License10 pts