Back to Discover

swiss-courts-mcp

connector

malkreide

Swiss court decisions via entscheidsuche.ch, including federal and cantonal courts

View on GitHub
0 starsSynced Aug 2, 2026

Install to Claude Code

/plugin marketplace add malkreide/swiss-courts-mcp

README

Part of the Swiss Public Data MCP Portfolio

๐Ÿ›๏ธ swiss-courts-mcp

Version License: MIT Python 3.11+ MCP No Auth Required CI

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

Deutsche Version

Demo: Claude searches Swiss court decisions via MCP tool call


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."

SourceCoverageData
entscheidsuche.ch (live, default)Federal + 26 cantonsCourt decisions since ~2000
SCD dump (offline fallback)Federal Supreme Court only, 2007โ€“2024Metadata/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):

VariableDefaultPurpose
MCP_HOST127.0.0.1Bind host. Set to 0.0.0.0 only in containers.
MCP_PORT8000Bind port.
MCP_ALLOW_PUBLIC_BINDfalseSuppress the 0.0.0.0 warning (containers).
MCP_STATELESS_HTTPtrueStateless HTTP โ†’ horizontal scaling without sticky sessions.
MCP_AUTH_ENABLEDfalseEnable 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)

VariableDefaultPurpose
SWISS_COURTS_FALLBACK_ENABLEDtrueMaster switch. 0 disables the dump fallback (live-only).
SWISS_COURTS_FORCE_DUMPfalseForce the dump path (skip live) โ€” for pre-warming the cache or offline testing.
SWISS_COURTS_CACHE_DIRplatformdirs cacheOverride the cache directory for the downloaded dump.
SWISS_COURTS_DUMP_RECORD14867950Zenodo 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

ToolDescription
search_court_decisionsFull-text search across all court decisions with canton, court level, and date filters
get_court_decisionRetrieve a single decision by its unique signature
search_bger_decisionsSearch Federal Supreme Court decisions with optional chamber filter
search_by_law_referenceFind decisions citing a specific law article (e.g., "Art. 8 BV")

Court Information

ToolDescription
list_courtsList all indexed courts, optionally filtered by canton
get_recent_decisionsLatest decisions, filterable by canton and court level
get_decision_statisticsStatistics on indexed decisions by canton and year
get_fallback_statusOffline-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:

AnnotationValue
readOnlyHinttrue
destructiveHintfalse
idempotentHinttrue
openWorldHinttrue

A rechtsrecherche prompt is also provided (a second MCP primitive alongside tools).

Example Use Cases

Use CaseTool Chain
Research case law on data protectionsearch_court_decisions("Datenschutz")
Find practice on a constitutional rightsearch_by_law_reference("Art. 8 BV")
Latest Federal Supreme Court rulingssearch_bger_decisions("Arbeitsrecht", date_from="2024-01-01")
Combined: Law text + case lawfedlex_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 heavy pyarrow dependency 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 (conceptrecid 7793043). Every response declares source (live/dump) and dump responses add a coverage_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_decision is 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

AspectDetails
AccessRead-only (readOnlyHint: true) โ€” the server cannot modify or delete any data
Personal dataNo personal data โ€” all decisions are public court rulings
Rate limitsBuilt-in per-query caps (max 50 results per search, 50 aggregation buckets)
Timeout30 seconds per API call
Data source authNo API keys required โ€” entscheidsuche.ch is publicly accessible
HTTP transport authOptional bearer-token auth (JWT, sub-claim identity); see ADR 0001
EgressCode-layer allow-lists (entscheidsuche.ch for live; zenodo.org for the offline dump), HTTPS-enforced; see egress policy
Error maskingInternal exceptions are logged server-side only; clients receive friendly messages
SecretsNo secrets in code/logs; .env git-ignored, Gitleaks on PRs; see secret management
LicensesCourt decisions are public domain under Swiss law (BGG Art. 27)
Terms of ServiceSubject 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.py rather than a tools/ 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 in fallback.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.json may 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_status reports 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 a coverage_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

MIT


Author

Hayal Oezkan ยท malkreide


Credits & Related Projects

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"
      ]
    }
  }
}

Rendered live from malkreide/swiss-courts-mcp's GitHub README โ€” not stored, always reflects the source repo.

1 Install Method

NameDescriptionCategorySource
pypi packageInstall via pypi (stdio transport)mcp-serverswiss-courts-mcp

0 Comments

Login required
Log in to post a comment or update on this repo.

No comments yet โ€” be the first to share an update.