Openlex Mcp — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Openlex Mcp (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
🇨🇭 Part of the [Swiss Public Data MCP Portfolio](https://github.com/malkreide)
MCP Server for Canton Zurich legislation (ZH-Lex) — full-text search, article extraction, and education law tools for ~970 cantonal laws
<p align="center"> <img src="assets/demo.png" alt="Demo: Claude searches Zurich education law via MCP tool call" width="720"> </p>
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 8000By 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 theMcp-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 versionAll 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
<!-- mcp-name: io.github.malkreide/openlex-mcp -->
<!-- BEGIN GENERATED: install -->
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"
]
}
}
}<!-- END GENERATED: install -->
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.