π¨π Part of the Swiss Public Data MCP Portfolio
βοΈ openlex-mcp
MCP Server for Canton Zurich legislation (ZH-Lex) β full-text search, article extraction, and education law tools for ~970 cantonal laws
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.
| Source | Data | Access |
|---|---|---|
| HuggingFace | 974 ZH laws β full text (PDF extracts) | Cached locally as SQLite + FTS5 |
| zh.ch ZH-Lex | Current metadata, PDF links, validity status | Live 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
# Clone the repository
git clone https://github.com/malkreide/openlex-mcp.git
cd openlex-mcp
# Install
pip install -e .
# or with uv:
uv pip install -e .
Quickstart
# 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):
{
"mcpServers": {
"openlex": {
"command": "python",
"args": ["-m", "openlex_mcp.server"]
}
}
}
Or with the installed entry point:
{
"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):
- 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
| Tool | Description |
|---|---|
openlex__zhlaw_search_laws | Full-text search across all ~970 ZH laws (FTS5 + BM25 ranking) |
openlex__zhlaw_get_law | Retrieve a law by LS number (e.g. 412.100) or abbreviation (e.g. VSG) |
openlex__zhlaw_list_laws | List and filter laws by legal area prefix |
openlex__zhlaw_find_education_laws | Specialized search in education law (LS 412.x series) |
Article Extraction
| Tool | Description |
|---|---|
openlex__zhlaw_get_article | Extract a specific article from a law (e.g. Art. 28 VSG) |
openlex__zhlaw_search_articles | Search within all articles of a specific law |
Metadata & Cache
| Tool | Description |
|---|---|
openlex__zhlaw_get_law_metadata | Get live metadata from zh.ch (PDF links, validity status) |
openlex__zhlaw_update_cache | Refresh the local data cache from HuggingFace |
Key Legal Area Prefixes (LS Numbers)
| Prefix | Legal Area | Example |
|---|---|---|
131 | Constitution and popular rights | Kantonsverfassung |
170 | Administrative procedure | Datenschutzgesetz |
331 | Tax law | Steuergesetz |
412 | Education and schools | Volksschulgesetz (VSG) |
700 | Spatial planning and building | Planungs- und Baugesetz |
810 | Health | Gesundheitsgesetz |
Example Use Cases
| Query | Tool |
|---|---|
| "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
| Source | Protocol | Coverage | Auth | License |
|---|---|---|---|---|
HuggingFace rcds/swiss_legislation | Datasets API | 974 ZH laws (full text) | None | CC-BY-SA 4.0 |
| zh.ch ZH-Lex | HTTP/HTML | Current metadata, PDFs | None | Public |
| LexFind.ch | HTTP | Cross-cantonal links | None | Public |
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 Streamable-HTTP transport keeps session state in-process (FastMCP default). This has two implications:
- Single-instance only β horizontal scaling (multiple replicas) breaks active sessions because there is no shared session store (Redis, Durable Objects, etc.).
- No sticky-session LB needed today β a single-replica Render deployment naturally routes all requests to one process.
Before scaling 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
| Item | Value |
|---|---|
| Supported protocol version | 2025-11-25 |
| SDK | mcp[cli] >= 1.3.0 (FastMCP) |
| Pinned in | src/openlex_mcp/server.py β MCP_PROTOCOL_VERSION constant |
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 > docs/tool-hashes.json), and note the change inCHANGELOG.md. - 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 # Weekly 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:
{
"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_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
Safety & Limits
| Aspect | Details |
|---|---|
| Access | Read-only (readOnlyHint: true) β the server cannot modify or delete any data |
| Personal data | No personal data β all sources are aggregated, public legal texts |
| Rate limits | Built-in per-query caps (max 50 search results, 5000 chars content preview) |
| Timeout | 30 seconds per HTTP call to zh.ch |
| Egress | Outbound 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 |
| Authentication | No 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 handling | Mcp-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. |
| Secrets | No secrets held β all data sources are public. See docs/secret-management.md. |
| Licenses | Law data: CC-BY-SA 4.0 (rcds/swiss_legislation); zh.ch metadata: public |
| Terms of Service | Subject to ToS of HuggingFace and Canton Zurich |
| Disclaimer | This server provides legal texts for informational purposes only β it does not constitute legal advice |
To report a vulnerability, see the Security Policy.
Testing
# Unit tests (no API key required)
PYTHONPATH=src pytest tests/ -m "not live"
# Integration tests (live API calls)
pytest tests/ -m "live"
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):
{
"mcpServers": {
"openlex-mcp": {
"command": "uvx",
"args": [
"openlex-mcp"
]
}
}
}