Connects Claude to the entire legal code of Canton Zurich through the ZH-Lex system. Eight tools expose full-text search across 970 cantonal laws with FTS5 indexing, article extraction with paragraph parsing, and live metadata from zh.ch. Built originally for Zurich's school department but covers all cantonal law from tax code to building regulations. Hybrid architecture caches full legal text locally from HuggingFace while pulling current validity status and PDF links from the official site. Ships with education law shortcuts for the LS 412.x series. No API keys needed. Reach for this when working with Swiss cantonal legislation, parsing specific articles, or building tools that need structured access to Zurich's Gesetzessammlung.
🇨🇭 Part of the Swiss Public Data MCP Portfolio
MCP Server for Canton Zurich legislation (ZH-Lex) — full-text search, article extraction, and education law tools for ~970 cantonal laws
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'."
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.
# 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 .
# 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
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'?"
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:
~/Library/Application Support/Claude/claude_desktop_config.json%APPDATA%\Claude\claude_desktop_config.jsonFor use via claude.ai in the browser (e.g. on managed workstations without local software):
Render.com (recommended):
python -m openlex_mcp.server --http --port 8000MCP_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)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)https://your-app.onrender.com/sse💡 "stdio for the developer laptop, SSE for the browser."
| 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) |
| 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 |
| 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 |
| 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 |
| 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 |
┌─────────────────┐ ┌──────────────────────────────┐ ┌──────────────────────────┐
│ 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) │
└──────────────────────────┘
| 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 |
All 8 endpoints are exposed as Tools rather than MCP Resources. Rationale:
zhlex://laws/{sr_number}) are a future consideration for Phase 2 if clients benefit from resource-level caching or subscriptions.The Streamable-HTTP transport keeps session state in-process (FastMCP default). This has two implications:
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.
| 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 |
mcp is upgraded (via Dependabot PR), verify the protocol version in the SDK release notes.MCP_PROTOCOL_VERSION in server.py, regenerate docs/tool-hashes.json (PYTHONPATH=src python scripts/gen_tool_hashes.py > docs/tool-hashes.json), and note the change in CHANGELOG.md.pytest tests/ -m "not live" to confirm compatibility before merging.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
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 */ ]
}
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)| 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.
# Unit tests (no API key required)
PYTHONPATH=src pytest tests/ -m "not live"
# Integration tests (live API calls)
pytest tests/ -m "live"
See CHANGELOG.md
See ROADMAP.md
See CONTRIBUTING.md
See SECURITY.md
MIT License — see LICENSE
Hayal Oezkan · malkreide
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"
]
}
}
}
io.github.pipeworx-io/brave-search
marcopesani/mcp-server-serper
brave/brave-search-mcp-server
com.mcparmory/google-search-console
acamolese/google-search-console-mcp
io.github.sarahpark/google-search-console