Swiss court decisions via entscheidsuche.ch, including federal and cantonal courts
Swiss court decisions via entscheidsuche.ch, including federal and cantonal courts
swiss-courts-mcp · v0.3.0 (latest)
by Malkreide
Part of the Swiss Public Data MCP Portfolio
🏛️ swiss-courts-mcp
MCP Server for Swiss court decisions — Federal Supreme Court (BGer), Federal Administrative Court (BVGer), Federal Criminal Court (BStGer), and all 26 cantonal courts via entscheidsuche.ch
Overview
Access Swiss court decisions from all judicial levels through a single MCP interface. Combines full-text search with structured filters for canton, court level, date range, and law references.
🎯 Anchor demo query: "Find Federal Supreme Court case law on data protection (Art. 25 DSG) since 2020 — and if entscheidsuche.ch is down, still answer from the offline dump, clearly flagged."
| Source | Coverage | Data |
|---|---|---|
| entscheidsuche.ch (live, default) | Federal + 26 cantons | Court decisions since ~2000 |
| SCD dump (offline fallback) | Federal Supreme Court only, 2007–2024 | Metadata/regesten, no full text |
Synergy with fedlex-mcp: Legislation (SR) + case law = complete legal research.
Availability: entscheidsuche.ch is non-profit infrastructure without an SLA. When it is unreachable, the server transparently falls back to a cached public dump (see Offline fallback). Every response declares its origin (source: "live" | "dump"), and dump answers carry a coverage_note — the fallback is partial, not equivalent.
Features
- Full-text search across all Swiss court decisions
- Multi-stage law reference search with regex parser and Elasticsearch boost scoring
- Dedicated Federal Supreme Court search with chamber filter
- Canton and court level filtering
- Recent decisions feed
- Court taxonomy listing
- Decision statistics with aggregations
- Trilingual support (German, French, Italian)
- Offline fallback to a cached public dump when entscheidsuche.ch is unreachable — with explicit provenance on every response
- No API key required
Prerequisites
- Python 3.11 or higher
- An MCP-compatible client (Claude Desktop, Cursor, Windsurf, etc.)
Installation
pip install swiss-courts-mcp
Or install from source:
git clone https://github.com/malkreide/swiss-courts-mcp.git
cd swiss-courts-mcp
pip install -e ".[dev]"
Quickstart
# Run directly
swiss-courts-mcp
# Or via Python module
python -m swiss_courts_mcp
Configuration
Claude Desktop
Add to your claude_desktop_config.json:
{
"mcpServers": {
"swiss-courts": {
"command": "python",
"args": ["-m", "swiss_courts_mcp"]
}
}
}
Cloud Deployment (HTTP transport)
The HTTP transport is off by default. The default bind host is 127.0.0.1
(loopback only) — 0.0.0.0 must be opted into explicitly (the Dockerfile does
this). Running HTTP without authentication logs a warning; only do so behind an
authenticating reverse proxy.
# Local HTTP (loopback), no auth — development only
swiss-courts-mcp --http --port 8000
# Container (binds 0.0.0.0, auth enabled) — see Dockerfile
docker build -t swiss-courts-mcp .
docker run -p 8000:8000 -e MCP_AUTH_SECRET="$(openssl rand -hex 32)" swiss-courts-mcp
Relevant environment variables (see .env.example):
| Variable | Default | Purpose |
|---|---|---|
MCP_HOST |
127.0.0.1 |
Bind host. Set to 0.0.0.0 only in containers. |
MCP_PORT |
8000 |
Bind port. |
MCP_ALLOW_PUBLIC_BIND |
false |
Suppress the 0.0.0.0 warning (containers). |
MCP_STATELESS_HTTP |
true |
Stateless HTTP → horizontal scaling without sticky sessions. |
MCP_AUTH_ENABLED |
false |
Enable bearer-token auth for HTTP. |
MCP_AUTH_SECRET |
— | HS256 signing key (dev). |
MCP_OAUTH_JWKS_URL |
— | JWKS URL for RS256 validation (production). |
MCP_REQUIRED_SCOPES |
— | Comma-separated required scopes. |
MCP_CORS_ORIGINS |
— | Comma-separated allowed origins (no wildcard in prod). |
Authentication validates the user identity from the JWT sub claim only; see
ADR 0001.
Offline fallback (env)
| Variable | Default | Purpose |
|---|---|---|
SWISS_COURTS_FALLBACK_ENABLED |
true |
Master switch. 0 disables the dump fallback (live-only). |
SWISS_COURTS_FORCE_DUMP |
false |
Force the dump path (skip live) — for pre-warming the cache or offline testing. |
SWISS_COURTS_CACHE_DIR |
platformdirs cache |
Override the cache directory for the downloaded dump. |
SWISS_COURTS_DUMP_RECORD |
14867950 |
Zenodo record id of the SCD dump to use. |
Pre-warm the cache (downloads the ~120 MB SCD CSV once, so the first real outage does not pay the download cost):
SWISS_COURTS_FORCE_DUMP=1 python -m swiss_courts_mcp # then issue one search
MCP Protocol Version
This server pins MCP protocol version 2025-11-25 (constant
PROTOCOL_VERSION in server.py). A regression test detects drift against the
installed SDK so a protocol bump is a conscious change (version + CHANGELOG +
this section). SDK updates land monthly via Dependabot.
Project Phase
Phase 1 — read-only (see ROADMAP.md). All tools are
readOnlyHint: true; there are no writing or destructive operations. A move to
Phase 2 (write) requires a clean re-audit and the gates listed in the roadmap.
Available Tools
Court Decision Search
| Tool | Description |
|---|---|
search_court_decisions |
Full-text search across all court decisions with canton, court level, and date filters |
get_court_decision |
Retrieve a single decision by its unique signature |
search_bger_decisions |
Search Federal Supreme Court decisions with optional chamber filter |
search_by_law_reference |
Find decisions citing a specific law article (e.g., "Art. 8 BV") |
Court Information
| Tool | Description |
|---|---|
list_courts |
List all indexed courts, optionally filtered by canton |
get_recent_decisions |
Latest decisions, filterable by canton and court level |
get_decision_statistics |
Statistics on indexed decisions by canton and year |
get_fallback_status |
Offline-dump cache state, coverage, version, pre-warming (read-only) |
Tool Annotations
All eight tools share the same hints — they are read-only, idempotent, non-destructive, and reach an external system:
| Annotation | Value |
|---|---|
readOnlyHint |
true |
destructiveHint |
false |
idempotentHint |
true |
openWorldHint |
true |
A rechtsrecherche prompt is also provided (a second MCP primitive
alongside tools).
Example Use Cases
| Use Case | Tool Chain |
|---|---|
| Research case law on data protection | search_court_decisions("Datenschutz") |
| Find practice on a constitutional right | search_by_law_reference("Art. 8 BV") |
| Latest Federal Supreme Court rulings | search_bger_decisions("Arbeitsrecht", date_from="2024-01-01") |
| Combined: Law text + case law | fedlex_search_laws("DSG") then search_by_law_reference("Art. 25 DSG") |
→ More use cases by audience →
Architecture
┌─────────────────────────────────────┐
│ MCP Client (LLM) │
│ Claude / Cursor / Windsurf │
└──────────────┬──────────────────────┘
│ MCP Protocol
┌──────────────▼──────────────────────────────┐
│ swiss-courts-mcp │
│ 8 tools · Pydantic validation │
│ Elasticsearch query builder │
│ Provenance envelope: source = live | dump │
└───────┬──────────────────────────────┬──────┘
│ ① live (default) │ ② fallback
│ HTTPS POST/GET │ on bot-block / 5xx / 429 /
│ │ timeout, or SWISS_COURTS_FORCE_DUMP=1
┌───────▼──────────────────┐ ┌───────▼───────────────────────────────┐
│ entscheidsuche.ch │ │ SCD dump — Zenodo 14867950 (CC BY) │
│ Elasticsearch backend │ │ lazy download → platformdirs cache │
│ Federal + 26 cantons │ │ → local SQLite search │
│ no auth · no SLA │ │ BGer only · 2007–2024 · no full text │
└──────────────────────────┘ └───────────────────────────────────────┘
Architecture decision
This server uses Architecture C (metadata-only offline fallback), delivered via lazy download (Option A mechanics) — decided after a live probe on 2026-07-19:
- Live-first, always. entscheidsuche.ch remains the sole source on success; its behaviour is unchanged. The fallback only engages on an availability failure (bot-block, HTTP 5xx/429, timeout, connect error) or when forced.
- Source: the SCD dump (Zenodo
10.5281/zenodo.14867950, Version 2024-3, CC BY 4.0), the ~120 MB CSV — metadata/regesten only, no full text. The 375 MB full-text Parquet and its heavypyarrowdependency were rejected: a partial fallback does not justify the footprint, and full text would fake an equivalence that does not exist (BGer only). - A second candidate was rejected: Zenodo
5529712("SwissJudgmentPrediction") is CC BY-NC-SA 4.0 — incompatible with this MIT project. - Consequences: the CSV is cached on disk (
platformdirs) and searched via SQLite; update detection uses the Zenodo versions API (conceptrecid7793043). Every response declaressource(live/dump) and dump responses add acoverage_note. CC-BY attribution ships in the tool output, not only here.
Offline fallback
The fallback is a behaviour of the existing tools, not a separate search
tool (only get_fallback_status was added, for transparency). It is
partial, not equivalent to the live source:
- Only the Federal Supreme Court (BGer/BGE), 2007–2024, no full text.
- Bundesverwaltungsgericht, Bundesstrafgericht and all 26 cantons are NOT covered. A cantonal or non-BGer query in dump mode returns an explicit "not covered" answer — never a silent empty result.
get_court_decisionis best-effort in dump mode: SCD case ids (docref, e.g.1C_517/2016) differ from entscheidsuche signatures, so some lookups are honestly reported as non-resolvable.- If neither live nor dump is available, tools return a clear, actionable error (no crash, no stack trace).
Inspect the cache and coverage at any time with get_fallback_status.
Safety & Limits
| Aspect | Details |
|---|---|
| Access | Read-only (readOnlyHint: true) — the server cannot modify or delete any data |
| Personal data | No personal data — all decisions are public court rulings |
| Rate limits | Built-in per-query caps (max 50 results per search, 50 aggregation buckets) |
| Timeout | 30 seconds per API call |
| Data source auth | No API keys required — entscheidsuche.ch is publicly accessible |
| HTTP transport auth | Optional bearer-token auth (JWT, sub-claim identity); see ADR 0001 |
| Egress | Code-layer allow-lists (entscheidsuche.ch for live; zenodo.org for the offline dump), HTTPS-enforced; see egress policy |
| Error masking | Internal exceptions are logged server-side only; clients receive friendly messages |
| Secrets | No secrets in code/logs; .env git-ignored, Gitleaks on PRs; see secret management |
| Licenses | Court decisions are public domain under Swiss law (BGG Art. 27) |
| Terms of Service | Subject to entscheidsuche.ch usage terms — please be kind to the server |
Project Structure
swiss-courts-mcp/
├── src/
│ └── swiss_courts_mcp/
│ ├── __init__.py
│ ├── __main__.py
│ ├── server.py # MCP server, 8 tools + 1 prompt, lifespan, auth wiring
│ ├── api_client.py # HTTP client, ES query builder, egress allow-list
│ ├── fallback.py # offline dump layer (Zenodo → cache → SQLite)
│ ├── auth.py # JWT bearer-token verifier (HTTP transport)
│ ├── config.py # Settings object (env-driven)
│ ├── logging_config.py # structured logging on stderr
│ └── models.py # structured response envelope (provenance: live|dump)
├── tests/ # unit (respx-mocked) + live + security tests
├── docs/ # egress, secret-management, ADRs
├── .github/workflows/ # ci · security (gitleaks) · live · publish
├── Dockerfile # hardened container (non-root, 0.0.0.0 only here)
├── ROADMAP.md
├── pyproject.toml · CHANGELOG.md · LICENSE
├── CONTRIBUTING.md · CONTRIBUTING.de.md
├── SECURITY.md · SECURITY.de.md
└── README.md · README.de.md
Note (single-file tools): the 8 tools live in
server.pyrather than atools/package. At this count a single module stays readable; the registry (register_tools) keeps registration declarative. This is a deliberate deviation from the "split when > 5 tools" convention and will be revisited if the tool count grows. The offline-fallback logic is isolated infallback.py, cleanly separated from the live client.
Known Limitations
- Search is limited to decisions indexed by entscheidsuche.ch (not all decisions are publicly available)
- Full-text document content is not returned — only metadata, title, and abstract
- Statistics depend on Elasticsearch aggregation support of the backend
- The court taxonomy structure from
Facetten_alle.jsonmay vary
Offline fallback (partial coverage — read this): the fallback is a safety net for availability, not an equivalent mirror of the live source:
- Court scope: Federal Supreme Court only (BGer/BGE). Bundesverwaltungsgericht, Bundesstrafgericht and all 26 cantonal courts are not covered.
- Time span: 2007 – December 2024 (the SCD dump's range). Decisions outside this window are not in the dump.
- Content: metadata/regesten only — no full text offline.
- Update latency: the SCD dump is refreshed roughly quarterly on Zenodo, so
the offline data lags the live index.
get_fallback_statusreports the cached version and can check Zenodo for a newer one. - Law-reference search offline only matches references named in the decision's
subject/regest (
topic/issue) — there is no offline cited-law index. - Responses always disclose their origin via
source(live/dump) and acoverage_note; the server never silently narrows coverage.
Testing
Unit tests mock all HTTP with respx; live tests hit the real API and run in a
separate nightly workflow (live.yml), never
blocking PRs.
Run from the project root with PYTHONPATH=src:
# Unit tests (HTTP mocked) — what CI runs
PYTHONPATH=src pytest tests/ -v -m "not live"
# Live API tests (real entscheidsuche.ch + Zenodo)
PYTHONPATH=src pytest tests/ -v -m live
# Linting
ruff check src/ tests/
ruff format src/ tests/
The offline-fallback tests use a small committed schema fixture
(tests/fixtures/scd_sample.csv) and mock the Zenodo download with respx —
the full ~120 MB dump is never committed or downloaded in CI.
Changelog
See CHANGELOG.md.
Contributing
See CONTRIBUTING.md.
Security
See SECURITY.md for the security posture and how to report a vulnerability.
License
Author
Hayal Oezkan · malkreide
Credits & Related Projects
- entscheidsuche.ch — Swiss court decision search engine (live source)
- Swiss Federal Supreme Court Dataset (SCD) — offline fallback source, CC BY 4.0: Geering, F. & Merane, J. (2025). Swiss Federal Supreme Court Dataset (SCD), Version 2024-3. Zenodo. https://doi.org/10.5281/zenodo.14867950
- fedlex-mcp — MCP Server for Swiss federal law (legislation synergy)
- zurich-opendata-mcp — MCP Server for Zurich open data
- Model Context Protocol — Open protocol for AI tool integration
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": {
"swiss-courts-mcp": {
"command": "uvx",
"args": [
"swiss-courts-mcp"
]
}
}
}